Symfony 1.4, la guía definitiva

8.4. Acceso a los datos

En Symfony, el acceso a los datos se realiza mediante objetos. Si se está acostumbrado al modelo relacional y a utilizar consultas SQL para acceder y modificar los datos, los métodos del modelo de objetos pueden parecer complicados. No obstante, una vez que se prueba el poder de la orientación a objetos para acceder a los datos, probablemente te gustará mucho más.

En primer lugar, hay que asegurarse de que se utiliza el mismo vocabulario. Aunque el modelo relacional y el modelo de objetos utilizan conceptos similares, cada uno tiene su propia nomenclatura:

Relacional Orientado a objetos
Tabla Clase
Fila, registro Objeto
Campo, columna Propiedad

8.4.1. Obtener el valor de una columna

Cuando Symfony construye el modelo, crea una clase de objeto base para cada uno de los modelos definidos en schema.yml. Estas clases contienen inicialmente una serie de métodos getters y setters apropiados para cada tipo de columna. Estos métodos new, getXXX() y setXXX() permiten crear los objetos y acceder a sus propiedades, como se muestra en el listado 8-7.

Listado 8-7 - Métodos generados en una clase objeto

$articulo = new Articulo();
$articulo->setTitulo('Mi primer artículo');
$articulo->setContenido("Este es mi primer artículo. \n Espero que te guste.");

$titulo    = $articulo->getTitulo();
$contenido = $articulo->getContenido();

Nota La clase objeto generada se llama Articulo, pero en la base de datos se almacena en una tabla llamada blog_article. Si no se hubiera definido la propiedad tableName, la tabla se habría llamado articulo. Los métodos getXXX() y setXXX() aplican la técnica camelCase al nombre e las columnas, por lo que el método getTitulo() obtiene el valor de la columna titulo y el método getCodigoPostal() obtendría el valor de una columna llamada codigo_postal.

Para establecer el valor de varios campos a la vez, se puede utilizar el método fromArray(), que incluyen todos los objetos clase, como se muestra en el listado 8-8.

Listado 8-8 - El método fromArray() es un setter múltiple

$articulo->fromArray(array(
  'Titulo'    => 'Mi primer artículo',
  'Contenido' => 'Este es mi primer artículo. \n Espero que te guste.'
));

8.4.2. Obtener los registros relacionados

La columna articulo_id de la tabla blog_comentario define implícitamente una clave externa a la tabla blog_articulo. Cada comentario está relacionado con un artículo y un artículo puede tener muchos comentarios. Las clases generadas contienen 5 métodos que traducen esta relación a la forma orientada a objetos, de la siguiente manera:

  • $comentario->getArticulo(): para obtener el objeto Articulo relacionado
  • $comentario->getArticuloId(): para obtener el id del objeto Articulo relacionado
  • $comentario->setArticulo($articulo): para definir el objeto Articulo relacionado
  • $comentario->setArticuloId($id): para definir el objeto Articulo relacionado a partir de un id
  • $articulo->getComentarios(): para obtener los objetos Comentario relacionados

Los métodos getArticuloId() y setArticuloId() demuestran que se puede utilizar la columna articulo_id como una columna normal y que se pueden indicar las relaciones manualmente, pero esto no es muy interesante. La ventaja de la forma orientada a objetos es mucho más evidente en los otros tres métodos. El listado 8-9 muestra cómo utilizar los setters generados.

Listado 8-9 - Las claves externas se traducen en un setter especial

$comentario = new Comentario();
$comentario->setAutor('Steve');
$comentario->setContenido('¡Es el mejor artículo que he leído nunca!');

// Añadir este comentario al anterior objeto $articulo
$comentario->setArticulo($articulo);

// Sintaxis alternativa
// Solo es correcta cuando el objeto artículo ya
// ha sido guardado anteriormente en la base de datos
$comentario->setArticuloId($articulo->getId());

El listado 8-10 muestra como utilizar los getters generados automáticamente. También muestra como encadenar varias llamadas a métodos en los objetos del modelo.

Listado 8-10 - Las claves externas se traducen en getters especiales

// Relación de "muchos a uno"
echo $comentario->getArticulo()->getTitulo();
 => Mi primer artículo
echo $comentario->getArticulo()->getContenido();
 => Este es mi primer artículo.
    Espero que te guste.

// Relación "uno a muchos"
$comentarios = $articulo->getComentarios();

El método getArticulo() devuelve un objeto de tipo Articulo, que permite utilizar el método accesor getTitulo(). Se trata de una alternativa mucho mejor que realizar la unión de las tablas manualmente, ya que esto último necesitaría varias líneas de código (empezando con la llamada al método $comment->getArticuloId()).

La variable $comentarios del listado 8-10 contiene un array de objetos de tipo Comentario. Se puede mostrar el primer comentario mediante $comentarios[0] o se puede recorrer la colección entera mediante foreach ($comentarios as $comentario).

8.4.3. Guardar y borrar datos

Al utilizar el constructor new se crea un nuevo objeto, pero no un registro en la tabla blog_articulo. Si se modifica el objeto, tampoco se reflejan esos cambios en la base de datos. Para guardar los datos en la base de datos, se debe invocar el método save() del objeto.

$articulo->save();

El ORM de Symfony es lo bastante inteligente como para detectar las relaciones entre objetos, por lo que al guardar el objeto $articulo también se guarda el objeto $comentario relacionado. También detecta si ya existía el objeto en la base de datos, por lo que el método save() a veces se traduce a una sentencia INSERT de SQL y otras veces se traduce a una sentencia UPDATE. La clave primaria se establece de forma automática al llamar al método save(), por lo que después de guardado, se puede obtener la nueva clave primaria del objeto mediante $articulo->getId().

Nota Para determinar si un objeto es completamente nuevo, puedes utilizar el método isNew(). Para detectar si un objeto ha sido modificado y por tanto se debe guardar en la base de datos, utiliza el método isModified().

Si lees los comentarios que insertan los usuarios en tus artículos, puede que te desanimes un poco para seguir publicando cosas en Internet. Si además no captas la ironía de los comentarios, puedes borrarlos fácilmente con el método delete(), como se muestra en el listado 8-11.

Listado 8-11 - Borrar registros de la base de datos mediante el método delete() del objeto relacionado

foreach ($articulo->getComentarios() as $comentario)
{
  $comentario->delete();
}

8.4.4. Obtener registros mediante la clave primaria

Si se conoce la clave primaria de un registro concreto, puedes emplear el método find() de la clase tabla para obtener el objeto relacionado.

$articulo = Doctrine_Core::getTable('Articulo')->find(7);

El archivo schema.yml define el campo id como clave primaria de la tabla blog_articulo, por lo que la sentencia anterior obtiene el artículo cuyo id sea igual a 7. Como se ha utilizado una clave primaria, sólo se obtiene un registro; la variable $articulo contiene un objeto de tipo Articulo.

En algunos casos, la clave primaria está formada por más de una columna, por lo que el método find() permite indicar varios parámetros, uno para cada columna de la clave primaria.

8.4.5. Obtener registros mediante Doctrine_Query

Cuando se quiere obtener más de un registro, se debe utilizar el método createQuery() de la clase table correspondiente a los objetos que se quieren obtener. Por ejemplo, para obtener objetos de la clase Articulo, se llama al método Doctrine_Core::getTable('Articulo')->createQuery()->execute().

El primer parámetro del método execute() es un array que contiene todos los valores que se utilizan para completar la consulta que se ejecuta.

Un metodo Doctrine_Query vacío devuelve todos los objetos de la clase. El código del listado 8-12 obtiene por ejemplo todos los artículos de la base de datos.

Listado 8-12 - Obtener registros mediante Doctrine_Query con el método createQuery() (consulta vacía)

$q = Doctrine_Core::getTable('Articulo')->createQuery();
$articulos = $q->execute();

El código anterior genera la siguiente consulta SQL:

SELECT b.id AS b__id, b.titulo AS b__titulo, b.contenido AS b__contenido, b.created_at AS b__created_at, b.updated_at AS b__updated_at FROM blog_articulo b

Para las selecciones más complejas de objetos, se necesitan equivalentes a las sentencias WHERE, ORDER BY, GROUP BY y demás de SQL. El objeto Doctrine_Query dispone de métodos y parámetros para indicar todas estas condiciones. Si quieres obtener por ejemplo todos los comentarios escritos por el usuario Steve ordenados por fecha, puedes utilizar un objeto Doctrine_Query como el del listado 8-13.

Listado 8-13 - Obtener registros mediante Doctrine_Query con el método createQuery() (Doctrine_Query con condiciones)

$q = Doctrine_Core::getTable('Comentario')
  ->createQuery('c')
  ->where('c.autor = ?', 'Steve')
  ->orderBy('c.created_at ASC');
$comentarios = $q->execute();

El código anterior genera la siguiente consulta SQL:

SELECT b.id AS b__id, b.articulo_id AS b__articulo_id, b.autor AS b__autor, b.contenido AS b__contenido, b.created_at AS b__created_at, b.updated_at AS b__updated_at FROM blog_comentario b WHERE (b.autor = ?) ORDER BY b.created_at ASC

La tabla 8-1 compara la sintaxis de SQL y del objeto Doctrine_Query.

Tabla 8-1 - Sintaxis de SQL y del objeto Doctrine_Query

SQL Doctrine_Query
WHERE columna = valor ->where('columna = ?', 'valor');
ORDER BY columna ASC ->orderBy('columna ASC');
ORDER BY columna DESC ->addOrderBy('columna DESC');
LIMIT limite ->limit(limite)
OFFSET desplazamiento ->offset(desplazamiento)
FROM tabla1 LEFT JOIN tabla2 ON tabla1.col1 = tabla2.col2 ->leftJoin('a.Modelo2 m')
FROM tabla1 INNER JOIN tabla2 ON tabla1.col1 = tabla2.col2 ->innerJoin('a.Modelo2 m')

El listado 8-14 muestra otro ejemplo del uso de Doctrine_Query con condiciones múltiples. En el ejemplo se obtienen todos los comentarios del usuario Steve en los artículos que contienen la palabra enjoy y además, ordenados por fecha.

Listado 8-14 - Otro ejemplo para obtener registros mediante Doctrine_Query con el método createQuery() (Doctrine_Query con condiciones)

$q = Doctrine_Core::getTable('Comentario')
  ->createQuery('c')
  ->where('c.autor = ?', 'Steve')
  ->leftJoin('c.Articulo a')
  ->andWhere('a.contenido LIKE ?', '%enjoy%')
  ->orderBy('c.created_at ASC');
$comentarios = $q->execute();

El código anterior genera la siguiente consulta SQL:

SELECT b.id AS b__id, b.articulo_id AS b__articulo_id, b.autor AS b__autor, b.contenido AS b__contenido, b.created_at AS b__created_at, b.updated_at AS b__updated_at, b2.id AS b2__id, b2.titulo AS b2__titulo, b2.contenido AS b2__contenido, b2.created_at AS b2__created_at, b2.updated_at AS b2__updated_at FROM blog_comentario b LEFT JOIN blog_articulo b2 ON b.articulo_id = b2.id WHERE (b.autor = ? AND b2.contenido LIKE ?) ORDER BY b.created_at ASC

De la misma forma que el lenguaje SQL es sencillo pero permite construir consultas muy complejas, el objeto Doctrine_Query permite manejar condiciones de cualquier nivel de complejidad. Sin embargo, como muchos programadores piensan primero en el código SQL y luego lo traducen a las condiciones de la lógica orientada a objetos, es difícil comprender bien el objeto Doctrine_Query cuando se utiliza las primeras veces. La mejor forma de aprender es mediante ejemplos y aplicaciones de prueba. El sitio web del proyecto Symfony está lleno de ejemplos de cómo construir objetos de tipo Doctrine_Query para todo tipo de situaciones.

Todas las instancias de Doctrine_Query disponen de un método count(), que simplemente cuenta el número de registros que satisfacen las condiciones de la consulta y devuelve ese número como un entero. Como no se devuelve ningún objeto, no se realiza el proceso de hidratación y por eso el método count() es mucho más rápido que execute().

Las clases tabla también incluyen los métodos findAll(), findBy*() y findOneBy*(), que en realidad son atajos para crear instancias de Doctrine_Query, ejecutarlas y devolver sus resultados.

Por último, si sólo quieres obtener el primer objeto devuelto por la consulta, reemplaza el método execute() por fetchOne(). Esto es muy útil cuando se sabe que las condiciones de Doctrine_Query solo van a devolver un resultado, y la ventaja es que el método devuelve directamente un objeto en vez de un array de objetos.

Nota Si una consulta execute() devuelve un gran número de resultados, puede que solo quieras mostrar unos pocos en la plantilla. Symfony incluye una clase especial para paginar resultados llamada sfDoctrinePager y que realiza la paginación de forma automática.

8.4.6. Uso de consultas con código SQL

A veces, no es necesario obtener los objetos, sino que solo son necesarios algunos datos calculados por la base de datos. Por ejemplo, para obtener la fecha de creación de todos los artículos, no tiene sentido obtener todos los artículos y después recorrer el array de los resultados. En este caso es preferible obtener directamente el resultado, ya que se evita el proceso de hydrating.

Por otra parte, no deberían utilizarse instrucciones PHP de acceso a la base de datos, porque se perderían las ventajas de la abstracción de bases de datos. Lo que significa que se debe evitar el ORM (Doctrine) pero no la abstracción de bases de datos (PDO).

Para realizar consultas a la base de datos con PDO, es necesario realizar los siguientes pasos:

  1. Obtener la conexión con la base de datos.
  2. Construir la consulta.
  3. Crear una sentencia con esa consulta.
  4. Iterar el result set que devuelve la ejecución de la sentencia.

Aunque lo anterior puede parecer un galimatías, el código del listado 8-15 es mucho más explícito.

Listado 8-15 - Consultas SQL personalizadas con PDO

$conexion = Doctrine_Manager::connection();
$consulta = 'SELECT MAX(created_at) AS max FROM blog_articulo';
$sentencia = $conexion->execute($consulta);
$sentencia->execute();
$resultset = $sentencia->fetch(PDO::FETCH_OBJ);
$max = $resultset->max;

Al igual que sucede con las selecciones realizadas con Doctrine, las consultas con PDO son un poco complicadas de usar al principio. Los ejemplos de las aplicaciones existentes y de los tutoriales pueden ser útiles para descubrir la forma de hacerlas.

Nota Si estás tentado de saltarte todo lo anterior para acceder de forma directa a la base de datos, corres el riesgo de perder la seguridad y la abstracción proporcionadas por Doctrine. Aunque con Doctrine es más largo de escribir, te obliga a hacer uso de las buenas prácticas que garantizan el buen rendimiento, la portabilidad y la seguridad de tu aplicación. Esta recomendación es especialmente útil para las consultas que contienen parámetros cuyo origen no es confiable, como por ejemplo cualquier dato proporcionado por un usuario de tu sitio. Doctrine se encarga de escapar todos los datos para mantener la seguridad de la base de datos. Si accedes directamente a la base de datos, corres el riesgo de sufrir ataques de tipo SQL-injection.

8.4.7. Uso de columnas especiales de fechas

Normalmente, cuando una tabla tiene una columna llamada created_at, se utiliza para almacenar un timestamp de la fecha de creación del registro. La misma idea se aplica a las columnas updated_at, cuyo valor se debe actualizar cada vez que se actualiza el propio registro.

La buena noticia es que Doctrine dispone de un comportamiento llamado Timestampable que se ocupa de actualizar su valor de forma automática. No es necesario establecer manualmente el valor de las columnas created_at y updated_at; se actualizan automáticamente, tal y como muestra el listado 8-16.

Listado 8-16 - Las columnas created_at y updated_at se gestionan automáticamente

$comentario = new Comentario();
$comentario->setAutor('Steve');
$comentario->save();

// Muestra la fecha de creación
echo $comentario->getCreatedAt();
 => [fecha de la operación INSERT de la base de datos]