我有一个查询,其中包含每个模型的一些连接和全局范围,例如:
SELECT *
FROM products p
WHERE EXISTS(
SELECT *
FROM orders o
WHERE o.user_id = 4
AND o.status_id = 1
AND o.user_id = 3
AND EXISTS(
SELECT *
FROM suborders s
WHERE s.status_id = 2
)
);
这意味着我可以简单地编写一些whereHas
语句,我的查询将有一些嵌套的EXIST
子句,但所有全局作用域(如orders
表上的user_id
(将自动应用:
$this->builder->whereHas('orders', function ($q) {
$q->where('status_id', '=', 1)
->whereHas('suborder', function ($q) {
$q->where('status_id', '=', 2);
});
});
问题是它很慢,最好有纯JOIN
的东西而不是丑陋的嵌套EXIST
子句:
SELECT *
FROM products p
INNER JOIN orders o ON p.order_id = o.id
INNER JOIN suborders s ON o.id = s.order_id
WHERE o.status_id = 1
AND u.user_id = 3
AND s.status_id = 2;
这样做的问题是我需要使用查询构建器来连接这些:
$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
->join('suborders', 'orders.id', '=', 'suborders.order_id')
->where('orders.status_id', 1)
->where('suborders.id', 2);
这不包括我对Order
和Suborder
模型的任何全局范围。我需要手动完成:
$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
->join('suborders', 'orders.id', '=', 'suborders.order_id')
->where('orders.status_id', 1)
->where('suborders.id', 2)
->where('orders.user_id', 3);
这很糟糕,因为每次编写这样的查询时,我都需要复制我的全局范围逻辑,而whereHas
会自动应用它们。
有没有办法联接表,并自动应用联接模型中的所有全局范围?
我以前处理过类似的问题,并提出了一些方法。
首先,让我们在服务提供商中为 Illuminate\Database\Eloquent\Builder 定义一个宏:
use IlluminateDatabaseEloquentBuilder;
use IlluminateDatabaseQueryJoinClause;
class AppServiceProvider
{
public function boot()
{
Builder::macro('hasJoinedWith', function ($table) {
return collect(
$this->getQuery()->joins
)
->contains(function (JoinClause $joinClause) use ($table) {
return $joinClause->table === $table;
})
});
}
}
然后,让我们定义范围:
class Product extends Model
{
public function scopeOrderStatus($query, $orderStatusId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
return $query->where('orders.status_id', $orderStatusId)
}
public function scopeOrderUser($query, $userId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
return $query->where('orders.user_id', $userId)
}
public function scopeSubOrder($query, $subOrderId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
if (! $query->hasJoinedWith('suborders')) {
$query->join('suborders', 'orders.id', '=', 'suborders.order_id');
}
return $query->where('suborders.id', $subOrderId)
}
}
最后,将作用域一起使用:
Product::orderStatus(1)
->subOrder(2)
->orderUser(3)
// This is optional. There are possibly duplicate products.
->distinct()
->get();
这是我能想到的最好的方法,可能有更好的方法。
实际上,你所描述的不可能是真的(将在下面提供证据(。EXISTS
不会使查询变慢,在 99% 的情况下,它会使查询更快! 因此,laravel不提供从框为关系进行连接的能力。
我在 github 上看到了不同的解决方案和包,但我不会提供它的链接,因为当我查看逻辑时,发现很多字段选择问题和罕见情况。
- Laravel不会像您描述的那样使用
EXISTS
生成代码,它会按ID向每个EXISTS
子查询添加关系搜索,例如
SELECT *
FROM products
WHERE EXISTS(
SELECT *
FROM orders
WHERE o.user_id = 4
AND o.status_id = 1
AND o.id = p.order_id
AND EXISTS(
SELECT *
FROM suborders s
WHERE s.status_id = 2
AND s.id = o.suborder_id
)
);
注意AND o.id = p.order_id
和AND s.id = o.suborder_id
当您选择连接时,您应该从主表中精确地设置
SELECT
,以正确填充模型字段。
全局范围是全局的。真正全球化的目的。如果你有超过 1-2 个地方没有它们,那么你应该找到另一个解决方案而不是全局范围。否则,您的应用程序将很难支持和编写新代码。开发人员不应该每次都记住,他应该关闭全局范围