Приложение Symfony - как добавить вычисляемые поля в объекты Propel? - PullRequest
5 голосов
/ 29 октября 2008

Как лучше всего работать с вычисляемыми полями объектов Propel?

Скажем, у меня есть объект "Клиент", у которого есть соответствующая таблица "клиенты", и каждый столбец соответствует атрибуту моего объекта. Я хотел бы сделать следующее: добавить вычисляемый атрибут «Количество выполненных заказов» к моему объекту при его использовании в представлениях А, но не в представлениях В и С.

Рассчитанный атрибут - это COUNT () объектов "Order", связанных с моим объектом "Customer" через идентификатор.

Что я могу сделать сейчас, так это сначала выбрать все объекты Customer, а затем итеративно считать заказы для всех них, но я думаю, что выполнение этого в одном запросе повысит производительность. Но я не могу должным образом «увлажнить» мой объект Propel, поскольку он не содержит определения вычисляемого поля (полей).

Как бы вы подошли к этому?

Ответы [ 5 ]

3 голосов
/ 29 октября 2008

Есть несколько вариантов. Во-первых, это создать представление в вашей БД, которое будет выполнять подсчет за вас, аналогично моему ответу здесь . Я делаю это для текущего проекта Symfony, в котором я работаю, где атрибуты только для чтения для данной таблицы на самом деле намного, намного шире, чем сама таблица. Это моя рекомендация, поскольку группировка столбцов (max (), count () и т. Д.) В любом случае доступна только для чтения.

Другие варианты - встроить эту функцию в вашу модель. Вы абсолютно МОЖЕТЕ сделать это увлажнение самостоятельно, но это немного сложно. Вот грубые шаги

  1. Добавьте столбцы в класс Table в качестве защищенных элементов данных.
  2. Напишите соответствующие методы получения и установки для этих столбцов
  3. Переопределите метод гидратов и внутри, заполните ваши новые столбцы данными из других запросов. Обязательно вызовите parent :: hydrate () в качестве первой строки

Однако это не намного лучше, чем то, о чем вы уже говорили. Вам все равно понадобится N + 1 запрос для получения одного набора записей. Однако вы можете проявить творческий подход на шаге 3, чтобы N было числом вычисляемых столбцов, а не числом возвращенных строк.

Другой вариант - создать собственный метод выбора в Таблице Класс равноправия.

  1. Выполните шаги 1 и 2 сверху.
  2. Напишите пользовательский SQL, который вы будете запрашивать вручную через процесс Propel :: getConnection ().
  3. Создайте набор данных вручную, выполнив итерацию по набору результатов, и обработайте пользовательскую гидратацию в этот момент, чтобы не нарушать гидратацию при использовании процессами doSelect.

Вот пример такого подхода

<?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;
    }
}

Могут быть и другие решения вашей проблемы, но они мне не известны. Желаем удачи!

1 голос
/ 26 марта 2010

Вот что я сделал, чтобы решить эту проблему без дополнительных запросов:

Проблема

Необходимо добавить пользовательское поле 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 - Перейдите в класс равноправного узла и переопределите родительский метод 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.

1 голос
/ 24 марта 2009

Сейчас я делаю это в проекте, переопределяя hydrate () и 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 () .

0 голосов
/ 30 октября 2008

Propel фактически создает автоматическую функцию на основе имени связанного поля. Допустим, у вас есть такая схема:

customer:
  id:
  name:
  ...

order:
  id:
  customer_id: # links to customer table automagically
  completed: { type: boolean, default false }
  ...

Когда вы строите свою модель, ваш объект Customer будет иметь метод getOrders (), который будет извлекать все заказы, связанные с этим клиентом. Затем вы можете просто использовать count ($ customer-> getOrders ()), чтобы получить количество заказов для этого клиента.

Недостатком является то, что он также будет получать и увлажнять эти объекты порядка. В большинстве RDBMS единственная разница в производительности между извлечением записей или использованием 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');
    }
    ...
  }
0 голосов
/ 29 октября 2008

Добавьте атрибут "orders_count" для клиента, а затем напишите что-то вроде этого:

class Order {
...
  public function save($conn = null) {
    $customer = $this->getCustomer();
    $customer->setOrdersCount($customer->getOrdersCount() + 1);
    $custoner->save();
    parent::save();
  }
...
}

Вы можете использовать не только метод «сохранить», но идея остается неизменной. К сожалению, Propel не поддерживает "магию" для таких полей.

...