Laravel 多对多(在同一用户上 table/Model):查询范围以包括指定用户的相关内容

Laravel Many-to-Many (on the same users table/Model): Query scopes to include related for the specified user

用户可以互相拉黑。一个用户可以屏蔽很多(其他)用户,一个用户可以被很多(其他)用户屏蔽。 在 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 是枢轴 table,它有 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 或使用 raw queries in whereIn...我可能错了,但我认为就性能而言 "left join" 解决方案会更好,对吗? (对此不确定,也许我完全错了)。

使用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

用户中超过 300,000 条记录的示例 table:

解释扫描 301119+1+1 行并获取 575ms 的第一个查询 whereHas

解释第二个查询 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 需要创建一个临时 table 来存储 所有用户的记录 并合并一些 ignore_lists。然后扫描这些记录并找出没有ignore_lists的记录。 whereDosentHave 也会扫描 所有用户 。对于我的 mysql 服务器,where not existsleft join 快一点。它的执行计划看起来不错。这两个查询的性能差别不大。

因为 whereDoesntHave 更具可读性。我会选择whereDoesntHave

3) 要包括 阻止了指定的 user ($id) 的用户,要使用 whereHas blockedUsers 像这样:

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) 的用户,请对 blockedByUsers 使用 whereDoesntHave

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 添加索引 table.

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

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

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

  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)
    {
        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` = ?
    
  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)
    {
        // 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` = ?
    
  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)
    {
        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` = ?
    
  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)
    {
        // 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` = ?