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
是数据透视表,它有 id
、user_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)
。
我认为使用whereHas
和whereDoesntHave
进行查询会更具可读性。
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 exists
比 left 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)
的用户,请将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
表添加索引。
【讨论】:
scopeAreBlockedBy
和 scopeWhoBlocked
是相同的。我认为在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 专用服务器上进行测试。
我们不希望在使用areNotBlockedBy
和whoDidNotBlock
查询时在结果中获取当前用户,因此这些函数将排除使用$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 多对多(在同一个用户表/模型上):查询范围以包含指定用户的相关信息的主要内容,如果未能解决你的问题,请参考以下文章