Metodos de busqueda

Data Hydrators con Symfony 1.4 y Doctrine

Rating: 5.0/5 (2 votes cast)
Muchas veces busqué alguna buena documentación sobre los Data Hydrators de Doctrine más como ejemplos que simples explicaciones y luego de mucha pelea logré entenderlos bien como para trabajar a gusto con ellos por lo que me gustaría dejarlo por escrito por si pueda serles de utilidad.

Me gustaría empezar con el concepto de que son los Data Hydrators haciendo referencia a la documentación oficial de Doctrine en su sitio web.

Doctrine has a concept of data hydrators for transforming your Doctrine_Query instances to a set of PHP data for the user to take advantage of. The most obvious way to hydrate the data is to put it into your object graph and return models/class instances. Sometimes though you want to hydrate the data to an array, use no hydration or return a single scalar value.

Yo lo explicaría diciendo que Doctrine utiliza Data Hydrators para la transformación de los Doctrine_Query que usamos al momento de hacer nuestros DQLs. Es decir, un DQL generado por nosotros nos sirve para generar dinámicamente el SQL necesario para ejecutarlo contra la base de datos y nos devuelve de alguna manera información de la base de datos que por lo general lo hubiésemos denominado un ResultSet. Estos datos devueltos vienen en un formato que Doctrine maneja y los Data Hydrators nos permiten decirle que nos devuelva de cierta manera que podamos manipularlos más fácilmente. A este proceso de transformación se le denomina “hidratar los datos” y nos sirve para manipularlos como objetos, arrays o como un valor único a lo que se le denomina valor escalar.

Para los ejemplos podríamos tener las siguientes tablas:

»config/doctrine/schema.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Persona:
columns:
id: { type: integer, notnull: true, primary: true, autoincrement: true }
nombre: { type: string(100), notnull: true }
apellido: { type: string(100), notnull: true }
usuario: { type: string(20), notnull: true }
clave: { type: string(100), notnull: true }
pais_id: { type: integer, notnull: true }
relations:
Pais:
foreignAlias: Persona

Pais:
columns:
id: { type: integer, notnull: true, primary: true, autoincrement: true }
nombre: { type: string(100), notnull: true }
Por lo general dentro de un modelo, por ejemplo PersonaTable.class.php, creamos métodos con los DQLs necesarios para un SELECT a la base de datos.

»lib/model/PersonaTable.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PersonaTable extends Doctrine_Table {
public static function getInstance() {
return Doctrine_Core::getTable('Persona');
}

public function getPersonasByUsuario($usuario)
{
$ret = Doctrine_Query::create()
->from('Persona p')
->where('p.usuario = ?', $usuario)
->execute();
return $ret;
}
}
Podemos ver en las líneas 8 a la 16 un método público que genera un DQL para traer todas las columnas de la tabla persona cuando el usuario sea igual al que viene por argumento. Este DQL sería traducido al siguiente SQL:

1
2
3
4
SELECT p.id AS p__id, p.nombre AS p__nombre, p.apellido AS p__apellido,
p.usuario AS p__usuario, p.clave AS p__clave, p.pais_id AS p__pais_id
FROM persona p
WHERE (p.usuario = 'jperez')
Existen cuatro tipos básicos de Data Hydrators:

Record: Devuelve los datos como un array de objetos
Array: Devuelve los datos como un array de arrays multidimensionales donde cada Foreing Key que este contenida dentro de nuestro query será nuevamente un array
Scalar: Devuelve los datos como un array plano. Este sería el más conocido a nivel de conexiones básicas con PHP por ejemplo usando mysql_query y mysql_fetch_array
Single Scalar: Este Data Hydrator nos sirve para obtener solo un dato por ejemplo si hacemos un select nombre from persona where id = 1 o un simple select now()
Empecemos a verlos uno por uno.

Data Hydrator: Record

Con el ejemplo visto más arriba se puede notar que hicimos un simple DQL y no nos importó el Data Hydrator. Esto es porque por default el Data Hydrator asignado será el Record. Esto también lo podemos escribir explícitamente escribiendo:

»lib/model/PersonaTable.class.php

1
2
3
4
5
6
7
8
9
10
public function getPersonasByUsuario($usuario) {
$ret = Doctrine_Query::create()
->from('Persona p')
->where('p.usuario = ?', $usuario)
->setHydrationMode(Doctrine::HYDRATE_RECORD)
->execute();

return $ret;
}
Como podemos ver en la línea 6, tenemos un método del Doctrine_Query llamado ->setHydrationMode() que nos permite enviar valores por cada uno. Para hacerlo más sencillo Doctrine ya nos prepara una constantes de la cuales la primera es Doctrine::HYDRATE_RECORD para asignar el tipo de hidratación Record.

Agregando entonces este método como lo vemos en el ejemplo, o dejándolo sin nada, doctrine nos devuelve los datos como un array de objetos que podríamos invocarlo en un action de esta manera para luego iterarlo:

»apps/frontend/modules/tests/actions/actions.class.php

1
2
3
4
5
6
7
8
9
public function executeIndex(sfWebRequest $request) {
$personas = PersonaTable::getInstance()->getPersonasByUsuario('jperez');

foreach($personas as $persona)
{
$this->logMessage($persona->getNombre());
}
}
Fijense como obtenemos los datos como si fuera un objeto utilizando los getters: getId(), getNombre(), getUsuario(), getPaisId(), etc.

El punto fuerte de este Data Hydrator es que si se fijan tenemos una columna pais_id por lo que si queremos obtener el nombre del país podríamos hacer uso de los getters para que Doctrine haga una consulta nueva y obtenga los datos del país de la siguiente manera:

1
2
3
//-- Fijense que uso el getter getPais() y no getPaisId()
// ya que tiene que obtener el país completo
$pais = $persona->getPais()->getNombre();
Doctrine se da cuenta que queremos acceder a un dato de la FK por lo que cuando ejecutamos getPais() vuelve a enviar un select * from pais where id = X y obtiene los datos del país correspondiente a la persona.

Hay que notar que aunque esta funcionalidad es muy interesante se ejecutan 2 consultas a la base de datos. Una para obtener los datos de la persona y otra para los datos del país. Lo cual sería mucho más útil resolverlo con un JOIN modificando nuestro DQL y de esta manera cuando ejecutemos getPais()->getNombre() Doctrine ya tendrá el dato cargado y no tendrá la necesidad de ejecutar una segundo consulta.

El DQL que podríamos usar para hacer el JOIN sería el siguiente:

»lib/model/PersonaTable.class.php

1
2
3
4
5
6
7
8
9
10
11
public function getPersonasByUsuario($usuario) {
$ret = Doctrine_Query::create()
->from('Persona p')
->where('p.usuario = ?', $usuario)
->innerJoin('p.Pais pa')
->setHydrationMode(Doctrine::HYDRATE_RECORD)
->execute();

return $ret;
}
Data Hydrator: Array

Este Data Hydrator me resultó un poco raro hasta que lo entendí bien y es importante entenderlo para no mezclarlo con la idea que siempre tenemos de obtener los registros de la base de datos como arrays planos.

Para obtener este Data Hydrator modificamos el método correspondiente en nuestro DQL:

»lib/model/PersonaTable.class.php

1
2
3
4
5
6
7
8
9
10
public function getPersonasByUsuario($usuario) {
$ret = Doctrine_Query::create()
->from('Persona p')
->where('p.usuario = ?', $usuario)
->setHydrationMode(Doctrine::HYDRATE_ARRAY)
->execute();

return $ret;
}
También podríamos obtenerlo usando el método ->fetchArray() en lugar de ->execute().

»lib/model/PersonaTable.class.php

1
2
3
4
5
6
7
8
9
public function getPersonasByUsuario($usuario) {
$ret = Doctrine_Query::create()
->from('Persona p')
->where('p.usuario = ?', $usuario)
->fetchArray();

return $ret;
}
Con esto obtenemos un array un poco complicado que intenta simular un array de objetos con un array de arrays asociativos. Para ejemplificar un poco más muestro la estructura del array que obtenemos con la consulta.

1
2
3
4
5
6
7
8
9
10
11
Array
(
0 => array(
'id' => '1',
'nombre' => 'Juan',
'apellido' => 'Perez',
'usuario' => 'jperez',
'clave' => '123456',
'pais_id' => '1',
)
)
En este caso como obtenemos un solo usuario entonces solo tenemos la posición cero que contiene un array con los datos necesarios. Hasta aquí es normal y sin son varios simplemente hacemos un bucle sobre el array principal y obtenemos los datos:

1
2
3
4
5
6
foreach($personas as $persona) {
echo $persona['nombre'];
echo $persona['usuario'];
echo $persona['pais_id'];
}
El problema que yo encuentro es cuando dentro del DQL empezamos a utilizar JOINs por ejemplo para obtener el nombre del Pais obtendríamos una posición Pais que hace referencia a la tabla foránea y dentro nuevamente un array con los datos del país:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Array
(
0 => array(
'id' => '1',
'nombre' => 'Juan',
'apellido' => 'Perez',
'usuario' => 'jperez',
'clave' => '123456',
'pais_id' => '1',
'Pais' => array(
'id' => '1',
'nombre' => 'Brasil'
),
)
)
Al querer obtener los datos sería algo así:

1
2
3
4
5
6
foreach($personas as $persona) {
echo $persona['nombre'];
echo $persona['usuario'];
echo $persona['Pais']['nombre'];
}
Como lo ven es una forma de simular la orientación de objetos con Arrays. Lo único que tienen que saber es que si la tabla País tiene una FK a Ciudades por ejemplo tendríamos nuevamente otro Array y para obtener el nombre de la ciudad seria $persona['Pais']['Ciudad']['nombre'].

La desventaja aquí es que si no traje el nombre del País en el select de mi DQL, utilizando el innerJoin, ya no podré hacer uso de ->getPais()->getNombre() y Doctrine no hará un select por separado para tratar de obtener el dato, esto solo ocurre con el Data Hydrator: Record pero el array devuelto por doctrine es mucho menor que cuando usamos el tipo Record justamente por no tener estas posibilidades.

Data Hydrator: Scalar

El tipo Scalar es muy parecido al tipo Array con la diferencia que no simula la orientación a objetos sino que simplemente nos devuelve un array plano utilizando para el nombre de los índices el nombre de la columna con el prefijo del alias utilizado para la tabla, por lo que para el siguiente DQL tendríamos el siguiente Array:

»lib/model/PersonaTable.class.php

1
2
3
4
5
6
7
8
9
10
public function getPersonasByUsuario($usuario) {
$ret = Doctrine_Query::create()
->from('Persona p')
->where('p.usuario = ?', $usuario)
->setHydrationMode(Doctrine::HYDRATE_SCALAR)
->execute();

return $ret;
}
1
2
3
4
5
6
7
8
9
10
11
Array
(
0 => array(
'p_id' => '1',
'p_nombre' => 'Juan',
'p_apellido' => 'Perez',
'p_usuario' => 'jperez',
'p_clave' => '123456',
'p_pais_id' => '1',
)
)
Hasta aquí la única diferencia es que se le agrega “p_” al nombre de las columnas porque elegí el alias “p” al hacer ->from(‘Persona p’). La gran diferencia se nota al utilizar los JOINs donde, utilizando el código con el JOIN que utiliza el alias “pa” para el país obtendríamos un Array plano también como el anterior:

1
2
3
4
5
6
7
8
9
10
11
12
13
Array
(
0 => array(
'p_id' => '1',
'p_nombre' => 'Juan',
'p_apellido' => 'Perez',
'p_usuario' => 'jperez',
'p_clave' => '123456',
'p_pais_id' => 1,
'pa_id' => '1',
'pa_nombre' => 'Brasil',
)
)
Este tipo de Arrays suele ser util para funciones genéricas como por ejemplo un helper que imprima una tabla a partir de un array.

Data Hydrator: Single Scalar

Este tipo de Data Hydrator es muy sencillo. Simplemente se utiliza cuando lo que yo quiero obtener de la base de datos es “un solo dato y nada más que eso“. En este caso no obtendríamos un array ni de objetos ni de arrays sino “un solo dato“.

Podríamos usarlo para el siguiente DQL con la idea de obtener el nombre de usuario de un determinado ID:

1
2
3
4
5
6
7
8
9
10
11
public function getUsuarioById($id) {
$usuario = Doctrine_Query::create()
->select('p.usuario')
->from('Persona p')
->where('p.id = ?', $id)
->setHydrationMode(Doctrine::HYDRATE_SINGLE_SCALAR)
->execute();

return $usuario;
}
De esta forma lo que se obtiene es simplemente el nombre del usuario y podríamos utilizarlo en el action de la siguiente manera:

»apps/frontend/modules/tests/actions/actions.class.php

1
2
3
4
5
6
public function executeIndex(sfWebRequest $request) {
$usuario = PersonaTable::getInstance()->getUsuarioById(1);

$this->logMessage($usuario);
}
Resumen Final

Haciendo un resumen final podríamos decir que:

Record: Array de objetos
Array: Array de Arrays multidimensionales. Simulan orientación a objetos con Arrays asociativos
Scalar: Array de Arrays planos
Single Scalar: Un solo dato
También podríamos decir que la funcionalidad de obtener datos que no fueron seleccionados en nuestro DQL al no haber hecho los JOINs solo lo tiene el tipo Record a través de los getters ya que si Doctrine lo detecta como un datos nulo hará automáticamente un select nuevamente a la base de datos. Los tipos Array, Scalar y Single Scalar no poseen esta funcionalidad por no tratarse de objetos.

También es bueno saber que el Record justamente por contar con la funcionalidad mencionada arriba ocupan más lugar en memoria por lo que yo suelo pensar que la mejor manera es preveer bien los campos que quiero seleccionar y manejarlo como un Array.

Espero que les haya servido. Hasta la próxima.

Data Hydrators con Symfony 1.4 y Doctrine, 5.0 out of 5 based on 2 ratings

Redmine Appliance - Powered by TurnKey Linux