我有两个模型User
和Training
,它们之间有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();
}
如何在创建的表格中添加一列,显示每个用户的关系总数(即每个Training
有多少User
个?)
答案 0 :(得分:10)
蛮力方式是尝试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();
不幸的是,如果您想在计数字段上订购,则需要使用强力方法。该包通过调用传递给->orderBy()
方法的Builder
对象上的of()
来进行排序,因此查询本身需要要订购的字段。
但是,即使您需要执行一些原始SQL,也可以使其更清晰。您可以添加将添加关系计数的模型范围。例如,将以下方法添加到用户模型:
注意:以下函数仅适用于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')
)。
上述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));
}
此更新允许您将闭包传递给范围,该范围将修改添加到选择字段的计数子查询。
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 datatables包在选择字段的子查询中存在参数绑定问题。数据将正确返回,但计数不会(“显示0到0的0个条目”)。我详细介绍了问题here。这两个选项是使用该问题中提供的代码手动更新datatables包,或者不使用count子查询中的参数绑定。使用whereRaw
来避免参数绑定。
答案 1 :(得分:1)
我会使用http://laravel.com/docs/4.2/eloquent提供的约定来设置您的数据库表和Eloquent模型。在您的示例中,您将有三个表。
你的模型看起来像这样。
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
既然你有几种方法可以计算关系的数量,你可以使用任何你喜欢的方法来存储那个值。请记住,如果您将该数字存储为用户表中的值,则每次用户创建/更新/删除培训时都需要更新它(反之亦然!)。