2008-10-29 1 views
5

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

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

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

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

Как вы к этому подходите?

ответ

3

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

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

  1. Добавить столбцы в таблице класс как защищенные члены данных.
  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; 
    } 
} 

Там может быть и другими решениями вашей проблемы, но они выходят за рамками моих знаний. Удачи!

+0

Спасибо за этот ответ - это помогло мне решить другую проблему! – 2008-11-03 16:01:15

0

Добавить атрибут «orders_count» к Заказчику, а затем написать что-то вроде этого:

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

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

+0

Я рассматриваю это, тоже - спасибо! – 2008-10-31 07:58:34

0

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() является пропускная способность, используемая для возврата набора результатов. Если пропускная способность будет иметь существенное значение для вашего приложения, вы можете создать метод в объекте клиента, который строит запрос 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

Я думал, что увлажнение ухудшает мою работу. Спасибо за подсказку с пользовательской функцией COUNT, хотя! – 2008-10-31 08:02:01

1

Я делаю это в проекте сейчас, перекрывая гидрат() и 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(), но я не могу вспомнить, на данный момент, но это работает. Вы можете read more about the AddAsColumn() issues.

1

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

Проблема

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

Смежные вопросы