Список форумов Cake-PHP.ru Cake-PHP.ru
Форум программистов CakePHP
(на сайт)
 
 Watched TopicsWatched Topics   FAQFAQ   ПоискПоиск   ПользователиПользователи   ГруппыГруппы   РегистрацияРегистрация 
 ПрофильПрофиль   Войти и проверить личные сообщенияВойти и проверить личные сообщения   ВходВход 

Pagination в CakePHP с таблицами HABTM связями

 
Начать новую тему   Ответить на тему    Список форумов Cake-PHP.ru -> Общий
Предыдущая тема :: Следующая тема  
Автор Сообщение
Vlad

цитировать



Зарегистрирован: 02 Ноя 2007 11:45:52
Сообщения: 241

СообщениеДобавлено: 10 Янв 2011 11:42:12    Заголовок сообщения: Pagination в CakePHP с таблицами HABTM связями Ответить с цитатой

Основная проблема для меня в CakePHP — это осуществление pagination штатными средствами для таблиц связанных HABTM.

Не знаю как лучше — назвать эту заметку переводом, или расширенным переводом. Я использую две статьи, плюс свои плюшки. Поэтому ссылки я проставлю, а перевод это, или компиляция — не столь важно, как мне кажется.

После недолгого шуршания по Интернету я нашёл решение вот по этому адресу: http://cakebaker.42dh.com/2007/10/17/pagination-of-data-from-a-habtm-relationship/. Хочу отметить что решение работает и для последней (на текущий момент) версии CakePHP 1.3.4.

Однако тут есть нюанс, который очень сильно портит малину. Если элемент связан с несколькими другими, то штатный педжинатор неправильно считает количество элементов, потому как они дублируются. Но и для этого есть решение http://debuggable.com/posts/how-to-paginate-a-search-using-the-cakephp-framework:48fc5f77-38d0-41e0-b711-77c64834cda3 .

Итак, переходим к задаче.

Что у нас есть на входе?


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 — зло. Но увы, я не знаю как по другому решить эту задачу. Если вам помогла эта статья — я рад, если вы поможете мне решить задачу ещё более простым способом — я буду счастлив.

И теперь дополнение, как это лучше сделать в реальности. Дело в том, что приведенный выше способ работает по методу "мягкого И" ну или точнее - "ИЛИ". Т.е. если вы задаёте "выбери товар с характеристикой 3,4,5 - оно выберет все товары у которых есть 3, 4 ИЛИ 5 или комбинации этих параметров. Т.е. вы не получите выборку где товар обладает и 3 и 4 и 5 характеристиками.

Есть решение такое, в момент записи HABTM а в табличку, в таблице товара заполняем поле "param" которое может быть текстовым, параметрами (отсортированными по возрастанию).

Т.е. у товара есть поле param " 1 3 4 5 "
(по два пробела вокруг цифры).

Теперь выборка будет выглядеть так:

LIKE "% 1 % 3 % 4 %" - выберутся ВСЕ товары, у которыех есть ВСЕ три параметра - 1,3 и 4. И не надо выкаблучиваться с подсчётом в педжинации.

В чём минусы? Решение не совсем красивое. В чём плюсы - огромная скорость выборки. На тестовой базе в 50 тысяч товаров - прирост составил - в 50 раз, по сравнению с первым решением.

Проблема, которая немного утяжеляет - это если вы решите удалить какой-то из параметров, то придётся "пробежаться" по всей базе товаров, и вычеркнуть ненужный номер из поля param. - Но это делает администратор, и делает не часто.

Вопросы?

Ах да, адрес сайта bcp.kiev.ua[/url]
Вернуться к началу
Посмотреть профиль Отправить личное сообщение
evilbloodydemon

цитировать



Зарегистрирован: 11 Окт 2007 20:32:19
Сообщения: 125

СообщениеДобавлено: 12 Янв 2011 23:03:32    Заголовок сообщения: Ответить с цитатой

особо не вдавался в подробности, но судя по всему проблему в том числе и хабтм паджинации решит вот этот плагин https://github.com/Terr/linkable/
_________________
поздняк метаться - ракеты в воздухе
jabber-конференция по CakePHP - xmpp:cakephp@conference.jabber.ru
Вернуться к началу
Посмотреть профиль Отправить личное сообщение Посетить сайт автора
Vlad

цитировать



Зарегистрирован: 02 Ноя 2007 11:45:52
Сообщения: 241

СообщениеДобавлено: 13 Янв 2011 00:13:30    Заголовок сообщения: Ответить с цитатой

Спасибо за полезную ссылку, завтра на работе посмотрим - сколько времени оно будет колдовать над 50к товаров.

Боюсь, что тут правильный метод - через джойны - то это правильно, но долго Sad

Завтра доложусь об испытаниях Smile))
Вернуться к началу
Посмотреть профиль Отправить личное сообщение
Vlad

цитировать



Зарегистрирован: 02 Ноя 2007 11:45:52
Сообщения: 241

СообщениеДобавлено: 16 Янв 2011 20:41:29    Заголовок сообщения: Ответить с цитатой

По ссылке - не совсем то, что нужно конкретно в моей задаче. Но в целом скажу - тут join-ы, они, конечно, правильнее (красивее). Но с точки зрения быстродействия - проигрывают LIKE-варианту - как первоклассники.
Вернуться к началу
Посмотреть профиль Отправить личное сообщение
Имя
Сообщение

Смайлики
Very Happy Smile Sad Surprised
Shocked Confused Cool Laughing
Mad Razz Embarassed Crying or Very sad
Evil or Very Mad Twisted Evil Rolling Eyes Wink
Exclamation Question Idea Arrow
Дополнительные смайлики

 
Показать сообщения:   
Начать новую тему   Ответить на тему    Список форумов Cake-PHP.ru -> Общий Часовой пояс: GMT + 3
Страница 1 из 1

 
Перейти:  
Вы можете начинать темы
Вы можете отвечать на сообщения
Вы можете редактировать свои сообщения
Вы можете удалять свои сообщения
Вы не можете голосовать в опросах


Powered by phpBB © 2001, 2005 phpBB Group
Русская поддержка phpBB

Рейтинг@Mail.ru