2008-10-29 12 views
5

Jaki jest najlepszy sposób pracy z polami obliczeniowymi obiektów Propel?Aplikacja Symfony - jak dodawać pola obliczeniowe do obiektów Propel?

że mam obiektu „Klient”, który ma odpowiedniej tabeli „klientów”, a każda kolumna odpowiada atrybut mojego obiektu. Chciałbym: dodać do mojego obiektu atrybut obliczeniowy "Liczba zrealizowanych zamówień", gdy używam go w widoku A, ale nie w widokach B i C.

Obliczony atrybut to COUNT() "zamówienia "obiekty powiązane z moim obiektem" klienta "za pomocą identyfikatora.

Co mogę teraz zrobić, to najpierw zaznaczyć wszystkie obiekty klienta, następnie iteracyjnie liczyć zapisów na wszystkie z nich, ale ja myślę, że robi to w jednym zapytaniu by poprawić wydajność. Ale nie mogę właściwie "uwadniać" mojego obiektu Propela, ponieważ nie zawiera on definicji pól obliczeniowych.

Jak podejdziesz do tego?

Odpowiedz

3

Istnieje kilka możliwości. Po pierwsze, należy utworzyć widok w twoim DB, który zrobi dla ciebie liczby, podobnie jak moja odpowiedź here. Robię to dla bieżącego projektu Symfony, nad którym pracuję, gdzie atrybuty tylko do odczytu dla danej tabeli są w rzeczywistości dużo, dużo szersze niż sama tabela. To jest moja rekomendacja, ponieważ grupowanie kolumn (max(), count(), itp) jest i tak tylko do odczytu.

inne opcje są rzeczywiście budować tę funkcjonalność do swojego modelu. Absolutnie MOŻESZ zrobić to samo nawodnienie, ale to trochę skomplikowane. Oto ostre kroki

  1. dodać kolumny do tabeli klasę jako chronione członków danych.
  2. Napisz odpowiednie procedury pobierające i ustawiające dla tych kolumn
  3. Zastąp metodę hydratacji i wewnątrz, zapełnij nowe kolumny danymi z innych zapytań. Pamiętaj, aby wywołać funkcję parent :: hydate() jako pierwszą linię:

Jednak to nie jest dużo lepsze niż to, o czym już mówisz. Nadal będziesz potrzebował N + 1 kwerendy, aby pobrać pojedynczy zestaw rekordów. Jednak w kroku 3 można uzyskać kreatywność, aby N była liczbą kolumn obliczeniowych, a nie liczbą zwróconych wierszy.

Inną opcją jest utworzenie niestandardowej metody wyboru na klasie Peer Tabela.

  1. Wykonaj kroki 1 i 2 z góry.
  2. Napisz niestandardowy kod SQL, który zapytasz ręcznie za pomocą procesu Propel :: getConnection().
  3. Tworzenie zestawu danych ręcznie przez powtarzanie całego zbioru wynikowego i obsługiwać niestandardową nawilżenie w tym momencie, aby nie złamać nawilżenie gdy stosowanie przez procesy doSelect.

Oto przykład tego podejścia

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

Nie mogą być inne rozwiązania swojego problemu, ale są poza moją wiedzą. Powodzenia!

+0

Dzięki za odpowiedź - pomogło mi to rozwiązać inny problem! –

0

dodać atrybut „orders_count” do klienta, a następnie napisać coś takiego:

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

można używać nie tylko „Zapisz” metody, ale idea pozostaje taka sama. Niestety, Propel nie obsługuje żadnej "magii" dla takich pól.

+0

Rozważam to również - dzięki! –

0

Propel faktycznie buduje funkcję automatyczną na podstawie nazwy połączonego pola. Powiedzmy, że masz schemat takiego:

customer: 
    id: 
    name: 
    ... 

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

Podczas tworzenia modelu, Twój obiekt Klient ma metoda A getOrders(), która pobiera wszystkie zamówienia związane z tym klientem. Następnie możesz po prostu użyć count ($ customer-> getOrders()), aby uzyskać liczbę zamówień dla tego klienta.

Minusem jest to, że spowoduje to również pobranie i nawodnienie tych obiektów zamówienia. W większości RDBMS jedyną różnicą w wydajności między pobieraniem rekordów lub używaniem COUNT() jest przepustowość używana do zwrócenia zestawu wyników. Jeśli to pasmo byłoby istotne dla aplikacji, możesz utworzyć metody w obiekcie klienta, który buduje COUNT() kwerendę ręcznie przy użyciu 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

Myślałem, że nawilżenie rani moje wyniki. Dzięki za podpowiedź z niestandardową funkcją COUNT! –

+0

Zależy od tego, ile rekordów mówimy, ale masz rację. Mówiłem bardziej po stronie bazy danych rzeczy. –

1

Robię to w projekcie teraz nadrzędnymi hydrat() i Peer :: addSelectColumns() dostępu do pól 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; 
} 

jest coś goofy z AddAsColumn(), ale nie pamiętam w tej chwili, ale to nie działa. Możesz read more about the AddAsColumn() issues.

1

Oto co zrobiłem, aby rozwiązać ten problem bez żadnych dodatkowych zapytań:

Problem

potrzebne, aby dodać niestandardowe pole liczyć do typowego zestawu wyników używanego z Symfony pager. Jednak, jak wiemy, Propel nie obsługuje tego po wyjęciu z pudełka. Tak więc proste rozwiązanie jest po prostu zrobić coś takiego w szablonie:

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

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

endforeach; 

Gdzie getNumMembers() prowadzi oddzielne kwerendy liczyć dla każdego obiektu $project. Oczywiście wiemy, że jest to bardzo nieefektywne, ponieważ możesz wykonać COUNT w locie, dodając go jako kolumnę do oryginalnego zapytania SELECT, zapisując zapytanie dla każdego wyświetlanego wyniku.

Miałem kilka różnych stron wyświetlających ten zestaw wyników, wszystkie używające różnych kryteriów. Napisanie własnego ciągu zapytań SQL z PDO byłoby więc zbyt kłopotliwe, ponieważ musiałbym dostać się do obiektu Criteria i rozejrzeć się, próbując utworzyć ciąg zapytania na podstawie tego, co w nim było!

To, co zrobiłem na koniec, unika tego wszystkiego, pozwalając, aby natywny kod Propla działał z kryteriami i jak zwykle tworzył SQL.

1 - najpierw utworzyć [get/ustawienie] NumMembers() równoważne metody dostępowej/modyfikujące w modelu obiektu zostanie zwrotu przez doSelect(). Pamiętaj, że akcesor nie wykonuje już zapytania COUNT, a jedynie przechowuje jego wartość.

2 - Idź do klasy rówieśniczej i zastąpić rodzica doSelect() metoda i skopiować cały kod z nią dokładnie, jak to jest

3 - Usuń ten bit bo getMixerPreSelectHook to prywatna metoda peer bazowej (lub skopiuj go do swojej peer, jeśli jest to potrzebne):

// symfony_behaviors behavior 
foreach (sfMixer::getCallables(self::getMixerPreSelectHook(__FUNCTION__)) as $sf_hook) 
{ 
    call_user_func($sf_hook, 'BaseTsProjectPeer', $criteria, $con); 
} 

4 - teraz dodać niestandardowe pole liczyć do sposobu doSelect w swojej klasie rówieśniczej:

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

to wszystko. Teraz masz dodane dodatkowe pole COUNT do twojego obiektu, nie robiąc oddzielnego zapytania, aby uzyskać je, gdy wyplujesz wyniki. Jedyną wadą tego rozwiązania jest to, że musisz skopiować cały kod nadrzędny, ponieważ musisz dodać bity dokładnie w jego środku. Ale w mojej sytuacji wydawało mi się to małym kompromisem, aby zapisać wszystkie te zapytania i nie pisać własnego ciągu zapytania SQL.

Powiązane problemy