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

时间:2018-01-13 19:52:42

标签: php mysql laravel eloquent

我正在开发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();

导致这一点:

  

SQLSTATE [23000]:违反完整性约束:1052列'状态' in where子句不明确

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

解决方案#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查询方法中?

3 个答案:

答案 0 :(得分:11)

我会使用一种关系:

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

答案 1 :(得分:3)

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

once(Goal)

答案 2 :(得分:2)

您必须创建SomeDatabaseBuilder扩展原始Illuminate\Database\Query\BuilderSomeEloquentBuilder扩展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上,默认情况下将方法更改为限定列(或使其成为可选项)。