Pregunta

¿Cuál es la mejor manera de trabajar con campos calculados de objetos Propel?

Digamos que tengo un objeto " Cliente " que tiene una tabla correspondiente " clientes " y cada columna corresponde a un atributo de mi objeto. Lo que me gustaría hacer es: agregar un atributo calculado " Número de pedidos completados " a mi objeto cuando lo uso en la Vista A pero no en las Vistas B y C.

El atributo calculado es un COUNT () de " Orden " objetos vinculados a mi " Cliente " objeto a través de ID.

Lo que puedo hacer ahora es seleccionar primero todos los objetos del Cliente, luego contar iterativamente los Pedidos para todos ellos, pero creo que hacerlo en una sola consulta mejoraría el rendimiento. Pero no puedo hidratar adecuadamente mi objeto Propel, ya que no contiene la definición de los campos calculados.

¿Cómo lo abordarías?

¿Fue útil?

Solución

Hay varias opciones. Primero, es crear una vista en su base de datos que haga los conteos por usted, similar a mi respuesta aquí . Hago esto para un proyecto actual de Symfony en el que trabajo donde los atributos de solo lectura para una tabla dada son en realidad mucho, mucho más amplios que la tabla misma. Esta es mi recomendación ya que las columnas de agrupación (max (), count (), etc.) son de solo lectura de todos modos.

Las otras opciones son realmente construir esta funcionalidad en su modelo. Usted absolutamente PUEDE hacer esta hidratación usted mismo, pero es un poco complicado. Aquí están los pasos aproximados

  1. Agregue las columnas a su clase Table como miembros de datos protegidos.
  2. Escriba los captadores y definidores adecuados para estas columnas
  3. Anule el método de hidrato y, dentro, complete sus nuevas columnas con los datos de otras consultas. Asegúrese de llamar a parent :: hydrate () como la primera línea

Sin embargo, esto no es mucho mejor de lo que ya estás hablando. Aún necesitará N + 1 consultas para recuperar un único conjunto de registros. Sin embargo, puede ser creativo en el paso 3 para que N sea el número de columnas calculadas, no el número de filas devueltas.

Otra opción es crear un método de selección personalizado en su Tabla Clase de pares.

  1. Haz los pasos 1 y 2 de arriba.
  2. Escriba SQL personalizado que consultará manualmente a través del proceso Propel :: getConnection ().
  3. Cree el conjunto de datos manualmente iterando sobre el conjunto de resultados y maneje la hidratación personalizada en este punto para no interrumpir la hidratación cuando los procesos doSelect lo usen.

Aquí hay un ejemplo de este enfoque

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

Puede haber otras soluciones a su problema, pero están más allá de mi conocimiento. ¡Mucha suerte!

Otros consejos

Estoy haciendo esto en un proyecto ahora al reemplazar a hydrate () y Peer :: addSelectColumns () para acceder a los campos de 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;
}   

Hay algo tonto con AddAsColumn () pero no puedo recordar por el momento, pero esto funciona. Puede leer más sobre los problemas de AddAsColumn () .

Esto es lo que hice para resolver esto sin ninguna consulta adicional:

Problema

Necesario para agregar un campo COUNT personalizado a un conjunto de resultados típico utilizado con el Buscapersonas de Symfony. Sin embargo, como sabemos, Propel no admite esto en el cuadro. Así que la solución fácil es simplemente hacer algo como esto en la plantilla:

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

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

endforeach;

Donde getNumMembers () ejecuta una consulta COUNT independiente para cada objeto $ project . Por supuesto, sabemos que esto es extremadamente ineficiente porque puede hacer el RECUENTO sobre la marcha agregándolo como una columna a la consulta SELECT original, guardando una consulta para cada resultado que se muestra.

Tenía varias páginas diferentes que mostraban este conjunto de resultados, todas con diferentes Criterios. Por lo tanto, escribir mi propia cadena de consulta SQL con PDO directamente sería demasiado complicado ya que tendría que entrar en el objeto Criteria y perder el tiempo tratando de formar una cadena de consulta en función de lo que haya en él.

Entonces, lo que hice al final evita todo eso, permitiendo que el código nativo de Propel funcione con los Criterios y cree el SQL como de costumbre.

1 - Primero cree los métodos equivalentes de acceso / mutador de [Obtener / establecer] NumMembers () en el objeto modelo que obtiene la devolución por el doSelect (). Recuerde, el descriptor de acceso ya no realiza la consulta COUNT, solo mantiene su valor.

2: ingrese en la clase de pares y anule el método principal doSelect () y copie todo el código tal y como está

3 - Elimine este bit porque getMixerPreSelectHook es un método privado del par base (o cópielo en su par si lo necesita):

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

4 - Ahora agregue su campo COUNT personalizado al método doSelect en su clase de pares:

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

Eso es todo. Ahora tiene el campo COUNT adicional agregado a su objeto sin hacer una consulta por separado para obtenerlo a medida que escupe los resultados. El único inconveniente de esta solución es que ha tenido que copiar todo el código principal porque necesita agregar bits justo en el medio. Pero en mi situación, esto parecía un pequeño compromiso para guardar todas esas consultas y no escribir mi propia cadena de consulta SQL.

Añadir un atributo " orders_count " a un cliente y luego escriba algo como esto:

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

Puede usar no solo el " guardar " método, pero la idea sigue siendo la misma. Desafortunadamente, Propel no admite ninguna magia. para tales campos.

En realidad, Propel crea una función automática basada en el nombre del campo vinculado. Digamos que tiene un esquema como este:

customer:
  id:
  name:
  ...

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

Cuando construya su modelo, su objeto Cliente tendrá un método getOrders () que recuperará todos los pedidos asociados con ese cliente. Luego, simplemente puede usar count ($ customer- > getOrders ()) para obtener el número de pedidos para ese cliente.

El inconveniente es que también recuperará e hidratará los objetos de la Orden. En la mayoría de los RDBMS, la única diferencia de rendimiento entre extraer los registros o usar COUNT () es el ancho de banda utilizado para devolver el conjunto de resultados. Si ese ancho de banda sería significativo para su aplicación, es posible que desee crear un método en el objeto Cliente que genere la consulta COUNT () manualmente usando Criollo:

  // 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');
    }
    ...
  }
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top