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

StackOverflow https://stackoverflow.com/questions/246400

  •  05-07-2019
  •  | 
  •  

Вопрос

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

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

Вычисляемый атрибут представляет собой COUNT() объектов «Заказ», связанных с моим объектом «Клиент» через идентификатор.

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

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

Это было полезно?

Решение

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

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

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

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

    <Ол>
  • Выполните шаги 1 и 2 сверху.
  • Напишите пользовательский SQL, который вы будете запрашивать вручную через процесс Propel :: getConnection ().
  • Создайте набор данных вручную, выполнив итерацию по набору результатов, и обработайте пользовательскую гидратацию в этот момент, чтобы не нарушать гидратацию при использовании процессами 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;
        }
    }
    

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

    Другие советы

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

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

    Проблема

    Необходимо добавить пользовательское поле COUNT к типичному набору результатов, используемому с Symfony Pager.Однако, как мы знаем, 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.

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

    class Order {
    ...
      public function save($conn = null) {
        $customer = $this->getCustomer();
        $customer->setOrdersCount($customer->getOrdersCount() + 1);
        $custoner->save();
        parent::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 ()), чтобы получить количество заказов для этого клиента.

    Недостатком является то, что он также будет извлекать и увлажнять эти объекты порядка. В большинстве 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');
        }
        ...
      }
    
    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top