2008-10-29 25 views
5

¿Cuál es la mejor forma de trabajar con los campos calculados de los objetos Propel?Aplicación Symfony: cómo agregar campos calculados a los 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 órdenes completadas" a mi objeto al usarlo en la Vista A, pero no en las Vistas B y C.

El atributo calculado es un COUNT() de "Orden "objetos vinculados a mi objeto" Cliente "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 campo (s) calculado (s).

¿Cómo te acercarías?

Respuesta

3

Hay varias opciones. Primero, es crear una vista en su base de datos que haga los recuentos por usted, similar a mi respuesta here. 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 construir esta funcionalidad en su modelo. Usted PUEDE hacer esta hidratación usted mismo, pero es un poco complicado. He aquí los pasos ásperos

  1. Añadir las columnas a la tabla clase como miembros de datos protegidos.
  2. Escriba los captadores y adaptadores apropiados para estas columnas
  3. Anule el método de hidrato y, dentro, rellene las columnas nuevas 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 usted está hablando. Todavía necesitará N + 1 consultas para recuperar un solo 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 Peer.

  1. Realice los pasos 1 y 2 desde arriba.
  2. Escriba el 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 romper la hidratación cuando se usa con los procesos de doSelect.

He aquí 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 son más allá de mi conocimiento. ¡La mejor de las suertes!

+0

Gracias por esa respuesta, ¡me ha ayudado a resolver un problema diferente! –

0

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

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

Usted puede utilizar no sólo el método de "salvar", pero la idea sigue siendo la misma. Desafortunadamente, Propel no admite ninguna "magia" para dichos campos.

+0

Estoy considerando esto también, ¡gracias! –

0

Propel realmente construye una función automática basada en el nombre del campo vinculado. Digamos que usted tiene un esquema como éste:

customer: 
    id: 
    name: 
    ... 

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

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

La desventaja es que esto también traerá e hidratará esos objetos Order. En la mayoría de RDBMS, la única diferencia de rendimiento entre tirar de 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 al cliente que se basa la consulta COUNT() de forma manual utilizando criolla:

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

Pensé que la hidratación perjudica mi rendimiento. ¡Gracias por la sugerencia con la función COUNT personalizada, sin embargo! –

+0

Depende de cuántos registros estamos hablando, pero tiene razón. Estaba hablando más sobre el lado del back-end de la base de datos. –

1

que estoy haciendo esto en un proyecto ahora reemplazando el hidrato() 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 torpe con AddAsColumn(), pero no puedo recordar en este momento, pero esto funciona. Puede read more about the AddAsColumn() issues.

1

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

Problema

necesario para añadir un campo COUNT personalizado a un conjunto de resultados típicos se utiliza con el Symfony Pager. Sin embargo, como sabemos, Propel no es compatible con esto. 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; 

Dónde getNumMembers() ejecuta una consulta COUNT separado para cada objeto $project. Por supuesto, sabemos que esto es extremadamente ineficiente porque puede hacer el COUNT sobre la marcha al agregarlo como una columna a la consulta SELECT original, guardando una consulta para cada resultado mostrado.

Tenía varias páginas diferentes que mostraban este conjunto de resultados, todas con diferentes criterios. ¡Así que escribir directamente mi propia cadena de consulta SQL con PDO 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 basada en lo que haya en ella!

Por lo tanto, lo que hice al final evita todo eso, dejando que el código nativo de Propel trabaje con los criterios y cree el SQL como de costumbre.

1 - Primero cree los métodos [get/set] NumMembers() equivalentes de acceso/mutador en el objeto modelo que regresa con doSelect(). Recuerde, el programa de acceso ya no hace la consulta COUNT, simplemente mantiene su valor.

2 - Ir a la clase peer y anular los padres método doSelect() y copiar todo el código de la misma tal y como es

3 - Eliminar este poco porque getMixerPreSelectHook es un método privado de la base peer (o copiarlo en su equipo 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 personalizadas cuenta con el método doSelect en su clase 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;   
} 

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.

Cuestiones relacionadas