viernes, 11 de enero de 2013

Usando JOIN con CDbCriteria (Yii Framework)


Ejemplo de modelo de persistencia tradicional Many-Many en YiiFramework


En el siguiente ejemplo tenemos Correa y Perrito, varios de cada una, a veces necesitamos saber (a) que correas son de un perrito, y a veces al revés, preguntando (b) que perritos usan una correa específica. Aunque este último caso (b) no esta en el código ejemplo puedes inferirlo fácilmente invirtiendo la lógica del caso (a).

Diagrama de Datos

1. Supongamos el siguiente "Diagrama de Clases UML" que muestra cómo estan relacionados los objetos:
















2. Del diagrama UML anterior, se puede deducir un "Diagrama de Datos" (que no es lo mismo aunque se parezcan):



3. Los datos de ejemplo del modelo son:


4.  Tenemos las siguientes Clases modeladas en sistema con Yii Framework y CActiveRecord, obviamente simplificadas para no perder el foco del ejemplo:


  1. class Perrito extends CActiveRecord {
  2.   // $id, $name  
  3. }
  4. class PerritoCorrea extends CActiveRecord {
  5.   // $perrito_id, $correa_id  
  6. }
  7. class Correa extends CActiveRecord {
  8. // $id, $color



5. El requerimiento de negocio es:
 "Listar las correas de un perrito, buscando por el nombre del perrito".

Considera que al buscar por correa, primero hay que ir a la relacion correa-perrito para saber que perritos usan la correa a ser considerada, pero antes, hay que ir desde correa-perrito hacia el perrito para saber si su nombre coincide con la búsqueda.

Esto ultimo se hará con una busqueda basada en el objeto de clase CDbCriteria aplicado en la clase Correa,  y por qué en Correa ? porque el concepto dice que son: "las correas de un perrito"...y no al revés.

Por tanto, en Correa.php, agregamos un método: listarPorPerrito($nombre), que recibe como argumento el nombre del perrito al que queremos sacar a pasear con su correa.


  1. class Correa extends CActiveRecord {
  2. // $id, $color
  3.     public function listarPorPerrito($nombre_perrito){
  4.         $c = new CDbCriteria();
  5.         // queremos buscar las correas del "Perrito.name" igual a "KIM",
  6.         // pero pasando por toda la relacion:
  7.         //
  8.         // select * from Correa 't'  <-- CDbCriteria pone la 't' automaticamente
  9.         //  left join PerritoCorrea PXC on PXC.correa_id = t.id
  10.         //  left join Perrito PX on PX.id = PXC.perrito_id
  11.         // where
  12.         //   PX.name like '%KIM%'
  13.        
  14.         $px  = Perrito::model()->tablename();
  15.         $pxc = PerritoCorrea::model()->tablename();
  16.         $c->join =
  17.            'left join '.$pxc.' PXC on PXC.correa_id = t.id'
  18.           .'left join '.$px.' PX on PXC.perrito_id = PX.id'
  19.         ;
  20.         $c->compare('px.name',$nombre_perrito,true); // <-- 'true' indica "Like"
  21.         return new CActiveDataProvider($this,array(
  22.            'criteria'=>$c,
  23.            'pagination'=>array('pageSize'=>5),
  24.             // el sort no lo pongo para no sacar de foco el ejemplo, pero considera
  25.             // que tu criteria $c dispone de PX.id, PX.name (PX es Perrito), por
  26.             // tanto puedes usar esos nombres para ordernar. experimentar con eso.
  27.         ));
  28.     }
  29. }



El equivalente SQL de este "Criteria" sería:

  1. select * from Correa t
  2.   left join PerritoCorrea PXC on PXC.correa_id = t.id
  3.   left join Perrito PX on PX.id = PXC.perrito_id
  4.   where
  5.   PX.name like '%KIM%'



6. Esta listo, y ahora cómo se usa ?

Como de 50 maneras diferentes..., aqui verás varias:

6.1-primero creas un action en algun controller para poder acceder a la información:

  1. // protected/controller/xxxController.php
  2. public function actionListarCorreas($perrito){
  3.   $dp = Correa::model()->listarPorPerrito($perrito);
  4.   $this->render('correas',array('dataProvider'=>$dp));
  5. }



6.2-ahora, creas una vista llamada "protected/views/xxx/correas.php", o como quieras, y la renderizas así:

  1. // protected/views/xxx/correas.php
  2. $this->widget('zii.widgets.grid.CGridView', array(
  3.    'dataProvider'=>$dataProvider,
  4. ));


6.3-listo, ya dispones de una URL para visualizarla:

index.php?r=/xxx/correas&perrito=kim

a la cual podrias acceder asi:

CHtml::link("ver correas de perrito",array("/xxx/correas","perrito"=>"kim"));

o con ajax desde cualquier parte usando javascript con jQuery:


  1. $.ajax({
  2.   url: 'index.php?r=/xxx/listarcorreas&perrito=kim',
  3.   cache: false, type: 'get',
  4.   success: function(html){
  5.     // $.fancybox(html);  <- si usas fancybox
  6.     // o a rin pelao usando un <div id='ventanita'></div>
  7.     $('#ventanita').html(html);
  8.   }
  9. });


Advertencia en Alto Volúmen, cómo evitar el colapso al usar las relaciones que GII contruye ?:

Si por ejemplo tuviésemos un modelo de datos "Marca->Factura", entonces pudiésemos tener 100 marcas y 500000 de facturas del otro lado, entonces, que sucede si una de esas marcas tiene 100.000 facturas ? sucederá que al usar relations() (foreach($marca->facturas as $f) echo $f->numero; ) esto colapsará estrepitosamente, debido a que relations() usa arrays.  Este ejemplo que pongo acá en el blog está basado en CDbCriteria, justamente para evitar el colapso, entregando DataProviders en vez de arrays.

Espero te haya servido.
el código de ejemplo esta aquí:  http://pastebin.com/ghxQ8GGn


OTRO EJEMPLO
/*
 relaciones:
 
 Paciente {id,  nombre}
 Centro {id, nombre}
 PacienteCentro {paciente_id,centro_id}
 
 USO:
 
 $centro = Centro::model()->findByAttributes(array("nombre"=>"villa portales block2"));
 $dataProvider = $centro->pacientes;
 // o con argumentos:
        $dataProvider = $centro->getPacientes("salazar");  // todos los salazar del centro seleccionado
*/
class Centro extends CActiveRecord {
public function getPacientes($filtra_por_nombre_paciente=null, $pagesize=5){
 $c = new CDbCriteria();
 $px  = Paciente::model()->tablename();
 $pxc = PacienteCentro::model()->tablename();
 $c->join =
  'left join '.$pxc.' PXC on PXC.centro_id = t.id'
  .'left join '.$px.' PX on PXC.paciente_id = PX.id'
 ;
        if(null != $filtra_por_nombre_paciente)
  $c->compare('px.nombre',$filtra_por_nombre_paciente,true);
 return new CActiveDataProvider($this,array(
  'criteria'=>$c,
  'pagination'=>array('pageSize'=>$pagesize),
 ));
}
}//eof

class Paciente extends CActiveRecord {
public function getCentros($filtra_por_nombre_centro=null, $pagesize=5){
 $c = new CDbCriteria();
 $cx  = Centro::model()->tablename();
 $pxc = PacienteCentro::model()->tablename();
 $c->join =
  'left join '.$pxc.' PXC on PXC.paciente_id = t.id'
  .'left join '.$cx.' CX on PXC.centro_id = CX.id'
 ;
        if(null != $filtra_por_nombre_centro)
  $c->compare('cx.nombre',$filtra_por_nombre_centro,true);
 return new CActiveDataProvider($this,array(
  'criteria'=>$c,
  'pagination'=>array('pageSize'=>$pagesize),
 ));
}
}//eof

3 comentarios:

  1. Gracias Crhistian! Muy bien explicado! Un Abrazo!

    ResponderEliminar
  2. Christian gracias por tus aportes, creo que en todos lados donde busco algo encuentro respuesta tuyas :D

    ResponderEliminar