Laravel ORM withAggregate 用法简析
最近遇到一个需求,要对模型 model
的某个关联表 relation
中的 created_at
字段做 withMax
查询,只需要日期信息不需要时间,也就是找出关联表中创建时间最新的记录的日期值。
我原本的做法先是用 withMax('relation', 'created_at')
取出来之后,再用循环对 relation_created_at_max 进行处理,这样实在是不太优雅,我尝试使用过 withMax('relation', 'date(created_at)')
, 但结果提示找不到 date(created_at)
这一列,于是我不得不点进去看了一下 withMax
的源码:
/**
* Add subselect queries to include the max of the relation's column.
*
* @param string|array $relation
* @param string $column
* @return $this
*/
public function withMax($relation, $column)
{
return $this->withAggregate($relation, $column, 'max');
}
可以看到这个方法里面实际上就是调用了 withAggregate
方法:
/**
* Add subselect queries to include an aggregate value for a relationship.
*
* @param mixed $relations
* @param string $column
* @param string $function
* @return $this
*/
public function withAggregate($relations, $column, $function = null)
{
if (empty($relations)) {
return $this;
}
if (is_null($this->query->columns)) {
$this->query->select([$this->query->from.'.*']);
}
$relations = is_array($relations) ? $relations : [$relations];
foreach ($this->parseWithRelations($relations) as $name => $constraints) {
// First we will determine if the name has been aliased using an "as" clause on the name
// and if it has we will extract the actual relationship name and the desired name of
// the resulting column. This allows multiple aggregates on the same relationships.
$segments = explode(' ', $name);
unset($alias);
if (count($segments) === 3 && Str::lower($segments[1]) === 'as') {
[$name, $alias] = [$segments[0], $segments[2]];
}
$relation = $this->getRelationWithoutConstraints($name);
if ($function) {
$hashedColumn = $this->getQuery()->from === $relation->getQuery()->getQuery()->from
? "{$relation->getRelationCountHash(false)}.$column"
: $column;
$expression = sprintf('%s(%s)', $function, $this->getQuery()->getGrammar()->wrap(
$column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn)
));
} else {
$expression = $column;
}
// Here, we will grab the relationship sub-query and prepare to add it to the main query
// as a sub-select. First, we'll get the "has" query and use that to get the relation
// sub-query. We'll format this relationship name and append this column if needed.
$query = $relation->getRelationExistenceQuery(
$relation->getRelated()->newQuery(), $this, new Expression($expression)
)->setBindings([], 'select');
$query->callScope($constraints);
$query = $query->mergeConstraintsFrom($relation->getQuery())->toBase();
// If the query contains certain elements like orderings / more than one column selected
// then we will remove those elements from the query so that it will execute properly
// when given to the database. Otherwise, we may receive SQL errors or poor syntax.
$query->orders = null;
$query->setBindings([], 'order');
if (count($query->columns) > 1) {
$query->columns = [$query->columns[0]];
$query->bindings['select'] = [];
}
// Finally, we will make the proper column alias to the query and run this sub-select on
// the query builder. Then, we will return the builder instance back to the developer
// for further constraint chaining that needs to take place on the query as needed.
$alias = $alias ?? Str::snake(
preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column")
);
$this->selectSub(
$function ? $query : $query->limit(1),
$alias
);
}
return $this;
}
这段源码看上去有点恐怖,说实话我还没有完全看明白,但是只要注意到其中关键的几行代码,就可以大致上摸清楚它的用法
if ($function) {
...
}else {
$expression = $column;
}
当这个函数有第三个参数 $function
传入的时候,它会走自己的一套逻辑去构造出一个 sprintf('%s(%s)', ...)
的一个表达式,在这套逻辑中它可能就自动加上了一些跟关联表相关的一些限定,类似于'relation'.'created_at'
,这样就导致我在传入 date(created_at)
的时候,被它解析成了'relation'.'date(created_at)'
, 从而出现了 BUG。
为了避免这种情况,我们考虑直接走 else
语句,不传入 $function
参数,也就是这样
$model->withAggregate('relation', 'max(date(created_at))')
这样就可以成功获取到我们想要的数据,唯一的缺点是这个字段会被命名为 relation_max_date_created_at
, 导致这个结果的罪魁祸首就是这条语句
$alias = $alias ?? Str::snake(
preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column")
);
$alias
变量被 $name $function $column
的组合覆盖掉了,为了解决这个问题,我们可以找到前面的这一行代码
if (count($segments) === 3 && Str::lower($segments[1]) === 'as') {
[$name, $alias] = [$segments[0], $segments[2]];
}
当传入的 $name
里包含 as
的时候,$alias
会被赋予一个初值,也就是 as
后面紧接的名字,于是我们可以把上面那条语句修改为
$model->withAggregate('relation as max_create_date', 'max(date(created_at))')
这样我们就成功地把聚合的结果保存到了 max_create_date
字段当中
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: