Laravel,Datatables,关系计数列

Том*_*раћ 1 php datatables laravel eloquent laravel-datatables

我有两个模型,User并且它们之间TrainingMany 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();
}
Run Code Online (Sandbox Code Playgroud)

我怎样才能添加一列到创建的表,该表显示关系的总数为每个用户(也就是,有多少Training小号是否每个User都有)?

pat*_*cus 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();
}
Run Code Online (Sandbox Code Playgroud)

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

    return \Datatables::of($users)
        ->add_column('trainings_count', function($user) {
            return $user->trainings->count();
        })
        ->remove_column('id')
        ->remove_column('trainings')
        ->make();
Run Code Online (Sandbox Code Playgroud)

编辑

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

但是,即使您需要执行一些原始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));
}
Run Code Online (Sandbox Code Playgroud)

将该范围添加到您的用户模型后,您的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();
}
Run Code Online (Sandbox Code Playgroud)

如果您希望count字段具有不同的名称,则可以将字段的名称作为第二个参数传递给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));
}
Run Code Online (Sandbox Code Playgroud)

编辑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);
}
Run Code Online (Sandbox Code Playgroud)

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

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();
}
Run Code Online (Sandbox Code Playgroud)

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