Каков наилучший способ работы с вычисленными полями объектов Propel?
Скажем, у меня есть объект «Клиент», у которого есть соответствующая таблица «клиенты», и каждый столбец соответствует атрибуту моего объекта. Я хотел бы сделать следующее: добавить вычисляемый атрибут «Количество завершенных заказов» к моему объекту при его использовании на View A, но не в представлениях B и C.
Вычисленным атрибутом является COUNT () объектов «Заказ», связанных с моим объектом «Клиент» через ID.
Теперь я могу сначала выбрать все объекты Customer, а затем итеративно подсчитать Orders для всех из них, но я думаю, что сделать это в одном запросе повысит производительность. Но я не могу правильно «увлажнить» мой объект Propel, так как он не содержит определения вычисленного поля (ов).
Как бы вы к нему подошли?
Есть несколько вариантов. Во-первых, это создать представление в вашей БД, которое будет делать подсчеты для вас, как и мой ответ здесь . Я делаю это для текущего проекта Symfony, над которым я работаю, где атрибуты только для чтения для данной таблицы на самом деле намного шире, чем сама таблица. Это моя рекомендация, поскольку группировка столбцов (max (), count () и т. Д.) В любом случае доступна только для чтения.
Другие варианты – фактически создать эту функциональность в вашей модели. Вы абсолютно МОЖЕТЕ выполнить эту гидратацию самостоятельно, но это немного сложно. Вот грубые шаги
Однако это не намного лучше, чем вы уже говорите. Для получения одного набора записей вам понадобятся запросы N + 1. Тем не менее, вы можете стать творческим на шаге №3, так что N – это количество вычисленных столбцов, а не количество возвращенных строк.
Другой вариант – создать настраиваемый метод выбора для вашего класса Table Peer.
Вот пример такого подхода
<?php class TablePeer extends BaseTablePeer { public static function selectWithCalculatedColumns() { // Do our custom selection, still using propel's column data constants $sql = " SELECT " . implode( ', ', self::getFieldNames( BasePeer::TYPE_COLNAME ) ) . " , count(" . JoinedTablePeer::ID . ") AS calc_col FROM " . self::TABLE_NAME . " LEFT JOIN " . JoinedTablePeer::TABLE_NAME . " ON " . JoinedTablePeer::ID . " = " . self::FKEY_COLUMN ; // Get the result set $conn = Propel::getConnection(); $stmt = $conn->prepareStatement( $sql ); $rs = $stmt->executeQuery( array(), ResultSet::FETCHMODE_NUM ); // Create an empty rowset $rowset = array(); // Iterate over the result set while ( $rs->next() ) { // Create each row individually $row = new Table(); $startcol = $row->hydrate( $rs ); // Use our custom setter to populate the new column $row->setCalcCol( $row->get( $startcol ) ); $rowset[] = $row; } return $rowset; } }
Могут быть другие решения вашей проблемы, но они не в моих силах. Удачи!
Я делаю это в проекте теперь, переопределяя гидраты () и Peer :: addSelectColumns () для доступа к полям postgis:
// in peer public static function locationAsEWKTColumnIndex() { return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS; } public static function polygonAsEWKTColumnIndex() { return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS + 1; } public static function addSelectColumns(Criteria $criteria) { parent::addSelectColumns($criteria); $criteria->addAsColumn("locationAsEWKT", "AsEWKT(" . GeographyPeer::LOCATION . ")"); $criteria->addAsColumn("polygonAsEWKT", "AsEWKT(" . GeographyPeer::POLYGON . ")"); } // in object public function hydrate($row, $startcol = 0, $rehydrate = false) { $r = parent::hydrate($row, $startcol, $rehydrate); if ($row[GeographyPeer::locationAsEWKTColumnIndex()]) // load GIS info from DB IFF the location field is populated. NOTE: These fields are either both NULL or both NOT NULL, so this IF is OK { $this->location_ = GeoPoint::PointFromEWKT($row[GeographyPeer::locationAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns(). $this->polygon_ = GeoMultiPolygon::MultiPolygonFromEWKT($row[GeographyPeer::polygonAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns(). } return $r; }
В AddAsColumn () есть что-то глупое, но я не могу вспомнить в данный момент, но это действительно работает. Вы можете больше узнать о проблемах AddAsColumn () .
Вот что я сделал, чтобы решить эту проблему без каких-либо дополнительных запросов:
проблема
Необходимо добавить настраиваемое поле COUNT к типичному набору результатов, используемому с пейджером Symfony. Однако, как мы знаем, Propel не поддерживает это в коробке. Поэтому простое решение – просто сделать что-то подобное в шаблоне:
foreach ($pager->getResults() as $project): echo $project->getName() . ' and ' . $project->getNumMembers() endforeach;
Где getNumMembers()
запускает отдельный запрос COUNT для каждого объекта $project
. Конечно, мы знаем, что это крайне неэффективно, потому что вы можете делать COUNT «на лету», добавляя его в качестве столбца в исходный запрос SELECT, сохраняя запрос для каждого отображаемого результата.
У меня было несколько разных страниц, отображающих этот результирующий набор, все с использованием разных критериев. Поэтому написать собственную строку запроса SQL с PDO напрямую было бы слишком сложной задачей, так как мне пришлось бы попасть в объект Criteria и запутаться, пытаясь сформировать строку запроса на основе того, что было в ней!
Таким образом, то, что я делал в конце, позволяет избежать этого, позволяя встроенному кодеку Propel работать с критериями и создавать SQL как обычно.
1 – Сначала создайте [get / set] методы NumMembers () эквивалентного доступа / мутатора в объекте модели, который возвращается функцией doSelect (). Помните, что аксессуар больше не выполняет запрос COUNT, он просто сохраняет свое значение.
2 – Перейдите в класс peer и переопределите родительский метод doSelect () и скопируйте весь код из него точно так, как он есть
3 – Удалите этот бит, потому что getMixerPreSelectHook – частный метод базового однорангового узла (или скопируйте его в сверстника, если вам это нужно):
// symfony_behaviors behavior foreach (sfMixer::getCallables(self::getMixerPreSelectHook(__FUNCTION__)) as $sf_hook) { call_user_func($sf_hook, 'BaseTsProjectPeer', $criteria, $con); }
4 – Теперь добавьте свое настраиваемое поле COUNT в метод doSelect в вашем одноранговом классе:
// copied into ProjectPeer - overrides BaseProjectPeer::doSelectJoinUser() public static function doSelectJoinUser(Criteria $criteria, ...) { // copied from parent method, along with everything else ProjectPeer::addSelectColumns($criteria); $startcol = (ProjectPeer::NUM_COLUMNS - ProjectPeer::NUM_LAZY_LOAD_COLUMNS); UserPeer::addSelectColumns($criteria); // now add our custom COUNT column after all other columns have been added // so as to not screw up Propel's position matching system when hydrating // the Project and User objects. $criteria->addSelectColumn('COUNT(' . ProjectMemberPeer::ID . ')'); // now add the GROUP BY clause to count members by project $criteria->addGroupByColumn(self::ID); // more parent code ... // until we get to this bit inside the hydrating loop: $obj1 = new $cls(); $obj1->hydrate($row); // AND...hydrate our custom COUNT property (the last column) $obj1->setNumMembers($row[count($row) - 1]); // more code copied from parent ... return $results; }
Вот и все. Теперь у вас есть дополнительное поле COUNT, добавленное к вашему объекту, без отдельного запроса, чтобы получить его, когда вы выплевываете результаты. Единственным недостатком этого решения является то, что вам пришлось скопировать весь родительский код, потому что вам нужно добавить бит прямо посередине. Но в моей ситуации это казалось небольшим компромиссом, чтобы сохранить все эти запросы и не писать собственную строку SQL-запроса.
Добавьте к клиенту атрибут «orders_count», а затем напишите что-то вроде этого:
class Order { ... public function save($conn = null) { $customer = $this->getCustomer(); $customer->setOrdersCount($customer->getOrdersCount() + 1); $custoner->save(); parent::save(); } ... }
Вы можете использовать не только метод «save», но и идея остается прежней. К сожалению, Propel не поддерживает никаких «магии» для таких полей.
Propel фактически создает автоматическую функцию, основанную на имени связанного поля. Допустим, у вас есть такая схема:
customer: id: name: ... order: id: customer_id: # links to customer table automagically completed: { type: boolean, default false } ...
Когда вы создадите свою модель, объект Customer будет иметь метод getOrders (), который будет извлекать все заказы, связанные с этим клиентом. Затем вы можете просто использовать count ($ customer-> getOrders ()), чтобы получить количество заказов для этого клиента.
Недостатком является то, что он также будет извлекать и увлажнять объекты Order. На большинстве СУБД единственной разницей в производительности между выводом записей или использованием COUNT () является пропускная способность, используемая для возврата набора результатов. Если эта пропускная способность будет значительна для вашего приложения, вы можете создать метод в объекте Customer, который строит запрос COUNT () вручную, используя Creole:
// in lib/model/Customer.php class Customer extends BaseCustomer { public function CountOrders() { $connection = Propel::getConnection(); $query = "SELECT COUNT(*) AS count FROM %s WHERE customer_id='%s'"; $statement = $connection->prepareStatement(sprintf($query, CustomerPeer::TABLE_NAME, $this->getId()); $resultset = $statement->executeQuery(); $resultset->next(); return $resultset->getInt('count'); } ... }