Laravel,数据表,具有关系计数的列

Posted

技术标签:

【中文标题】Laravel,数据表,具有关系计数的列【英文标题】:Laravel, Datatables, column with relations count 【发布时间】:2015-03-10 15:10:01 【问题描述】:

我有两个模型,UserTraining,它们之间有Many to many 的关系。我正在使用Laravel Datatables 包来显示所有用户的表格。数据控制器方法(检索查询结果并创建 Datatables 表)如下所示:

public function getData()

    $users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
        ->where('users.is_active', '=', 1);

    return \Datatables::of($users)
        ->remove_column('id')
        ->make();

如何在创建的表中添加一列,显示每个用户的关系总数(即每个User 有多少个Trainings)?

【问题讨论】:

【参考方案1】:

蛮力的方法是尝试User::selectRaw(...),它有一个内置的子查询来获取用户的训练计数并将其公开为一个字段。

但是,有一种更内置的方法可以做到这一点。您可以预先加载关系(以避免 n+1 查询),并使用 DataTables add_column 方法添加计数。假设你的关系被命名为trainings

public function getData() 
    $users = User::with('trainings')->select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
        ->where('users.is_active', '=', 1);

    return \Datatables::of($users)
        ->add_column('trainings', function($user) 
            return $user->trainings->count();
        )
        ->remove_column('id')
        ->make();

add_column 中的列名应与加载的关系同名。如果出于某种原因使用不同的名称,则需要确保删除关系列,以便将其从数据数组中删除。例如:

    return \Datatables::of($users)
        ->add_column('trainings_count', function($user) 
            return $user->trainings->count();
        )
        ->remove_column('id')
        ->remove_column('trainings')
        ->make();

编辑

不幸的是,如果您想在计数字段上订购,您将需要蛮力方法。包通过在传递给of() 方法的Builder 对象上调用->orderBy() 来进行排序,因此查询本身需要排序字段。

但是,即使您需要执行一些原始 SQL,它也可以变得更简洁一些。您可以添加将添加关系计数的模型范围。例如,将以下方法添加到您的 User 模型中:

注意:以下函数仅适用于 hasOne/hasMany 关系。请参阅下面的 Edit 2 以获取适用于所有关系的更新函数。

public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)

    $relation = $this->$relationName(); // ex: $this->trainings()
    $related = $relation->getRelated(); // ex: Training
    $parentKey = $relation->getQualifiedParentKeyName(); // ex: users.id
    $relatedKey = $relation->getForeignKey(); // ex: trainings.user_id
    $fieldName = $fieldName ?: $relationName; // ex: trainings

    // build the query to get the count of the related records
    // ex: select count(*) from trainings where trainings.id = users.id
    $subQuery = $related->select(DB::raw('count(*)'))->whereRaw($relatedKey . ' = ' . $parentKey);

    // build the select text to add to the query
    // ex: (select count(*) from trainings where trainings.id = users.id) as trainings
    $select = '(' . $subQuery->toSql() . ') as ' . $fieldName;

    // add the select to the query
    return $query->addSelect(DB::raw($select));

将该范围添加到您的用户模型后,您的 getData 函数变为:

public function getData() 
    $users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
        ->selectRelatedCount('trainings')
        ->where('users.is_active', '=', 1);

    return \Datatables::of($users)
        ->remove_column('id')
        ->make();

如果您希望计数字段具有不同的名称,您可以将字段名称作为第二个参数传递给selectRelatedCount 范围(例如selectRelatedCount('trainings', 'training_count'))。

编辑 2

上述scopeSelectRelatedCount() 方法存在一些问题。

首先,对$relation->getQualifiedParentKeyName() 的调用仅适用于 hasOne/hasMany 关系。这是该方法定义为public 的唯一关系。所有其他关系都将此方法定义为protected。因此,将此范围与不是 hasOne/hasMany 的关系一起使用会引发 Illuminate\Database\Query\Builder::getQualifiedParentKeyName() 异常。

其次,生成的计数 SQL 并不是对所有关系都正确。同样,它对于 hasOne/hasMany 也可以正常工作,但手动生成的 SQL 对于多对多关系 (belongsToMany) 根本不起作用。

不过,我确实找到了解决这两个问题的方法。在查看了关系代码确定异常原因后,我发现 Laravel 已经提供了一个公共方法来为一个关系生成计数 SQL:getRelationCountQuery()。应该适用于所有关系的更新范围方法是:

public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)

    $relation = $this->$relationName(); // ex: $this->trainings()
    $related = $relation->getRelated(); // ex: Training
    $fieldName = $fieldName ?: $relationName; // ex: trainings

    // build the query to get the count of the related records
    // ex: select count(*) from trainings where trainings.id = users.id
    $subQuery = $relation->getRelationCountQuery($related->newQuery(), $query);

    // build the select text to add to the query
    // ex: (select count(*) from trainings where trainings.id = users.id) as trainings
    $select = '(' . $subQuery->toSql() . ') as ' . $fieldName;

    // add the select to the query
    return $query->addSelect(DB::raw($select));

编辑 3

此更新允许您将闭包传递给将修改添加到选择字段的计数子查询的范围。

public function scopeSelectRelatedCount($query, $relationName, $fieldName = null, $callback = null)

    $relation = $this->$relationName(); // ex: $this->trainings()
    $related = $relation->getRelated(); // ex: Training
    $fieldName = $fieldName ?: $relationName; // ex: trainings

    // start a new query for the count statement
    $countQuery = $related->newQuery();

    // if a callback closure was given, call it with the count query and relationship
    if ($callback instanceof Closure) 
        call_user_func($callback, $countQuery, $relation);
    

    // build the query to get the count of the related records
    // ex: select count(*) from trainings where trainings.id = users.id
    $subQuery = $relation->getRelationCountQuery($countQuery, $query);

    // build the select text to add to the query
    // ex: (select count(*) from trainings where trainings.id = users.id) as trainings
    $select = '(' . $subQuery->toSql() . ') as ' . $fieldName;

    $queryBindings = $query->getBindings();
    $countBindings = $countQuery->getBindings();

    // if the new count query has parameter bindings, they need to be spliced
    // into the existing query bindings in the correct spot
    if (!empty($countBindings)) 
        // if the current query has no bindings, just set the current bindings
        // to the bindings for the count query
        if (empty($queryBindings)) 
            $queryBindings = $countBindings;
         else 
            // the new count query bindings must be placed directly after any
            // existing bindings for the select fields
            $fields = implode(',', $query->getQuery()->columns);
            $numFieldParams = 0;
            // shortcut the regex if no ? at all in fields
            if (strpos($fields, '?') !== false) 
                // count the number of unquoted parameters (?) in the field list
                $paramRegex = '/(?:(["\'])(?:\\\.|[^\1])*\1|\\\.|[^\?])+/';
                $numFieldParams = preg_match_all($paramRegex, $fields) - 1;
            
            // splice into the current query bindings the bindings needed for the count subquery
            array_splice($queryBindings, $numFieldParams, 0, $countBindings);
        
    

    // add the select to the query and update the bindings
    return $query->addSelect(DB::raw($select))->setBindings($queryBindings);

使用更新的范围,您可以使用闭包来修改计数查询:

public function getData() 
    $users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
        ->selectRelatedCount('trainings', 'trainings', function($query, $relation) 
            return $query
                ->where($relation->getTable().'.is_creator', false)
                ->where($relation->getTable().'.is_speaker', false)
                ->where($relation->getTable().'.was_absent', false);
        )
        ->where('users.is_active', '=', 1);

    return \Datatables::of($users)
        ->remove_column('id')
        ->make();

注意:在撰写本文时,bllim/laravel4-datatables-package 数据表包在选择字段的子查询中存在参数绑定问题。数据将正确返回,但计数不会(“显示 0 到 0 个条目”)。我已经详细说明了问题here。这两个选项是使用该问题中提供的代码手动更新数据表包,或者不使用 count 子查询中的参数绑定。使用whereRaw 避免参数绑定。

【讨论】:

@ТомицаКораћ 我已根据您的评论/问题更新了答案。基本上,该字段需要成为 select 语句的一部分才能对其进行排序。更新有解决方案。 @ТомицаКораћ 您可能需要发布一些代码。该方法在 Illuminate/Database/Eloquent/Relations/Relation 对象上。我不知道你是否复制/粘贴了我的代码,但如果你不小心有$related->getQualifiedParentKeyName() 而不是$relation->getQualifiedParentKeyName(),你会得到那个错误(我想)。 $related 是模型,$relation 是关系对象。 @ТомицаКораћ User trainings() 方法是什么样的?您可能需要开始另一个问题以保持所有这些都井井有条。 @ТомицаКораћ 这种关系没有错,但它确实帮助我找出了错误。我已经用详细信息更新了我的答案(编辑 2)。除此之外,我想提一下您的getData 方法,如果您急于加载“培训”关系,则需要使用selectRelatedCount() 范围的第二个参数。否则,您的“培训”关系数据将覆盖您的“培训”计数,然后remove_column('trainings') 也会将其删除。 @ТомицаКораћ 它不应该那么糟糕。基本上,添加第三个参数(默认为 null),它接受一个需要一个参数的闭包:生成计数的查询。然后闭包将用于添加 count 语句的 where 条件。我现在无法访问我的开发环境,但我明天会发布更新的代码。【参考方案2】:

我会使用http://laravel.com/docs/4.2/eloquent 提供的约定来设置您的数据库表和 Eloquent 模型。在您的示例中,您将拥有三个表。

培训 training_user 用户

你的模型看起来像这样。

class Training 

    public function users() 
        return $this->belongsToMany('User');    
    



class User 

    public function trainings() 
        return $this->belongsToMany('Training');    
    


然后,您可以使用 Eloquent 获取用户列表并立即加载他们的训练。

// Get all users and eager load their trainings
$users = User::with('trainings')->get();

如果您想计算每个用户的培训次数,您可以简单地遍历 $users 并计算培训数组的大小。

foreach ( $users as $v ) 
    $numberOfTrainings = sizeof($v->trainings); 

或者您可以简单地使用纯 SQL 来完成。请注意,我下面的示例假设您遵循 Laravel 命名表和列的约定。

SELECT 
    u.*, COUNT(p.user_id) AS number_of_trainings 
FROM 
    users u 
JOIN 
    training_user p ON u.id = p.user_id 
GROUP BY 
    u.id

现在您有几种方法可以计算关系的数量,您可以使用任何您喜欢的方法将该值存储在某处。请记住,如果您将该数字作为值存储在用户表中,则每次用户创建/更新/删除培训时都需要更新它(反之亦然!)。

【讨论】:

戴夫,感谢您的回答,但这不是我的问题。我的模型已经按照您描述的方式设置,并且我确实知道如何计算关系。但我的问题是它在Datatables 类中不起作用。如何将计数添加到 Datatales 表中?

以上是关于Laravel,数据表,具有关系计数的列的主要内容,如果未能解决你的问题,请参考以下文章

Laravel:如何编写关于 belongsToMany 关系的连接计数查询?

具有计数关系的 Laravel 查询构建器

Laravel - 从数据透视表中获取额外的列值

加入的 Laravel 查询生成器计数

Laravel 关系多对多按外键计数相关数据

Laravel 多对多关系计数