【问题标题】:Laravel/Eloquent: How do I refer to a column of the outer select in a Larvael `->whereNotExists` sub-query?Laravel/Eloquent:如何在 Larvael `->whereNotExists` 子查询中引用外部选择的列?
【发布时间】:2021-12-01 08:36:19
【问题描述】:

请把问题读到最后;用法的最后部分包含说明限制和条件的重要示例

问题描述

假设我有以下简化的表格方案

CREATE TABLE albums (
  id SEQUENCE PRIMARY KEY,
  parent_id BIGINT,
  _lft BIGINT NOT NULL,
  _rgt BIGINT NOT NULL
  ...
)

可以将相册组织成一棵树并使用nested set approach

假设我有两个整数p_lftp_rgt(如父左和父右)。最终,Eloquent Builder 应该构造一个查询,该查询应该在 SQL 层编译成类似这样的内容:

SELECT * FROM albums AS child
WHERE
  p_lft < child._lft AND child._rgt < p_rgt
AND
  NOT EXISTS (
    SELECT * FROM albums AS inner
    WHERE
      p_lft < inner._lft AND inner._lft <= child._lft
    AND
      child._rgt <= inner._rgt AND inner._rgt < p_rgt
    AND
      (more conditions here)
  );

实际问题出现在在程序中的两个不同点构建查询:

a) 查询的外部部分(即上面与SELECT * FROM albums AS child 对应的部分) b) 内部子查询

特别是内部子查询应该写成一个通用函数——称为过滤函数——它可以应用于外部查询的任意实例。外部查询的唯一要求是它查询正确的模型,即Album。特别是,我无法控制外部查询,因此我不知道是否已经发生了任何别名。因此,我不知道如何从内部查询(即从过滤器内部)引用外部查询。

解决方案的尝试

这就是我走了多远

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as BaseBuilder;

function applyFilter(Builder $builder, int $p_lft, int $p_rgt): void {
  // Ensure that the outer query queries for the right model
  $model = $query->getModel();
  if (!($model instanceof Album )) {
    throw new \InvalidArgumentException();
  }
  // We must wrap everything into an outer query to avoid any undesired
  // effects in case that the original query already contains an
  // "OR"-clause.
  $filter = function (Builder $query) use ($p_lft, $p_rgt) {
    $query
      ->where('_lft', '>', $p_lft)   // _lft corresponds to child._lft in SQL
      ->where('_rgt', '<', $p_rgt)   // _rgt corresponds to child._rgt in SQL
      ->whereNotExists(function (BaseBuilder $subQuery) use ($p_lft, $p_rgt) {
        $subQuery->from('albums')
          ->where('_lft', '>', p_lft)  // here _lft corresponds to inner._lft in SQL
          ->where('_lft', '<=', ????)  // how do I refer to the outer _lft here?
          ->where('_rgt', '>=', ????)  // some question
          ->where('_rgt', '<', p_rgt)  // here _rgt corresponds to inner._rgt in SQL
      });
    };
  
  $builder->where($filter);
}

使用示例

在代码中的其他地方可能会像这样调用过滤器

applyFilter(
  Albums::query()
    ->where(some_condition)
    ->orWhere(some_other_condition),
  $left, $right
)->get();

或者像这样

Photo::query()
  ->where(some_condition)
  ->whereHas('album', fn(Builder $b) => applyFilter($b, $left, $right))

甚至像这样

Album::query()
  ->whereHas('parent', fn(Builder $b) => applyFilter($b, $left, $right))

请注意,最后一种情况特别棘手。过滤器函数在whereHas 内部使用,用于将Album 与其自身关联起来。因此,过滤器函数会针对已经将 album 别名为不同名称的查询调用。

【问题讨论】:

    标签: php laravel eloquent subquery exists


    【解决方案1】:

    我的主要问题是我无法控制外部查询,因此我不知道如何控制别名。

    您可以使用from() 为任何内容设置别名。例如

    Album::query()
        ->select('*')              // ->select('*') is completely optional in this query.
        ->from('albums', 'child')
        ->get();
    

    编译成

    SELECT * FROM `albums` as `child`
    

    默认情况下,表别名应该是表本身。 (Album -> albums -> albums._lft)

    假设分组已经存在(没有丢失(...)),逐行翻译查询并不复杂


    Album::query()
        ->select('*')
        ->from('albums', 'child')
        ->where('child._lft', '>', $p_lft)
        ->where('child._rgt', '<', $p_rgt)
        ->whereNotExists(function ($sub) use ($p_lft, $p_rgt) {
            $sub->select('*')
                ->from('albums', 'inner')
                ->where('inner._lft', '>', $p_lft)
                ->whereColumn('inner._lft', '<=', 'child._lft')  // when comparing two columns, whereColumn/orWhereColumn must be used.
                ->whereColumn('child._rgt', '<=', 'inner._rgt')
                ->where('inner._rgt', '<', $p_rgt);
                ->where(/* other conditions here */)
        })
        ->get();
    

    在你的函数中获取表名:

    use Illuminate\Support\Str;
    
    function applyFilter(Builder $builder, int $p_lft, int $p_rgt): void {
        // get table name or aliased table name.
        $table = Str::after($builder->getQuery()->table, ' as '); 
        // similarly, you can get an array of all joined tables in the outer query
        $joined_tables = array_map(fn($join) => Str::after($join->table, ' as '), $builder->getQuery()->joins)
    }
    

    【讨论】:

    • 这个答案没有考虑到我对问题中所述的外部查询没有控制权。我自己没有创建外部Album::query(),而是将其作为参数获取。另外,我可能会发生“外部”查询实际上是子查询本身(cp。我的第二个示例如何调用该方法)。当然,如果我也构造了外部查询,那么一切都会很容易。但是我一开始就不会问这个问题。所以答案实际上是没有答案。
    • 我误会了。然后它实际上要简单得多。别名应该是外部表的名称。/s 如果没有进行其他别名。
    • 您说“别名应该是外部表的名称,如果没有其他别名已完成”。但后一种情况正是问题所在。我不能保证。如果设置了任何别名,我如何找出真正的别名?还要记住我的示例如何调用该方法。专辑查询不一定是顶级查询。
    • 好的,我想我明白了。您可以使用Builder 类从applyFilter() 内部获取该数据。 $table = $builder-&gt;getQuery()-&gt;table; (这将显示 albumsalbums as something)。可以类似地获得连接表名称。 $builder-&gt;getQuery()-&gt;joins[0]-&gt;table
    【解决方案2】:

    你可以试试这个吗?

    DB::query()->fromSub($builder, "my_new_table_alias")
         ->whereNotExists(function (BaseBuilder $subQuery) use ($p_lft, $p_rgt) {
            $subQuery->from('albums')
              ->where('_lft', '>', p_lft)  
              // check if this is the field you are looking for
              ->where('_lft', '<=', 'my_new_table_alias._lft')  
              ->where('_rgt', '>=', 'my_new_table_alias._rgt')  
              ->where('_rgt', '<', p_rgt)  
          });
    

    这“应该有效”,因为如果您执行这样的 POC:

    DB::query()->fromSub(Album::query(), 'my_album_table_alias')->toSql()
    

    在你的 tinker 控制台中,你会看到类似这样的东西

    "select * from (select * from `album`) as `my_album_table_alias`"
    

    ...希望它有效

    【讨论】:

    • 这行不通,因为Album::query() 已被原始查询“吞下”,您无法从中获取专辑模型的水合物。我的过滤器函数的调用者应该如何在查询中调用-&gt;get
    • @user2690527 你可以用Album::query()-&gt;fromSub(...)做同样的事情,你会得到雄辩提供的所有模型功能,所以它不再是一个“原始”查询
    猜你喜欢
    • 2016-11-29
    • 1970-01-01
    • 1970-01-01
    • 2019-05-23
    • 2021-04-08
    • 2017-11-23
    • 1970-01-01
    • 2022-01-26
    • 1970-01-01
    相关资源
    最近更新 更多