Laravel, Datatables, столбец с количеством отношений

У меня есть две модели, 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

Решение

Грубой силой было бы попробовать 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, его можно сделать немного чище. Вы можете добавить область действия модели, которая будет добавлена ​​в счетчик отношений. Например, добавьте следующий метод к вашей модели 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));
}

С этой областью, добавленной к вашей модели User, ваша функция 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 не будет работать вообще для отношения многие ко многим (принадлежит ToMany).

Однако я нашел решение обеих проблем. Просматривая код отношения, чтобы определить причину исключения, я обнаружил, что 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

Это обновление позволяет передать закрытие области, которая изменит подзапрос count, добавляемый в поля выбора.

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();
}

Примечание. На момент написания этой статьи в пакете datatables пакета bllim / laravel4-datatables-package возникла проблема с привязкой параметров в подзапросах в полях выбора. Данные будут возвращены правильно, но отсчет не будет («Отображение от 0 до 0 из 0 записей»). Я подробно изложил проблему Вот. Два варианта — вручную обновить пакет datatables с помощью кода, предоставленного в этой проблеме, или не использовать привязку параметров внутри подзапроса count. использование whereRaw чтобы избежать привязки параметров.

10

Другие решения

Я бы настроил ваши таблицы БД и модели Eloquent, используя соглашения, представленные на http://laravel.com/docs/4.2/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

Теперь, когда у вас есть несколько способов подсчитать количество отношений, вы можете использовать любой метод, который вам нравится, для хранения этого значения где-либо. Просто помните, что если вы сохраните это число в качестве значения в пользовательской таблице, вам нужно будет обновлять его каждый раз, когда пользователь создает / обновляет / удаляет тренинг (и наоборот!).

1

По вопросам рекламы [email protected]