有没有一种方法可以让表名自动添加到有说服力的查询方法中?

g2ieeal7  于 2021-08-20  发布在  Mysql
关注(0)|答案(3)|浏览(441)

我正在Laravel5.5上开发一个应用程序,我面临一个特定查询范围的问题。我有以下表格结构(省略了一些字段):

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

这个 parent_id 列引用 id 从同一张table。我使用此查询范围来筛选没有任何子项的记录:

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]:完整性约束冲突:where子句中的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);
}

完美的解决方案

我认为,能够在不依赖子查询的情况下使用查询而不使用表名前缀将是完美的解决方案。
这就是为什么我要问:有没有办法让表名自动添加到有说服力的查询方法中?

nue99wik

nue99wik1#

我会使用关系:

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

uujelgoq2#

在mysql中,有两种在邻接列表中查找叶节点(行)的好方法。一个是左连接where null方法(antijoin),这就是您所做的。另一个是不存在子查询。这两种方法的性能应该相当(理论上,它们的性能完全相同)。但是,子查询解决方案不会向结果中引入新列。

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

sg24os4d3#

您必须创建一个 SomeDatabaseBuilder 扩展原有的 Illuminate\Database\Query\Builder ,及 SomeEloquentBuilder 扩展 Illuminate\Database\Eloquent\Builder 最后,一个 BaseModel 延伸 Illuminate\Database\Eloquent\Model 并覆盖这些方法:

/**
 * @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);
}

然后,就 SomeDatabaseBuilderSameEloquentBulder ,更改默认情况下限定列的方法(或使其成为可选)。

相关问题