Question

Quelle est la meilleure façon de travailler avec les champs calculés des objets Propel?

Dites que j'ai un objet " Client " qui a une table correspondante " clients " et chaque colonne correspond à un attribut de mon objet. Ce que je voudrais faire est: ajouter un attribut calculé "Nombre de commandes terminées" à mon objet lorsque je l’utilise sur la vue A mais pas sur les vues B et C.

L'attribut calculé est un COUNT () de "Commande". objets liés à mon " Client " objet via ID.

Ce que je peux faire maintenant, c’est d’abord de sélectionner tous les objets Client, puis de compter de manière itérative les ordres pour tous les objets, mais je pense que le faire en une seule requête améliorerait les performances. Mais je ne peux pas correctement "hydrater". Mon objet Propel puisqu'il ne contient pas la définition du ou des champs calculés.

Comment l'aborderiez-vous?

Était-ce utile?

La solution

Il y a plusieurs choix. Commencez par créer une vue dans votre base de données qui effectuera le décompte à votre place, comme dans ma réponse, ici . Je le fais pour un projet Symfony en cours sur lequel je travaille où les attributs en lecture seule pour une table donnée sont en réalité beaucoup plus larges que la table elle-même. C’est ma recommandation, car regrouper des colonnes (max (), count (), etc.) est de toute façon en lecture seule.

Les autres options consistent à intégrer cette fonctionnalité à votre modèle. Vous POUVEZ absolument faire cette hydratation vous-même, mais c'est un peu compliqué. Voici les étapes approximatives

  1. Ajoutez les colonnes à votre classe Table en tant que membres de données protégés.
  2. Écrivez les getters et les setters appropriés pour ces colonnes
  3. Remplacez la méthode hydrate et remplissez vos nouvelles colonnes avec les données d'autres requêtes. Assurez-vous d’appeler parent :: hydrate () comme première ligne

Cependant, ce n'est pas beaucoup mieux que ce dont vous parlez déjà. Vous aurez toujours besoin de N + 1 requêtes pour récupérer un seul jeu d'enregistrements. Toutefois, vous pouvez créer à l'étape 3 de manière à ce que N soit le nombre de colonnes calculées, et non le nombre de lignes renvoyées.

Une autre option consiste à créer une méthode de sélection personnalisée sur votre classe Table Peer.

  1. Effectuez les étapes 1 et 2 ci-dessus.
  2. Ecrivez un SQL personnalisé que vous interrogerez manuellement via le processus Propel :: getConnection ().
  3. Créez le jeu de données manuellement en effectuant une itération sur le jeu de résultats et gérez l'hydratation personnalisée à ce stade afin de ne pas interrompre l'hydratation lors de son utilisation par les processus doSelect.

Voici un exemple de cette approche

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

Il existe peut-être d'autres solutions à votre problème, mais elles vont au-delà de mes connaissances. Bonne chance!

Autres conseils

Je le fais maintenant dans un projet en surchargeant hydrate () et Peer :: addSelectColumns () pour accéder aux champs 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;
}   

Il y a quelque chose de bizarre avec AddAsColumn (), mais je ne m'en souviens pas pour le moment, mais cela fonctionne. Vous pouvez en savoir plus sur les problèmes AddAsColumn () .

Voici ce que j'ai fait pour résoudre ce problème sans poser de questions supplémentaires:

Problème

Nécessaire pour ajouter un champ COUNT personnalisé à un ensemble de résultats typique utilisé avec Symfony Pager. Cependant, comme nous le savons, Propel ne supporte pas cela dans la boîte. La solution la plus simple est donc de faire quelque chose comme ceci dans le modèle:

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

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

endforeach;

getNumMembers () exécute une requête COUNT distinct pour chaque objet $ project . Bien sûr, nous savons que cela est extrêmement inefficace, car vous pouvez effectuer le décompte à la volée en l'ajoutant sous forme de colonne à la requête SELECT d'origine, en enregistrant une requête pour chaque résultat affiché.

J'ai eu plusieurs pages différentes affichant cet ensemble de résultats, toutes utilisant des critères différents. Donc, écrire ma propre chaîne de requête SQL directement avec PDO serait bien trop compliqué, car il me faudrait entrer dans l’objet Criteria et perdre mon temps à essayer de former une chaîne de requête en fonction de son contenu!

Donc, ce que j'ai fait à la fin évite tout cela, laissant le code natif de Propel fonctionner avec les critères et créer le code SQL comme d'habitude.

1 - Créez tout d'abord les méthodes accessor / mutator équivalentes à [get / set] NumMembers () dans l'objet modèle renvoyé par la méthode doSelect (). Rappelez-vous que l’accesseur ne fait plus la requête COUNT, il ne conserve que sa valeur.

2 - Accédez à la classe des pairs, remplacez la méthode parent doSelect () et copiez-y tout le code exactement tel qu'il est

.

3 - Supprimez ce bit car getMixerPreSelectHook est une méthode privée de l'homologue de base (ou copiez-le dans votre homologue si vous en avez besoin):

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

4 - Ajoutez maintenant votre champ COUNT personnalisé à la méthode doSelect de votre classe homologue:

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

C'est ça. Vous avez maintenant ajouté le champ COUNT supplémentaire à votre objet sans effectuer de requête distincte pour l'obtenir au fur et à mesure que vous crachez les résultats. Le seul inconvénient de cette solution est que vous avez dû copier tout le code parent car vous devez ajouter des bits en plein milieu. Mais dans ma situation, cela semblait être un petit compromis pour enregistrer toutes ces requêtes et ne pas écrire ma propre chaîne de requête SQL.

Ajouter un attribut " orders_count " à un client, puis écrivez quelque chose comme ceci:

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

Vous pouvez utiliser non seulement le " save " méthode, mais l’idée reste la même. Malheureusement, Propel ne supporte aucune option "magique". pour de tels champs.

Propel construit en fait une fonction automatique basée sur le nom du champ lié. Disons que vous avez un schéma comme celui-ci:

customer:
  id:
  name:
  ...

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

Lorsque vous construisez votre modèle, votre objet Client aura une méthode getOrders () qui récupérera toutes les commandes associées à ce client. Vous pouvez ensuite simplement utiliser le compte ($ customer- > getOrders ()) pour obtenir le nombre de commandes de ce client.

L'inconvénient est que cela va également chercher et hydrater ces objets de l'ordre. Sur la plupart des SGBDR, la seule différence de performance entre l'extraction des enregistrements ou l'utilisation de COUNT () est la bande passante utilisée pour renvoyer le jeu de résultats. Si cette bande passante est importante pour votre application, vous pouvez créer une méthode dans l'objet Client qui construit manuellement la requête COUNT () à l'aide de 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');
    }
    ...
  }
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top