Data Hydrators con Symfony 1.4 y Doctrine

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:

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.

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:

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:

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:

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:

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

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:

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().

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.

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:

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:

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í:

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:

public function getPersonasByUsuario($usuario)
{
    $ret = Doctrine_Query::create()
            ->from('Persona p')
            ->where('p.usuario = ?', $usuario)
            ->setHydrationMode(Doctrine::HYDRATE_SCALAR)
            ->execute();
 
    return $ret;
}

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:

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:

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:

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.

15 comentarios en “Data Hydrators con Symfony 1.4 y Doctrine”

  1. right hit the nail, I have worked on a blog post here part I http://www.craftitonline.com about what hydration means, but your post is far better, however it is for an out dated version of doctrine and type of hydration. I was just wonder if you have or we could work together in an udpated version for current doctrine that I am preparing on my blog. What you think?

  2. Juan, además de un excelente aporte, te felicito por la excelencia de tu explicación. En lo personal, me dedico a la formación y debo decirte que estoy encantado con esta nota, es un placer leerte.
    Un abrazo de un colega argentino.
    Saludos,
    Fernando.

  3. Gracias por tu explicación, esta muy digerible la forma de explicar.
    Te agradecería si conoces y publicas algo acerca de cuál de las 4 formas es más rápida en procesar los datos que vienen de la base de datos, para ganar aún más en rendimiento (por supuesto que no debe ser el Record)

    Saludos.

    1. Gracias el comentario Ernesto.

      Entre los cuatro a mi me gusta más el Scalar aunque la verdad que para ganar rapidez lo mejor es NO realizar este proceso, es decir diciéndole que tal cual como trae el resultset de la base de datos te lo devuelva, al muy puro estilo de la extensión nativa de php y ahí ya te encargas de recorrerlo.

      José Antonio Pio (@josetonyp) explica esto mismo y muy bien en la conferencia de Desymfony 2011. Puedes ver el video y la presentación aquí.

      Espero que te haya servido. Saludos

  4. Muy bueno el articulo. no sabia mucho sobre doctrine y sus metodos de hidratacion. la duda mia esta en como pudiera hidratar una consulta realizada sin utilizar dql. me explico mejor, tengo una variable que contiene en una cadena la consulta y la ejecuto con un doctrine_manager (no recuerdo bien), ese resultado como lo pudiera hidratar?? muchas gracias de antemano.

    1. Gracias por el comentar pepe.

      La verdad que nunca me puse a investigar como hidratar un native query porque la verdad que por lo general lo uso sin hidratar, es decir, directamente lo recorro. En Java con Hibernate si lo llegue a hacer pero sería cuestión de investigar como hacerlo con Doctrine.

      Por lo general lo uso sin hidratar porque tiene mejor performance como se lo expliqué a Ernesto en el comentario anterior dándole el link de una conferencia muy interesante que habla sobre esto.

Comenta este artículo