Ламер - он почему Аццкий? Потому что, если где какие грабли есть - то грабли обязательно будут его.
Вчера столкнулся с интересным глюком.
Дано: Symfony-проект. Ajax.
Есть список элементов, порядок следования которых определяется атрибутом SORT. За работу с этим списком "отвечают" два класса: первый создан на основе таблицы списка - parent_id, child_id, sort.
Второй - базируется на любимом Ламером setSQLinsteadOfView (
http://hellish-lamer.livejournal.com/1298.html ), т.е. "подмене" обращения к таблице результатами работы сырого запроса. Это довольно удобно, т.к. позволяет легко создавать объекты с нужным набором свойств. В данном случае - джойнами вытаскиваются имена родителя и потомка и несколько других нужных для отображения списка атрибутов.
Задача стояла, в целом, элементарная.
По задумке происходило следующее:
- юзер, желая переместить элемент, кликает на стрелку вверх-вниз;
- по клику запускается jq_remote_function, функция дергает экшн;
- экшн получает из реквеста нужные айдишники и дергает статический метод, который меняет порядок списка, перезаписывая атрибуты SORT;
- шаблон экшна - вызов стандартного компонента, заново делающего запрос к базе и отрисовывающего список через sfPropelPager.
Просто? Просто. Хрен. Потому что и тут появились они. Грабли.
Грабли заключались в том, что, когда экшн после изменения порядка списка вызывал компонент, компонент послушно выводил список в правильном порядке - но со старыми значениями свойства SORT элементов списка. Т.е. сортируются элементы по SORT правильно, но при этом они - старые. О как.
При обновлении страницы (или открытии того же урл в новом окне) все становилось на свои места: SORT каждого элемента приобретал ожидаемое значение.
Ламер начал локализовывать проблему:
- после срабатывания метода по изменению порядка списка в базе уже лежали правильные значения;
- если для отображения списка вызвать не модифицированный, а "родной" класс, передав ему айдишники, значение SORT было корректным;
- не имело значения, сколько раз срабатывал селект модифицированного метода: до обновления страницы значения все равно отображались неправильно.
Подозрение пало на какое-то хитрое кеширование. Ламер начал копать.
Первый подозреваемый: doSelect, подмененный модифицированным вариантом:
public static function doSelect(Criteria $criteria, PropelPDO $con = null) {
self::setSQLinsteadOfView($criteria);
return parent::doSelect($criteria, $con);
}
Ничего подозрительного. В лог шел запрос, который, если запустить его в сыром виде, выдавал верные результаты.
Дальше: parent::doSelect
public static function doSelect(Criteria $criteria, PropelPDO $con = null) {
return ModContainerContentPeer::populateObjects(ModContainerContentPeer::doSelectStmt($criteria, $con));
}
Т.е. или дело в doSelectStmt, или в populateObjects.
Проверяем doSelectStmt. Метод возвращает некоторое количество технической информации и строку запроса, который затем выполняется для извлечения данных. Ничего подозрительного.
Смотрим в populateObjects. Тут все интересно.
// populate the object(s)
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$key = ModContainerContentPeer::getPrimaryKeyHashFromRow($row, 0);
if (null !== ($obj = ModContainerContentPeer::getInstanceFromPool($key))) {
$results[] = $obj;
} else {
$obj = new $cls();
$obj->hydrate($row);
$results[] = $obj;
ModContainerContentPeer::addInstanceToPool($obj, $key);
} // if key exists
}
Проверяем вардампами $row = $stmt->fetch(PDO::FETCH_NUM).
Данные верны. Вывод: в populateObjects идет массив с правильными данными, а объекты возвращаются - уже с неправильными.
Копаем populateObjects дальше. Делаем вардамп каждого из случаев. Бинго:
if (null !== ($obj = ModContainerContentPeer::getInstanceFromPool($key))) {
$results[] = $obj;
Common::printVar($row);
Common::printVar($obj);
}
...
Однако getInstanceFromPool не делает ничего криминального: проверяет, есть ли объекты в Pool, есть есть, достает оттуда.
Вывод: Propel объекты таки кеширует, и таки в некоторых случаях кеширование срабатывает некорректно.
Гуглим.
Первый интересный результат найден тут:
http://www.joelwickard.name/15/symfony-propel-level-object-caching.html class StoreRegionPeer extends BaseStoreRegionPeer {
public static function doSelectOne(Criteria $criteria, $con = null){
$fc = new sfMemcacheFunctionCache();
$items = $fc->call ('StoreRegionPeer::doSelectOne', $criteria);
return $items;
}
public static function doSelect(Criteria $criteria, $con = null){
$fc = new sfMemcacheFunctionCache();
$items = $fc->call ('StoreRegionPeer::doSelect', $criteria);
return $items;
}
}
Но это не лучший выход: каждый раз в doSelect вмешиваться - не дело.
Ищем по "symfony disable Instance Pooling"
http://www.google.ru/search?hl=ru&source=hp&q=symfony+disable+Instance+Pooling+&btnG=%CF%EE%E8%F1%EA+%E2+Google&lr=&aq=f&oq=Бинго! Первым же результатом:
http://raw.trac.symfony-project.org/wiki/sfPropel13Plugin Действительно, есть такая настройка.
instance-pooling: true/false
Но совсем отключать - чревато потерей быстродействия.
В конечном итоге - остановился на варианте:
Propel::disableInstancePooling();
Применяется там, где необходимо. В моем случае - в коде компонента, выстраивающего список.
Вот так как-то.