Domanda

Qual è il modo migliore di lavorare con campi calcolati di oggetti Propel?

Supponi di avere un oggetto " Cliente " che ha una tabella corrispondente "clienti" e ogni colonna corrisponde a un attributo del mio oggetto. Quello che vorrei fare è: aggiungere un attributo calcolato " Numero di ordini completati " al mio oggetto quando lo uso su View A ma non su Views B e C.

L'attributo calcolato è un COUNT () di " Ordine " oggetti collegati al mio " Cliente " oggetto tramite ID.

Quello che posso fare ora è prima selezionare tutti gli oggetti Cliente, quindi conteggiare iterativamente gli ordini per tutti loro, ma penso che farlo in una singola query migliorerebbe le prestazioni. Ma non riesco correttamente a "idratare" il mio oggetto Propel poiché non contiene la definizione dei campi calcolati.

Come lo approcceresti?

È stato utile?

Soluzione

Esistono diverse opzioni. Innanzitutto, è creare una vista nel tuo DB che farà i conti per te, in modo simile alla mia risposta qui . Faccio questo per un attuale progetto Symfony su cui lavoro in cui gli attributi di sola lettura per una data tabella sono in realtà molto, molto più ampi della tabella stessa. Questa è la mia raccomandazione poiché le colonne di raggruppamento (max (), count (), ecc.) Sono comunque di sola lettura.

Le altre opzioni sono di integrare effettivamente questa funzionalità nel tuo modello. Puoi assolutamente farlo da solo, ma è un po 'complicato. Ecco i passaggi approssimativi

  1. Aggiungi le colonne alla tua classe Tabella come membri di dati protetti.
  2. Scrivi i getter e setter appropriati per queste colonne
  3. Ignora il metodo Hydrate e, all'interno, popola le tue nuove colonne con i dati di altre query. Assicurati di chiamare parent :: hydrate () come prima riga

Tuttavia, questo non è molto meglio di quello di cui stai già parlando. Avrai ancora bisogno di N + 1 query per recuperare un singolo set di record. Tuttavia, puoi ottenere creatività nel passaggio 3 in modo che N sia il numero di colonne calcolate, non il numero di righe restituite.

Un'altra opzione è quella di creare un metodo di selezione personalizzato sulla tua tabella classe peer.

  1. Esegui i passaggi 1 e 2 dall'alto.
  2. Scrivi SQL personalizzato che eseguirai una query manualmente tramite il processo Propel :: getConnection ().
  3. Crea manualmente il set di dati ripetendo il set di risultati e gestisci l'idratazione personalizzata a questo punto in modo da non interrompere l'idratazione quando viene utilizzata dai processi doSelect.

Ecco un esempio di questo approccio

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

Potrebbero esserci altre soluzioni al tuo problema, ma sono al di là delle mie conoscenze. Buona fortuna!

Altri suggerimenti

Lo sto facendo ora in un progetto sovrascrivendo hydrate () e Peer :: addSelectColumns () per accedere ai campi 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;
}   

C'è qualcosa di sciocco con AddAsColumn () ma al momento non ricordo, ma funziona. Puoi leggere ulteriori informazioni sugli argomenti AddAsColumn () .

Ecco cosa ho fatto per risolverlo senza ulteriori domande:

problema

Necessario per aggiungere un campo COUNT personalizzato a un set di risultati tipico usato con Symfony Pager. Tuttavia, come sappiamo, Propel non lo supporta immediatamente. Quindi la soluzione semplice è fare qualcosa del genere nel modello:

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

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

endforeach;

Dove getNumMembers () esegue una COUNT query separata per ciascun oggetto $ project . Naturalmente, sappiamo che questo è gravemente inefficiente perché puoi eseguire COUNT al volo aggiungendolo come colonna alla query SELECT originale, salvando una query per ogni risultato visualizzato.

Avevo diverse pagine che mostravano questo set di risultati, tutti usando criteri diversi. Quindi scrivere direttamente la mia stringa di query SQL con PDO sarebbe una seccatura, dato che dovrei entrare nell'oggetto Criteria e cercare di formare una stringa di query basata su qualsiasi cosa contenesse!

Quindi, ciò che ho fatto alla fine evita tutto ciò, lasciando che il codice nativo di Propel funzioni con i criteri e crei l'SQL come al solito.

1 - Per prima cosa creare i metodi di accessor / mutator [get / set] NumMembers () equivalenti nell'oggetto modello che viene restituito da doSelect (). Ricorda, l'accessor non esegue più la COUNT query, ma mantiene solo il suo valore.

2 - Entra nella classe peer e sovrascrive il metodo doSelect () genitore e copia tutto il codice da esso esattamente com'è

3 - Rimuovi questo bit perché getMixerPreSelectHook è un metodo privato del peer di base (o copialo nel tuo peer se ne hai bisogno):

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

4 - Ora aggiungi il tuo campo COUNT personalizzato al metodo doSelect nella tua classe peer:

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

Questo è tutto. Ora hai aggiunto il campo COUNT aggiuntivo al tuo oggetto senza fare una query separata per ottenerlo mentre sputi i risultati. L'unico svantaggio di questa soluzione è che hai dovuto copiare tutto il codice padre perché devi aggiungere bit nel mezzo. Ma nella mia situazione, questo sembrava un piccolo compromesso per salvare tutte quelle query e non scrivere la mia stringa di query SQL.

Aggiungi un attributo " ordini_conto " a un cliente, quindi scrivi qualcosa del genere:

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

Puoi utilizzare non solo il " salva " metodo, ma l'idea rimane la stessa. Sfortunatamente, Propel non supporta alcuna "magia" per tali campi.

Propel crea effettivamente una funzione automatica basata sul nome del campo collegato. Diciamo che hai uno schema come questo:

customer:
  id:
  name:
  ...

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

Quando compili il tuo modello, l'oggetto Cliente avrà un metodo getOrders () che recupererà tutti gli ordini associati a quel cliente. Puoi quindi semplicemente utilizzare count ($ customer- > getOrders ()) per ottenere il numero di ordini per quel cliente.

Il rovescio della medaglia è che questo recupererà e idraterà anche quegli oggetti dell'Ordine. Nella maggior parte dei RDBMS, l'unica differenza di prestazioni tra l'estrazione dei record o l'utilizzo di COUNT () è la larghezza di banda utilizzata per restituire il set di risultati. Se quella larghezza di banda fosse significativa per la tua applicazione, potresti voler creare un metodo nell'oggetto Customer che crea manualmente la query COUNT () utilizzando 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');
    }
    ...
  }
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top