Laravel 多对多(在同一个用户表/模型上):查询范围以包含指定用户的相关信息

Posted

技术标签:

【中文标题】Laravel 多对多(在同一个用户表/模型上):查询范围以包含指定用户的相关信息【英文标题】:Laravel Many-to-Many (on the same users table/Model): Query scopes to include related for the specified user 【发布时间】:2020-07-06 13:46:26 【问题描述】:

用户可以互相屏蔽。一位用户可以屏蔽许多(其他)用户,一位用户可以被许多(其他)用户屏蔽。 在User 模型中,我有这些多对多 关系:

/**
 * Get the users that are blocked by $this user.
 *
 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 */
public function blockedUsers()

    return $this->belongsToMany(User::class, 'ignore_lists', 'user_id', 'blocked_user_id');


/**
 * Get the users that blocked $this user.
 *
 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 */
public function blockedByUsers()

    return $this->belongsToMany(User::class, 'ignore_lists', 'blocked_user_id', 'user_id');

ignore_lists 是数据透视表,它有 iduser_id'blocked_user_id' 列)

我想创建以下查询范围

1) 包含指定用户 ($id) 阻止的用户:

/**
 * Scope a query to only include users that are blocked by the specified user.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 * @param $id
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeAreBlockedBy($query, $id)

    // How to do this? :)

使用示例: User::areBlockedBy(auth()->id())->where('verified', 1)->get();

2) 包含被指定用户 ($id) 阻止的用户:

/**
 * Scope a query to only include users that are not blocked by the specified user.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 * @param $id
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeAreNotBlockedBy($query, $id)

    // How to do this? :)

使用示例: User::areNotBlockedBy(auth()->id())->where('verified', 1)->get();

3) 包括阻止指定用户 ($id) 的用户:

/**
 * Scope a query to only include users that blocked the specified user.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 * @param $id
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeWhoBlocked($query, $id)

    // How to do this? :)

使用示例: User::whoBlocked(auth()->id())->where('verified', 1)->get();

4) 包含未阻止指定用户 ($id) 的用户:

/**
 * Scope a query to only include users that did not block the specified user.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 * @param $id
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeWhoDidNotBlock($query, $id)

    // How to do this? :)

使用示例: User::whoDidNotBlock(auth()->id())->where('verified', 1)->get();


你会怎么做? 我在Laravel docs 中没有找到任何关于此的内容(也许我错过了)。 (我正在使用 Laravel 6.x

我不确定,但我认为这可以通过两种方式完成:使用 Left Join 或在 whereIn 中使用 原始查询 ...我可能错了,但我认为就性能而言,“左连接”解决方案会更好,对吧? (不确定这一点,也许我完全错了)。

【问题讨论】:

【参考方案1】:

使用join(inner join) 性能优于whereIn 子查询。

mysql 中,IN 子句中的子选择会为外部查询中的每一行重新执行,从而创建O(n^2)

我认为使用whereHaswhereDoesntHave 进行查询会更具可读性。

1) 关系方法blockedUsers()已经包含了被指定user ($id)屏蔽的用户,可以直接使用该方法:

User::where('id', $id)->first()->blockedUsers();

首先考虑应用where('verified', 1),所以你可以使用User::where('verified', 1)->areBlockedBy(auth()->id())这样的查询,范围可以是这样的:

public function scopeAreBlockedBy($query, $id)

    return $query->whereHas('blockedByUsers', function($users) use($id) 
               $users->where('ignore_lists.user_id', $id);
           );


// better performance: however, when you apply another where condition, you need to specify the table name ->where('users.verified', 1)
public function scopeAreBlockedBy($query, $id)

    return $query->join('ignore_lists', function($q) use ($id) 
               $q->on('ignore_lists.blocked_user_id', '=', 'users.id')
                 ->where('ignore_lists.user_id', $id);
           )->select('users.*')->distinct();


我们将join 用于第二个查询,这将提高性能,因为它不需要使用where exists

users 表中超过 300,000 条记录的示例:

解释第一个查询whereHas,它扫描301119+1+1行并采用575ms

解释第二个查询join 扫描3+1 行并采用10.1ms

2) 要包含未被指定的user ($id) 阻止的用户,您可以像这样使用whereDoesntHave 闭包:

public function scopeNotBlockedUsers($query, $id)

    return $query->whereDoesntHave('blockedByUsers', function($users) use ($id)
           $users->where('ignore_lists.user_id', $id);
     );

我更喜欢在这里使用whereDoesntHave 而不是leftJoin。因为当你像下面这样使用leftjoin 时:

User::leftjoin('ignore_lists', function($q) use ($id)                                                             
     $q->on('ignore_lists.blocked_user_id', '=', 'users.id') 
       ->where('ignore_lists.user_id', $id);
)->whereNull('ignore_lists.id')->select('users.*')->distinct()->get();

Mysql需要创建一个临时表来存储所有用户的记录,并结合一些ignore_lists。然后扫描这些记录,找出没有ignore_lists的记录。 whereDosentHave 也会扫描所有用户。对于我的 mysql 服务器,where not existsleft join 快一点。它的执行计划似乎不错。这两个查询的性能差别不大。

对于whereDoesntHave 更具可读性。我会选择whereDoesntHave

3) 包含屏蔽指定的用户 user ($id),使用whereHasblockedUsers 像这样:

public function scopeWhoBlocked($query, $id)

    return $query->whereHas('blockedUsers', function($q) use ($id) 
                $q->where('ignore_lists.blocked_user_id', $id);
           );


// better performance: however, when you apply another where condition, you need to specify the table name ->where('users.verified', 1)
public function scopeWhoBlocked($query, $id)

    return $query->join('ignore_lists', function($q) use ($id) 
               $q->on('ignore_lists.user_id', '=', 'users.id')
                 ->where('ignore_lists.blocked_user_id', $id);
           )->select('users.*')->distinct();

4) 要包含未阻止指定user ($id) 的用户,请将whereDoesntHave 用于blockedByUsers:

public function scopeWhoDidNotBlock($query, $id)

    return $query->whereDoesntHave('blockedUsers', function($q) use ($id) 
                $q->where('ignore_lists.blocked_user_id', $id);
           );

PS:记得在foreign_key 上为ignore_lists 表添加索引。

【讨论】:

scopeAreBlockedByscopeWhoBlocked 是相同的。我认为在scopeAreBlockedBy 中应该是select('blocked_user_id') 而不是select('user_id')where('user_id', $id) 而不是where('blocked_user_id', $id)。第一个也缺少use ($id)。无论如何,我明天会测试这些(并且会等几天,直到我接受正确的答案/添加赏金:))谢谢! @PeraMika 我已将答案更改为 whereHas 和 whereDoesntHave,这样更具可读性 我刚刚测试了第一个 (scopeAreBlockedBy),结果它错了。而不是where('users.id', $id),它应该是where('ignore_lists.user_id', $id)。似乎其他示例也有类似的错误...您能否更正/仔细检查您的答案? 另外,第一个的“加入”版本似乎不起作用,得到Unknown column 'ignore_lists' in 'on clause' ... @PeraMika 对那个语法错误感到抱歉。我已经修好了。【参考方案2】:

您可以使用 Querying Relationship Existence whereHas 和 Querying Relationship Absence whereDoesntHave 查询构建器函数来构建您的结果查询。

我已经包含了每个查询生成的 SQL 代码和查询时间(以毫秒为单位),在具有 1000 个用户的表上的双 Xeon 专用服务器上进行测试。

我们不希望在使用areNotBlockedBywhoDidNotBlock 查询时在结果中获取当前用户,因此这些函数将排除使用$id 的用户。

    包含被指定用户 ($id) 阻止的用户:

    /**
     * Scope a query to only include users that are blocked by the specified user.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeAreBlockedBy($query, $id)
    
        return User::whereHas('blockedByUsers', function($q) use($id) 
            $q->where('user_id', $id);
        );
    
    

    执行:

    User::areBlockedBy(auth()->id())->where('verified', 1)->get();
    

    将生成以下 SQL:

    -- Showing rows 0 - 3 (4 total, Query took 0.0006 seconds.)
    select * from `users` where exists (select * from `users` as `laravel_reserved_9` inner join `ignore_lists` on `laravel_reserved_9`.`id` = `ignore_lists`.`user_id` where `users`.`id` = `ignore_lists`.`blocked_user_id` and `user_id` = ?) and `verified` = ?
    

    包含被指定用户 ($id) 阻止的用户:

    /**
     * Scope a query to only include users that are not blocked by the specified user.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeAreNotBlockedBy($query, $id)
    
        // It will exclude the user with $id
        return User::where('id', '!=', $id)
            ->whereDoesntHave('blockedByUsers', function($q) use($id) 
                $q->where('user_id', $id);
            );
    
    

    执行:

    User::areNotBlockedBy(auth()->id())->where('verified', 1)->get();
    

    将生成以下 SQL:

    -- Showing rows 0 - 24 (990 total, Query took 0.0005 seconds.)
    select * from `users` where `id` != ? and not exists (select * from `users` as `laravel_reserved_0` inner join `ignore_lists` on `laravel_reserved_0`.`id` = `ignore_lists`.`user_id` where `users`.`id` = `ignore_lists`.`blocked_user_id` and `user_id` = ?) and `verified` = ?
    

    要包含阻止指定用户 ($id) 的用户:

    /**
     * Scope a query to only include users that blocked the specified user.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeWhoBlocked($query, $id)
    
        return User::whereHas('blockedUsers', function($q) use($id) 
            $q->where('blocked_user_id', $id);
        );
    
    

    执行:

    User::whoBlocked(auth()->id())->where('verified', 1)->get();
    

    将生成以下 SQL:

    -- Showing rows 0 - 1 (2 total, Query took 0.0004 seconds.)
    select * from `users` where exists (select * from `users` as `laravel_reserved_12` inner join `ignore_lists` on `laravel_reserved_12`.`id` = `ignore_lists`.`blocked_user_id` where `users`.`id` = `ignore_lists`.`user_id` and `blocked_user_id` = ?) and `verified` = ?
    

    要包含未阻止指定用户 ($id) 的用户:

    /**
     * Scope a query to only include users that did not block the specified user.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeWhoDidNotBlock($query, $id)
    
        // It will exclude the user with $id
        return User::where('id', '!=', $id)
            ->whereDoesntHave('blockedUsers', function($q) use($id) 
                $q->where('blocked_user_id', $id);
            );
    
    

    执行:

    User::whoDidNotBlock(auth()->id())->where('verified', 1)->get();
    

    将生成以下 SQL:

    -- Showing rows 0 - 24 (992 total, Query took 0.0004 seconds.)
    select * from `users` where `id` != ? and not exists (select * from `users` as `laravel_reserved_1` inner join `ignore_lists` on `laravel_reserved_1`.`id` = `ignore_lists`.`blocked_user_id` where `users`.`id` = `ignore_lists`.`user_id` and `blocked_user_id` = ?) and `verified` = ?
    

【讨论】:

以上是关于Laravel 多对多(在同一个用户表/模型上):查询范围以包含指定用户的相关信息的主要内容,如果未能解决你的问题,请参考以下文章

在 Laravel 中查询用户的多对多关系

同一模型上的 Laravel 多对多关系

Laravel 多对多同步与附加列

从laravel中的多对多关系中获取单列

Laravel 多对多关系 - 检索模型

Laravel 5.1 中 3 个模型之间的关系(“像多对多通过”)