有没有办法将表名自动添加到 Eloquent 查询方法中?

2022-08-30 17:51:56

我正在 Laravel 5.5 上开发一个应用,并且遇到了一个特定查询范围的问题。我有下面的表格结构(省略了一些字段):

orders
---------
id
parent_id
status

该列引用来自同一表的 。我有这个查询范围来筛选没有任何子项的记录:parent_idid

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();

导致这种情况:

SQLSTATE[23000]:完整性约束冲突:1052 子句不明确的列“状态”

我找到了两种方法来避免这种错误:

解决方案#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);
}

完美的解决方案

我认为能够在不以表名为前缀的情况下使用查询而不依赖于子查询将是完美的解决方案。

这就是为什么我要问:有没有办法让表名自动添加到 Eloquent 查询方法中?


答案 1

我会使用一个关系:

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

它使用子查询,但它简短,简单,不会导致任何歧义问题。

我不认为性能会是一个相关的问题,除非你有数百万行(我假设你没有)。如果子查询性能将来会成为问题,您仍然可以返回到 JOIN 解决方案。在那之前,我会专注于代码的可读性和灵活性。

一种重用关系的方法(如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();

答案 2

MySQL中,有两种很好的方法可以在邻接列表中查找叶节点(行)。一个是 LEFT-JOIN-WHERE-NULL 方法(反连接),这就是你所做的。另一个是“不存在”子查询。这两种方法应该具有可比的性能(理论上它们的作用完全相同)。但是,子查询解决方案不会在结果中引入新列。

return $query->select('orders.*')
    ->whereRaw("not exists (
        select *
        from orders as children
        where children.parent_id = orders.id
          and children.status = ?
    )", [self::STATUS_COMPLETED]);

推荐