Приложение Symfony - как добавить вычисляемые поля для продвижения объектов?


Каков наилучший способ работы с вычисляемыми полями движущихся объектов?

Допустим, у меня есть объект "Клиент", у которого есть соответствующая таблица "клиенты", и каждый столбец соответствует атрибуту моего объекта. Что я хотел бы сделать, так это: добавить вычисляемый атрибут "Количество выполненных заказов" к моему объекту при его использовании в представлении A, но не в представлениях B и C.

Вычисляемый атрибут - это КОЛИЧЕСТВО() объектов "Заказа", связанных с моим объектом "Клиент" через Идентификатор.

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

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

Author: Tomas Kohl, 2008-10-29

5 answers

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

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

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

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

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

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

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

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

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

 3
Author: Peter Bailey, 2017-05-23 12:19:33

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

 1
Author: apinstein, 2009-03-23 21:16:10

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

Проблема

Необходимо добавить настраиваемое поле ПОДСЧЕТА в типичный результирующий набор, используемый с пейджером Symfony. Однако, как мы знаем, Propel не поддерживает это из коробки. Таким образом, простое решение - просто сделать что-то подобное в шаблоне:

foreach ($pager->getResults() as $project):

 echo $project->getName() . ' and ' . $project->getNumMembers()

endforeach;

Где getNumMembers() выполняет отдельный запрос ПОДСЧЕТА для каждого объекта $project. Конечно, мы знаем, что это крайне неэффективно, потому что вы можете ПОСЧИТАТЬ на лету добавляя его в качестве столбца в исходный запрос SELECT, сохраняя запрос для каждого отображаемого результата.

У меня было несколько разных страниц, отображающих этот набор результатов, все с использованием разных критериев. Поэтому написание моей собственной строки запроса SQL с помощью PDO напрямую было бы слишком хлопотно, так как мне пришлось бы входить в объект критериев и возиться, пытаясь сформировать строку запроса на основе того, что в ней было!

Итак, то, что я сделал в конце концов, позволяет избежать всего этого, позволяя машинному коду 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 - Теперь добавьте свое пользовательское поле ПОДСЧЕТА в метод 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;         
}

Вот и все. Теперь у вас есть дополнительное поле ПОДСЧЕТА, добавленное к вашему объекту, без выполнения отдельного запроса, чтобы получить его при выводе результатов. Единственным недостатком этого решения является то, что вам пришлось скопировать весь родительский код, потому что вам нужно добавить биты прямо в его середине. Но в моей ситуации это казалось небольшим компромисс, чтобы сохранить все эти запросы и не писать свою собственную строку SQL-запроса.

 1
Author: Ashton King, 2010-03-26 15:10:40

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

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

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

 0
Author: , 2008-10-29 14:00:11

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

customer:
  id:
  name:
  ...

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

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

Недостатком является то, что это также приведет к извлечению и увлажнению этих объектов порядка. В большинстве СУБД единственным разница в производительности между извлечением записей или использованием COUNT() заключается в пропускной способности, используемой для возврата набора результатов. Если эта пропускная способность будет существенной для вашего приложения, вам может потребоваться создать метод в объекте Customer, который строит запрос COUNT() вручную, используя креольский язык:

  // 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
Author: Nathan Strong, 2008-10-30 16:58:48