我正在Laravel 5.5上开发一个应用程序,我遇到了一个具有特定查询范围的问题。我有以下表结构(省略了一些字段):
orders
---------
id
parent_id
status
parent_id
列引用同一个表中的id
。我有这个查询范围来过滤没有任何子项的记录:
public function scopeNoChildren(Builder $query): Builder
{
return $query->select('orders.*')
->leftJoin('orders AS children', function ($join) {
$join->on('orders.id', '=', 'children.parent_id')
->where('children.status', self::STATUS_COMPLETED);
})
->where('children.id', null);
}
这个作用域单独使用时工作得很好。但是,如果我尝试将它与任何其他条件组合在一起,它会抛出SQL异常:
Order::where('status', Order::STATUS_COMPLETED)
->noChildren()
->get();
导致这样的结果:
SQLSTATE23000: where子句中的完整性约束冲突: 1052列'status‘不明确
我找到了两种方法来避免这个错误:
解决方案#1:在所有其他条件前加上表名
这样做是可行的:
Order::where('orders.status', Order::STATUS_COMPLETED)
->noChildren()
->get();
但我不认为这是一个好的方法,因为它不清楚表名是必需的,以防其他开发人员甚至我自己将来再次尝试使用该作用域。他们可能最终会发现这一点,但这似乎不是一个好的做法。
解决方案#2:使用子查询
我可以在子查询中将不明确的列分开。不过,在这种情况下,随着表的增长,性能将会下降。
不过,这是我正在使用的策略。因为它不需要对其他作用域和条件进行任何更改。至少不是我现在应用它的方式。
public function scopeNoChildren(Builder $query): Builder
{
$subQueryChildren = self::select('id', 'parent_id')
->completed();
$sqlChildren = DB::raw(sprintf(
'(%s) AS children',
$subQueryChildren->toSql()
));
return $query->select('orders.*')
->leftJoin($sqlChildren, function ($join) use ($subQueryChildren) {
$join->on('orders.id', '=', 'children.parent_id')
->addBinding($subQueryChildren->getBindings());
})->where('children.id', null);
}
完美的解决方案
我认为,能够在不使用表名作为前缀的情况下使用查询,而不依赖子查询,这将是一个完美的解决方案。
这就是为什么我要问:有没有办法自动将表名添加到雄辩的查询方法中?
发布于 2018-06-04 22:57:48
我会使用一个关系:
public function children()
{
return $this->hasMany(self::class, 'parent_id')
->where('status', self::STATUS_COMPLETED);
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('children')
->get();
这将执行以下查询:
select *
from `orders`
where `status` = ?
and not exists
(select *
from `orders` as `laravel_reserved_0`
where `orders`.`id` = `laravel_reserved_0`.`parent_id`
and `status` = ?)
它使用子查询,但它很简短,简单,并且不会引起任何歧义问题。
我不认为性能会是一个相关的问题,除非你有数百万行(我假设你没有)。如果将来子查询性能会有问题,您仍然可以返回到连接解决方案。在此之前,我将重点关注代码的可读性和灵活性。
重用关系的一种方法(如OP所指出的):
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('children', function ($query) {
$query->where('status', self::STATUS_COMPLETED);
})->get();
或者一种有两种关系的方式:
public function completedChildren()
{
return $this->children()
->where('status', self::STATUS_COMPLETED);
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('completedChildren')
->get();
发布于 2018-06-04 20:51:37
在MySQL中,有两种很好的方法来查找邻接表中的叶节点(行)。一个是LEFT-JOIN-WHERE-NULL方法(antijoin),这就是您所做的。另一个是NOT EXISTS子查询。这两种方法应该具有可比较的性能(理论上它们做的是完全相同的)。但是,子查询解决方案不会在结果中引入新列。
return $query->select('orders.*')
->whereRaw("not exists (
select *
from orders as children
where children.parent_id = orders.id
and children.status = ?
)", [self::STATUS_COMPLETED]);
发布于 2018-06-03 03:56:26
您必须创建一个扩展原始Illuminate\Database\Query\Builder
的SomeDatabaseBuilder
,以及一个扩展Illuminate\Database\Eloquent\Builder
的SomeEloquentBuilder
,最后创建一个扩展Illuminate\Database\Eloquent\Model
的BaseModel
,并覆盖这些方法:
/**
* @return SomeDatabaseBuilder
*/
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new SomeDatabaseBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
/**
* @param \Illuminate\Database\Query\Builder $query
* @return SameEloquentBulder
*/
public function newEloquentBuilder($query)
{
return new SameEloquentBulder($query);
}
然后,在SomeDatabaseBuilder
和SameEloquentBulder
上,将方法更改为默认情况下限定列(或将其设为可选)。
https://stackoverflow.com/questions/48243734
复制相似问题