Paingation в CakePHP с таблицами HABTM связями
Основная проблема для меня в CakePHP — это осуществление pagination штатными средствами для таблиц связанных HABTM.
Не знаю как лучше — назвать эту заметку переводом, или расширенным переводом. Я использую две статьи, плюс свои плюшки. Поэтому ссылки я проставлю, а перевод это, или компиляция — не столь важно, как мне кажется.
После недолгого шуршания по Интернету я нашёл решение вот по этому адресу: Pagination for HABTM. Хочу отметить что решение работает и для последней (на текущий момент) версии CakePHP 1.3.4.
Однако тут есть нюанс, который очень сильно портит малину. Если элемент связан с несколькими другими, то штатный педжинатор неправильно считает количество элементов, потому как они дублируются. Но и для этого есть решение DISTINCT in pagination.
Итак, переходим к задаче.
CakePHP 1.3.4 версии, MySQL 5.1.37
Есть категория товара («Принтеры лазерные», «Картриджи» и т.д.), есть производитель («HP», «Xerox»), есть виды характеристик («Формат печати», «Цветность»), есть сами эти характеристики — («А4», «А3» и, соответственно «Цветной», «Монохромный»), и есть сам товар, который лежит в одной из категорий, имеет одного производителя, может иметь характеристику (принтеры имеют формат печати в списке характеристик, а телефоны — нет), а характеристика, соответственно имеет свой список (для формта — «А4», «А3»).
Что нам необходимо? Необходимо чтобы делалась выборка (с работающей педжинацией) по следующему запросу (например): показать категорию товара 1, выбрать только производителя 2, показать товар, у которого есть в параметрах что-то из списка атрибутов («А4» или «А4» и «А3»).
Для начала, посмотрим как выглядят таблицы. (я сознательно опустил огромное количество полей, которые не влияют на результат — чтобы не загромождать и без того долгую статью)
Таблица производителей
Таблица категорий
Таблица товаров
Таблица типов характеристик
И таблица собственно характеристик
Есть ещё, табличка, которая связывает категории и order — потому как не у каждой категории товара есть уточняющий список характеристик. Эта табличка служит для формирования странички выдачи товара и сортировочного меню, поэтому считаю её вспомогательной и не требующей разъяснения или публикации.
Итак, таблички созданы, теперь необходимо связать товар с характеристиками. Для этого создаётся вспомогательная табличка:
Для связи конкретного товара с характеристикой (и не одной) из таблицы ordernames. Связываем таблички напрямую, без участия orders — зачем нам посредник?
Отобрать товар по категории и производителю не представляет труда, ибо id-шник присутствует в таблице товара и нет необходимости делать какие-либо join-ы дополнительные. Тут всё просто.
Нас же интересует — как выбрать товар, обладающий характеристиками A4 и A3 одновременно, или только А3 (например).
Берём решение из первой ссылки, и прикручиваем внутрь контроллера:
Затем мы далее переопределим эту переменную, в зависимости от потребности. А потребностей у нас может быть две — первая нас интересуют характеристики товара, и вторая — нас не интересуют характеристики товара.
($named — массив характеристик).
Обратите внимание на две вещи, во первых нам не нужны дубли, и используем DISTINCT для отсекания повторов (а повторы будут, если товар обладает и «А4» и «А3» а мы выбираем обе характеристики). Второе — если массив характеристик не задан — мы не городим огород с join-ами. Логично? Логично. Параметр 'order' — тут вроде всё понятятно, $poduce — тоже ('Good.produce_id' => array(1,3,4...));
Всё? Не всё. Дело в том, что мы выбираем нужный нам товар — без дублирования. Но, при этом счётчик педжинации считает всё (если товар обладает характеристикой A3 и A4 — то, он будет посчитан дважды) — то есть имея на самом деле 4 единицы товара со множественными характеристиками, счётчик страниц покажет нам 2-3-4-5… страниц для листания, хотя на самом деле товара значительно меньше. Что делать? Открываем вторую ссылку приведенную в начале статьи, и дополняем модель good следующим:
Всё. Теперь всё работает так, как надо. В комментариях ко второй статье было указано — DISTINCT — зло. Но увы, я не знаю как по другому решить эту задачу. Если вам помогла эта статья — я рад, если вы поможете мне решить задачу ещё более простым способом — я буду счастлив.
P.S. Адрес сайта, где реализован данный механизм я приведу через неделю. Потому что клиент ещё находится на стадии «приёма» и я не хочу подвергать сайта хабраэффекту (если, конечно, таковой будет).
Не знаю как лучше — назвать эту заметку переводом, или расширенным переводом. Я использую две статьи, плюс свои плюшки. Поэтому ссылки я проставлю, а перевод это, или компиляция — не столь важно, как мне кажется.
После недолгого шуршания по Интернету я нашёл решение вот по этому адресу: Pagination for HABTM. Хочу отметить что решение работает и для последней (на текущий момент) версии CakePHP 1.3.4.
Однако тут есть нюанс, который очень сильно портит малину. Если элемент связан с несколькими другими, то штатный педжинатор неправильно считает количество элементов, потому как они дублируются. Но и для этого есть решение DISTINCT in pagination.
Итак, переходим к задаче.
Что у нас есть на входе?
CakePHP 1.3.4 версии, MySQL 5.1.37
Есть категория товара («Принтеры лазерные», «Картриджи» и т.д.), есть производитель («HP», «Xerox»), есть виды характеристик («Формат печати», «Цветность»), есть сами эти характеристики — («А4», «А3» и, соответственно «Цветной», «Монохромный»), и есть сам товар, который лежит в одной из категорий, имеет одного производителя, может иметь характеристику (принтеры имеют формат печати в списке характеристик, а телефоны — нет), а характеристика, соответственно имеет свой список (для формта — «А4», «А3»).
Что нам необходимо? Необходимо чтобы делалась выборка (с работающей педжинацией) по следующему запросу (например): показать категорию товара 1, выбрать только производителя 2, показать товар, у которого есть в параметрах что-то из списка атрибутов («А4» или «А4» и «А3»).
Для начала, посмотрим как выглядят таблицы. (я сознательно опустил огромное количество полей, которые не влияют на результат — чтобы не загромождать и без того долгую статью)
Таблица производителей
CREATE TABLE `produces` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `produces` VALUES(1, 'Xerox');
INSERT INTO `produces` VALUES(2, 'Hewlett Packard');
INSERT INTO `produces` VALUES(3, 'Alcatel');
Таблица категорий
CREATE TABLE `categories` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `categories` VALUES(1, 'Принтеры лазерные');
INSERT INTO `categories` VALUES(2, 'Принтеры струйные');
INSERT INTO `categories` VALUES(3, 'Принтеры матричные');
INSERT INTO `categories` VALUES(4, 'Копировальные аппараты');
Таблица товаров
CREATE TABLE `goods` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`model` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`category_id` bigint(20) NOT NULL,
`produce_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `goods` VALUES(8, '2010-12-14 00:29:33','model1', 1, 1);
INSERT INTO `goods` VALUES(7, '2010-12-14 00:21:59', 'model2', 1, 2);
INSERT INTO `goods` VALUES(9, '2010-12-14 13:50:51', 'model3', 2, 2);
Таблица типов характеристик
CREATE TABLE `orders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `orders` VALUES(1, 'Цветопередача');
INSERT INTO `orders` VALUES(2, 'Формат');
INSERT INTO `orders` VALUES(3, 'Тип печати');
И таблица собственно характеристик
CREATE TABLE `ordernames` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) NOT NULL,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `ordernames` VALUES(1, 1, 'цветные');
INSERT INTO `ordernames` VALUES(2, 1, 'монохромные');
INSERT INTO `ordernames` VALUES(3, 2, 'A3');
INSERT INTO `ordernames` VALUES(4, 2, 'A4');
INSERT INTO `ordernames` VALUES(5, 2, '10x15 cm');
INSERT INTO `ordernames` VALUES(6, 2, '42”');
INSERT INTO `ordernames` VALUES(7, 3, 'на термобумаге');
INSERT INTO `ordernames` VALUES(8, 3, 'на термоплёнке')
;Есть ещё, табличка, которая связывает категории и order — потому как не у каждой категории товара есть уточняющий список характеристик. Эта табличка служит для формирования странички выдачи товара и сортировочного меню, поэтому считаю её вспомогательной и не требующей разъяснения или публикации.
Итак, таблички созданы, теперь необходимо связать товар с характеристиками. Для этого создаётся вспомогательная табличка:
CREATE TABLE `goods_ordernames` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`good_id` bigint(20) NOT NULL,
`ordername_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Для связи конкретного товара с характеристикой (и не одной) из таблицы ordernames. Связываем таблички напрямую, без участия orders — зачем нам посредник?
Теперь пришла пора строить выборку
Отобрать товар по категории и производителю не представляет труда, ибо id-шник присутствует в таблице товара и нет необходимости делать какие-либо join-ы дополнительные. Тут всё просто.
Нас же интересует — как выбрать товар, обладающий характеристиками A4 и A3 одновременно, или только А3 (например).
Берём решение из первой ссылки, и прикручиваем внутрь контроллера:
public $paginate = array('Good' => array('limit' => 1, 'joins' => array(
array(
'table' => 'goods_ordernames',
'alias' => 'GoodsOrdername',
'type' => 'inner',
'conditions'=> array('GoodsOrdername.good_id = Good.id')
),
array(
'table' => 'ordernames',
'alias' => 'Ordername',
'type' => 'inner',
'conditions'=> array(
'Ordername.id = GoodsOrdername.ordername_id',
'Ordername.id' => 3
)
)),'contain' => array('GoodsOrdername','Ordername','Produce'),'conditions' => array('produce_id' => 1)));
Затем мы далее переопределим эту переменную, в зависимости от потребности. А потребностей у нас может быть две — первая нас интересуют характеристики товара, и вторая — нас не интересуют характеристики товара.
($named — массив характеристик).
if ($named != array()) {
$order = array('Ordername.id' => $named);
$this->paginate = array('Good' => array('limit' => 10, 'joins' => array(
array(
'table' => 'goods_ordernames',
'alias' => 'GoodsOrdername',
'type' => 'inner',
'conditions'=> array('GoodsOrdername.good_id = Good.id')
),
array(
'table' => 'ordernames',
'alias' => 'Ordername',
'type' => 'inner',
'conditions'=> array(
'Ordername.id = GoodsOrdername.ordername_id',
$order
)
)),'order' => $sort,'fields' => array('DISTINCT Good.id','Good.*'),'conditions' => array('and' => array($produce,'Good.category_id' => $id))));
}
else {
$this->paginate = array('Good' => array('limit' => 10, 'order' => $sort,'fields' => array('DISTINCT Good.id','Good.*'),'conditions' => array('and' => array($produce,'Good.category_id' => $id))));
}
Обратите внимание на две вещи, во первых нам не нужны дубли, и используем DISTINCT для отсекания повторов (а повторы будут, если товар обладает и «А4» и «А3» а мы выбираем обе характеристики). Второе — если массив характеристик не задан — мы не городим огород с join-ами. Логично? Логично. Параметр 'order' — тут вроде всё понятятно, $poduce — тоже ('Good.produce_id' => array(1,3,4...));
Считаем страницы
Всё? Не всё. Дело в том, что мы выбираем нужный нам товар — без дублирования. Но, при этом счётчик педжинации считает всё (если товар обладает характеристикой A3 и A4 — то, он будет посчитан дважды) — то есть имея на самом деле 4 единицы товара со множественными характеристиками, счётчик страниц покажет нам 2-3-4-5… страниц для листания, хотя на самом деле товара значительно меньше. Что делать? Открываем вторую ссылку приведенную в начале статьи, и дополняем модель good следующим:
/**
* Return count for given pagination
*
* @param string $paginator Pagination name
* @param array $conditions Conditions to use
* @return mixed Count, or false
* @access public
*/
function paginatorCount($paginator, $conditions = array(), $contain = array()) {
$Db = ConnectionManager::getDataSource($this->useDbConfig);
if (!empty($contain)) {
$related = ClassRegistry::init($contain[0]);
}
$sql = 'SELECT
COUNT(DISTINCT ' . $this->alias . '.' . $this->belongsTo['Game']['foreignKey'] . ') count
FROM ' . $Db->fullTableName($this->table) . ' ' . $Db->name($this->alias) . ' ';
if (!empty($contain)) {
$sql .= ' INNER JOIN ' . $Db->fullTableName($related->table) . ' ' . $Db->name($related->alias) . ' ';
}
$sql .= $Db->conditions($this->paginatorConditions($paginator, $conditions, 'count'));
$count = $this->query($sql);
if (!empty($count)) {
$count = $count[0][0]['count'];
}
return $count;
}
/**
* Build conditions for given pagination
*
* @param string $paginator Pagination name
* @param array $extraConditions Extra conditions to use
* @param string $method 'count', or 'find'
* @return array Conditions
* @access public
*/
function paginatorConditions($paginator, $extraConditions = array(), $method = null) {
$Db = ConnectionManager::getDataSource($this->useDbConfig);
$conditions = null;
if (empty($extraConditions)) {
$extraConditions = array('1=1');
}
switch (strtolower($paginator)) {
case 'game_categories_games':
if ($method != 'count') {
$conditions = array_merge($extraConditions, array('1=1 GROUP BY ' . $this->alias . '.' . $this->belongsTo['Game']['foreignKey']));
} else {
$conditions = $extraConditions;
}
break;
}
return $conditions;
}
/**
* Executed by the paginator to get the count. Overriden to allow
* forcing a count (through var $forcePaginateCount)
*
* @param array $conditions Conditions to use
* @param int $recursive Recursivity level
* @return int Count
* @access public
*/
function paginateCount($conditions, $recursive) {
if (isset($this->forcePaginateCount)) {
$count = $this->forcePaginateCount;
unset($this->forcePaginateCount);
} else {
$count = $this->find('count', compact('conditions', 'recursive'));
}
return $count;
}
Всё. Теперь всё работает так, как надо. В комментариях ко второй статье было указано — DISTINCT — зло. Но увы, я не знаю как по другому решить эту задачу. Если вам помогла эта статья — я рад, если вы поможете мне решить задачу ещё более простым способом — я буду счастлив.
P.S. Адрес сайта, где реализован данный механизм я приведу через неделю. Потому что клиент ещё находится на стадии «приёма» и я не хочу подвергать сайта хабраэффекту (если, конечно, таковой будет).
0 комментариев