Класс CDbCriteria инкапсулирует критерии запроса: в нем можно хранить какие колонки требуется выбрать, с какими таблицами нужен join, какие условия накладывать на записи (where) и прочее. Но в нем нет понятия UNION, что бывает довольно неприятно. Здесь я опишу как я предлагаю с этим бороться.
В моем понимании страница с пейджером, фильтром и списком чего-то (пользователей, задач или товаров) устроена так:
Фильтр может оказаться очень сложным.
Пусть есть таблицы: задач (Problem), пользователей (User) и решений задач пользователями (Solve). Фильтр требует выводить все нерешенные пользователем задачи: те, которые он не решал и те, которые он решил неправильно.
Такой запрос можно будет выглядеть примерно так: выбрать задачи, которые пользователь не решал (для них записи в таблице Solve нет) и объединить UNION'ом с теми задачами, которые он пытался решить, но безуспешно:
Предлагается определить класс DbUnionCriteria, который будет хранить внутри себя несколько экземпляров CDbCriteria. А когда потребуется строить sql-запрос — построим sql-запросы всех детей и объединим их словом UNION.
Не буду усложнять, сейчас важен принцип. Пусть DbUnionCriteria хранит два экземпляра CDbCriteria:
DbUnionCriteria обязательно должен быть родственником CDbCriteria, поскольку в коде yii есть прямая проверка этого. Также DbUnionCriteria должен хранить в себе информацию про order и limit, которая не должна содержаться в его компонентных критериях.
Придется переопределить класс CDbCommandBuilder, чтобы строить запросы с использованием UNION:
Остается проблема с заменой CDbCommandBuilder на DbCommandBuilder. Для этого мне пришлось править код yii — этот класс захаркоден в framework/db/schema/CDbSchema.php в функции createCommandBuilder().
Теперь в CActiveRecord можно вместо объекта CDbCriteria можно подсунуть объект UnionDbCriteria и фильтрация и пейджинг будут работать.
Фильтрация и пейджинг средствами yii
В моем понимании страница с пейджером, фильтром и списком чего-то (пользователей, задач или товаров) устроена так:
- Где-то на основании данных из $_POST и $_GET строится запрос средствами CDbCriteria
- Вместе с объектом CPagination они инкапсулируются внутри объекта CActiveDataProvider, который передается в шаблон
- Шаблон отображает те товары, которые ему выдаст 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 и фильтрация и пейджинг будут работать.