0 follower

Registro Activo Relacional

Ya hemos visto como usar Registro Activo (AR) para seleccionar datos desde una tabla sencilla de la base de datos. En esta sección, describiremos como usar AR para unir varias tablas relacionadas de la base de datos y obtener de vuelta el conjunto de datos unidos.

Para usar AR relacional, se requiere que las relaciones claver primaria-foránea estén bien definidas entre las tablas que necesitan ser unidas. AR depende de los metadatos acerca de estas relaciones para determinar como unir las tablas.

Nota: Comenzando desde la versión 1.0.1, podemos usar AR relacional aún si no definimos ninguna clave foránea en nuestra base de datos.

Por sencillez, usamos el esquema de la base de datos mostrado en el siguiente diagrama entidad-relacion (ER) para ilustrar ejemplos en esta sección.

Diagrama ER

Diagrama ER

Información: El soporte para la clave foránea varía en diferentes DBMS.

SQLite no soporta claves foráneas, pero podemos todavía declararlas cuando creamos las tablas. AR puede aprovechar estas declataciones para soportar correctamente las consultas relacionales.

MySQL soporta claves foráneas con el motor InnoDB, pero no con MyISAM. Es recomendable usar InnoDB para nuestra base de datos. Cuando usamos MyISAM, podemos aprovechar el siguiente truco para que podamos ejecutar las consultas relacionales usando AR:

CREATE TABLE Foo
(
  id INTEGER NOT NULL PRIMARY KEY
);
CREATE TABLE bar
(
  id INTEGER NOT NULL PRIMARY KEY,
  fooID INTEGER
     COMMENT 'CONSTRAINT FOREIGN KEY (fooID) REFERENCES Foo(id)'
);

En lo anterior, usamos la palabra clave COMMENT para describir la clave foránea el cual puede ser leído por AR para reconocer la relación descripta.

1. Declarando Relaciones

Antes de usar AR para ejecutar consultas relacionales, necesitamos darle conocer a AR como una clase AR se relaciona con otra.

La relación entre dos clases AR está directamente asociada con la relación entre las tablas de la base de datos representadas por esas clases. Desde el punto de vista de la base de datos, una relación entre dos tablas A y B tiene tres tipos: uno-a-muchos (ej.: User y Post), uno-a-uno (ej.: User y Profile) y muchos-a-muchos (ej.: Category y Post). En AR, hay cuatro tipo de relaciones:

  • BELONGS_TO: si la relación entre la tabla A y B es uno-a-muchos, entonces B pertenece a A (ej.: Post pertenece a User);

  • HAS_MANY: si la relación entre la tabla A y B es uno-a-muchos, entonces A tiene muchos B (ej.: User tiene muchos Post);

  • HAS_ONE: este es un caso especial de HAS_MANY donde A tiene a lo sumo un B (ej.: User tiene a lo sumo un Profile);

  • MANY_MANY: corresponde a la relación muchos-a-muchos en la base de datos Una tabla asociativa es necesaria para romper una relación muchos-a-muchos en relaciones uno-a-muchos, ya que la mayoría de los DBMS no soportan directamente la relación muchos-a-muchos. En nuestro esquema de la base de datos de ejemplo, la tabla PostCategory sirve para este propósito. En terminología AR, podemos explicar MANY_MANY como la combinación de BELONGS_TO y HAS_MANY. Por ejemplo, Post pertenece a muchas Category y Category tiene muchos Post.

Declarar relaciones en AR involucra sobreescribir el método relations() de CActiveRecord. El método devuelve un arreglo de configuraciones de relaciones. Cada elemento del arreglo representa una sola relación con el siguiente formato:

'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options)

donde VarName es el nombre de la relación; RelationType especifica el tipo de relación, que puede ser una de las cuatro constantes: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY y self::MANY_MANY; ClassName es el nombre de la clase relacionada a ésta clase AR; y ForeignKey especifica la(s) clave(s) foránea(s) involucrada(s) en la relación. Pueden ser especificadas opciones adicionales al finad de cada relación (será descripto luego).

El siguiente código muestra como declarar las relaciones para las clases User y Post.

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'authorID'),
            'categories'=>array(self::MANY_MANY, 'Category', 'PostCategory(postID, categoryID)'),
        );
    }
}
 
class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

Información: Una clave foránea puede ser conpuesta, cosistiendo de dos o más columnas. En este caso, debemos concatenar los nombres de la clave foránea y separarlos con un espacio o coma. Para las relaciones de tipo MANY_MANY, el nombre de la tabla asociativa también debe ser especificado en la clave foránea. Por ejemplo, la relación categories en Post está especificada con la clave foránea PostCategory(postID, categoryID).

La declaración de relaciones en las clases AR implicitamente agrega una propiedad a la clase por cada relación. Luego de que una consulta relacional es ejecutada, la correspondiente propiedad será rellenada con la(s) instancia(s) AR relacionada(s). Por ejemplo, si $author representa una instancia AR User, podemos usar $author->posts para acceder a sus instancias Post relacionadas.

2. Ejecutando Consultas Relacionales

La manera más sencilla de ejecutar consultas relacionales es leer una propiedad relacional en una instancia AR. Si la propiedad no fue accedida previamente, será iniciada una consulta relacional, la cual unirá las dos tablas relacionadas y las filtrará con la clave primaria de la instancia AR actual. El resultado de la consulta será guardado en la propiedad como instancia(s) de la clase AR relacionada. Esto se conoce como enfoque lazy loading, es decir, la consulta relacional es ejecutada sólo cuando los objetos relacionados son accedidos por primera vez. El ejemplo siguiente muestra como usar este enfoque:

// recuperar el post cuyo ID es 10
$post=Post::model()->findByPk(10);
// recuperar el autor del post: una consulta relacional se ejecutará aquí
$author=$post->author;

Información: Si no hay una instancia relacionada para la relación, la correspondiente propiedad podría ser null o un arreglo vacío. Para las relaciones BELONGS_TO y HAS_ONE, el resultado es null; para las relaciones HAS_MANY y MANY_MANY, el resultado es un arreglo vacío.

El enfoque lazy loading es muy conveniente de usar, pero no es eficiente en algunos escenarios. Por ejemplo, si queremos acceder a la información del autor para N posts, usar el enfoque lazy loading podría involucrar ejecutar N consultas de unión. Bajo estas circunstancias debemos recurrir al enfoque llamado eager loading.

El enfoque eager loading recupera las instancias AR relacionadas junto con la(s) instancia(s) AR principal(es). Esto se logra mediante el uso del método with() junto con uno de los métodos find o findAll de AR. Por ejemplo,

$posts=Post::model()->with('author')->findAll();

El código anterior devolverá un arreglo de instancias Post. A diferencia del enfoque lazy loading, la propiedad author en cada Post ya está rellenada con la instancia User relacionada antes de acceder a la propiedad. En vez de ejecutar una consulta de join por cada post, el enfoque eager loading traerá todos los posts juntos con sus autores en una sola consulta de union!

Podemos especificar multiples nombres de relación en el método with() y el enfoque eager loading los traerá de una sola vez. Por ejemplo, el siguiente código traerá los posts juntos con sus autores y sus categorías:

$posts=Post::model()->with('author','categories')->findAll();

Podemos también anidar los eager loading. En vez de una lista de nombres de relación, pasamos una representación jerárquica de nombres de relación al método with(), como la siguiente,

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

Lo anterior nos traerá todos los posts con sus autores y categorías. También traerá cada perfil de autor y sus posts.

Nota: El uso del método with() ha sido cambiado desde la versión 1.0.2. Por favor lee la correspondiente documentación de la API cuidadosamente.

LA implementación de AR en Yii es muy eficiente. Cuando se usa eager loading en una jerarquía de objetos relacionados que involucran N relaciones HAS_MANY o MANY_MANY, tomará N+1 consultas SQL para obtener el resultado necesario. Esto significa que en el ejemplo anterior necesita tres consultas SQL debido a las propiedades posts y categories. Otros frameworks toman un enfoque más radical usando solo una consulta SQL. A primera vista el enfoque radical parece ser más eficiente porque menos consultas son analizadas y ejecutadas por el DBMS. Esto es de hecho poco práctico en la realidad por dos razones: Primero, existen muchos datos repetitivos en el resultado que toman tiempo extra para transmitir y procesar; Segundo, el número de filas en el resultado crece exponencialmente con el número de tablas involucradas, lo que las hace simplemente inmanejable cuanto más relaciones estén involucradas.

Desde la versión 1.0.2, podemos además obligar a la consulta relacional a ser hecha con sólo con una consulta SQL. Simplemente agregamos una llamada a together() luego de with(). Por ejemplo,

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->together()->findAll();

La consulta anterior será hecha en una sola consulta SQL. Sin llamar a together, serán necesarias dos consultas SQL: una junta las tablas Post, User y Profile, y la otra junta las tablas User y Post.

3. Opciones de la Consulta Relacional

Mencionamos que las opciones adicionales pueden ser especificadas en la declaración de la relación. Estas opciones, especificadas como pares nombre-valor, son usadas para personalizar la consulta relacional.

We mentioned that additional options can be specified in relationship declaration. These options, specified as name-value pairs, are used to customize the relational query. Se resumen a continuación:

  • select: una lista de columnas a ser seleccionadad para la clase AR relacionada. Por defecto es '*', lo que significa todas las columnas. Los nombres de las columnas deben ser desambiguados usando aliasToken si aparecen en una expresión (ej.: COUNT(??.name) AS nameCount).

  • condition: la cláusula WHERE. Por defecto vacía. Notar que las referencias a las columnas deber ser desambiguadas usando aliasToken (ej.: ??.id=10).

  • params: los parámetros a ser enlazados en la sentencia SQL generada. Éstos deben ser dados como un arreglo de pares nombre-valor. Esta opción está disponible desde la versión 1.0.3.

  • on: la cláusula ON. La condición especificada aquí será agregada a la condición de union (del join) usando el operador AND. Esta opción está disponible desde la versión 1.0.2.

  • order: la cláusula ORDER BY. Por defecto vacía. Notar que las referencias a las columnas deber ser desambiguadas usando aliasToken (ej.: ??.age DESC).

  • with: una lisata de objetos relacionados que deben ser cargados junto con este objeto. Note, this is only honored by lazy loading, not eager loading.

  • joinType: tipo de union (join) para esta relación. Por defecto es LEFT OUTER JOIN.

  • aliasToken: el marcador profijo de columna. Será reemplazado por el alias de la tabla para desambiguar las referencias a las columnas. Por defecto es '??.'.

  • alias: el alias de la tabla asociada con esta relación. Esta opcion está disponible desde la versión 1.0.1. Por defecto en null, lo que significa que el alias de la tabla es generado automáticamente. Difiere de aliasToken en que aliasToken es sólo un marcador de posición y será reemplazado por el alias real de la tabla.

  • together: si la tabla asociada con esta relación debe ser forzada a unirse junto con la tabla primaria. Esta opcion sólo tiene sentido para relaciones HAS_MANY y MANY_MANY. Si esta opción no esta establecida en falso, cada relación HAS_MANY o MANY_MANY terndra su propia sentencia JOIN para mejorar el desempeño. Esta opción está disponible desde la versión 1.0.3

Además, las siguientes opciones están disponibles para ciertas relación durante el lazy loading:

  • group: la cláusula GROUP BY. Por defecto vacía. Notar que las referencias a las columnas deben ser desambiguadas usando aliasToken (ej.: ??.age). Esta opción solo se aplica a relaciones HAS_MANY y MANY_MANY.

  • having: la cláusula HAVING. Por defecto vacía. Notar que las referencias a las columnas deben ser desambiguadas usando aliasToken (ej.: ??.age). Esta opción solo se aplica a relaciones HAS_MANY y MANY_MANY. Nota: esta opción está disponible desde la versión 1.0.1.

  • limit: el límite de las filas a ser seleccionadas. Esta opción NO se aplica a la relación BELONGS_TO.

  • offset: desplazamiento de las filas a ser seleccionadas. Esta opción NO se aplica a la relación BELONGS_TO.

A continuación modificamos la declaración de la relación posts en User incluyendo algunas de las opción anteriores:

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID'
                            'order'=>'??.createTime DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

Ahora si accedemos a $author->posts, obtendremos los posts del autor ordenados de acuerdo a su hora de creación en orden descendiente. Cada instancia post también tiene cargadas sus categorías.

Información: Cuando un nombre de columna aparece en dos o más tablas a ser unidas, es necesario desambiguarlas. Esto se hace poniendo como prefijo el nombre de la tabla al nombre de la columa. Por ejemplo, id se vuelve Team.id. En las consultas relacionales de AR, sin embargo, no tenemos esta libertad puesto que las sentencias SQL son generadas automáticamente por AR, que, sistemáticamente, le da a cada tabla un alias. Por lo tanto, para evitar conflictos con los nombres de columnas, debemos usar un marcador de posición para indicar la existencia de una columna que necesita ser desambiguada. AR reemplazará el marcador con un alias de tabla adecuado y desambiguar la columna correctamente.

4. Opciones Dinámicas de Consultas Relacionales

Comenzando desde la versión 1.0.2, podemos usar opciones dinámicas de consultas relacionales tanto en with() como en la opcion with. Las opciónes dinámicas sobreescribirán las opciones existentes como se especifica en el método relations(). Por ejemplo, con el anterior modelo User, si queremos usar el enfoque eager loading para traer de vuelta los posts pertenecientes al autor en órden ascendiente (la opción order en la especificación de la relación es órden descendiente), podemos hacer lo siguiente:

User::model()->with(array(
    'posts'=>array('order'=>'??.createTime ASC'),
    'profile',
))->findAll();