Search
Write a publication
Pull to refresh

CDbCriteria и UNION в yii

Класс CDbCriteria инкапсулирует критерии запроса: в нем можно хранить какие колонки требуется выбрать, с какими таблицами нужен join, какие условия накладывать на записи (where) и прочее. Но в нем нет понятия UNION, что бывает довольно неприятно. Здесь я опишу как я предлагаю с этим бороться.

Фильтрация и пейджинг средствами yii

В моем понимании страница с пейджером, фильтром и списком чего-то (пользователей, задач или товаров) устроена так:
  1. Где-то на основании данных из $_POST и $_GET строится запрос средствами CDbCriteria
  2. Вместе с объектом CPagination они инкапсулируются внутри объекта CActiveDataProvider, который передается в шаблон
  3. Шаблон отображает те товары, которые ему выдаст CActiveDataProvider (обычно с помощью CListView или его родственниками)

Почему UNION нужен

Фильтр может оказаться очень сложным.

Пусть есть таблицы: задач (Problem), пользователей (User) и решений задач пользователями (Solve). Фильтр требует выводить все нерешенные пользователем задачи: те, которые он не решал и те, которые он решил неправильно.

Такой запрос можно будет выглядеть примерно так: выбрать задачи, которые пользователь не решал (для них записи в таблице Solve нет) и объединить UNION'ом с теми задачами, которые он пытался решить, но безуспешно:
-- задачи, которые пользователь не пытался решить
SELECT problem.* FROM {{Problem}} AS problem
LEFT JOIN {{Solve}} AS solve ON problem.Id=solve.problemId AND solve.userId = :userId
WHERE solve.problemId IS NULL

UNION

-- задачи, которые пользователь не смог решить
SELECT problem.* FROM {{Problem}} AS problem
INNER JOIN {{Solve}} AS solve ON problem.Id=solve.problemId AND solve.userId=:userId
WHERE solve.Success = 1


Решение

Предлагается определить класс DbUnionCriteria, который будет хранить внутри себя несколько экземпляров CDbCriteria. А когда потребуется строить sql-запрос — построим sql-запросы всех детей и объединим их словом UNION.

Не буду усложнять, сейчас важен принцип. Пусть DbUnionCriteria хранит два экземпляра CDbCriteria:
class DbUnionCriteria extends CDbCriteria{
public $criteria1;
public $criteria2;
}

DbUnionCriteria обязательно должен быть родственником CDbCriteria, поскольку в коде yii есть прямая проверка этого. Также DbUnionCriteria должен хранить в себе информацию про order и limit, которая не должна содержаться в его компонентных критериях.

Придется переопределить класс CDbCommandBuilder, чтобы строить запросы с использованием UNION:
class DbCommandBuilder extends CDbCommandBuilder{
public function createFindCommand($table,$criteria,$alias='t'){
if($criteria instanceof DbUnionCriteria){
$sql = parent::createFindCommand($table,$criteria->criteria1,$alias)->text;
$sql .= ' UNION ';
$sql .= parent::createFindCommand($table,$criteria->criteria2,$alias)->text;

$sql=$this->applyOrder($sql,$criteria->order);
$sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset);

$command=Yii::app() -> db ->createCommand($sql);
return $command;
}

return parent::createFindCommand($table,$criteria,$alias);
}

public function createCountCommand($table,$criteria,$alias='t'){
if($criteria instanceof DbUnionCriteria){
$sql = 'SELECT (';
$sql .= parent::createCountCommand($table,$criteria->criteria1,$alias)->text;
$sql .= ') + (';
$sql .= parent::createCountCommand($table,$criteria->criteria2,$alias)->text;
$sql .= ') as total';

$command=Yii::app() -> db ->createCommand($sql);
return $command;
}

return parent::createCountCommand($table,$criteria,$alias);
}
}


Остается проблема с заменой CDbCommandBuilder на DbCommandBuilder. Для этого мне пришлось править код yii — этот класс захаркоден в framework/db/schema/CDbSchema.php в функции createCommandBuilder().

Теперь в CActiveRecord можно вместо объекта CDbCriteria можно подсунуть объект UnionDbCriteria и фильтрация и пейджинг будут работать.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.