Symfony 1.1, 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 una de las tablas definidas en schema.yml. Cada una de estas clases contiene una serie de constructores y accesores por defecto en función de la definición de cada columna: los métodos new, getXXX() y setXXX() permiten crear y obtener las propiedades de los objetos, 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, que es el valor de la propiedad phpName para la tabla blog_articulo. Si no se hubiera definido la propiedad phpName, la clase se habría llamado BlogArticulo. Los métodos accesores (get y set) utilizan una variante de camelCase aplicada al nombre de las columnas, por lo que el método getTitulo() obtiene el valor de la columna titulo.

Para establecer el valor de varios campos a la vez, se puede utilizar el método fromArray(), que también se genera para cada clase objeto, 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 3 métodos. El listado 8-9 muestra como 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).

Nota Los objetos del modelo se definen siguiendo la convención de utilizar nombres en singular, y ahora se puede entender la razón. La clave externa definida en la tabla blog_comentario crea el método getComentarios(), cuyo nombre se crea añadiendo una letra s al nombre del objeto Comentario. Si el nombre del modelo fuera plural, la generación automática llamaría getComentarioss() a ese método, lo cual no tiene mucho sentido.

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

Truco Para determinar si un objeto es completamente nuevo, se puede utilizar el método isNew(). Para detectar si un objeto ha sido modificado y por tanto se debe guardar en la base de datos, se puede utilizar 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();
}

Nota Después de ejecutar el método delete(), el objeto sigue disponible hasta que finaliza la ejecución de la petición actual. Para determinar si un objeto ha sido borrado de la base de datos, se puede utilizar el método isDeleted().

8.4.4. Obtener registros mediante la clave primaria

Si se conoce la clave primaria de un registro concreto, se puede utilizar el método retrieveByPk() de la clase peer para obtener el objeto relacionado.

$articulo = ArticuloPeer::retrieveByPk(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, solo 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. Es esos casos, el método retrieveByPK() permite indicar varios parámetros, uno para cada columna de la clave primaria.

También se pueden obtener varios objetos a la vez mediante sus claves primarias, invocando el método retrieveByPKs(), que espera como argumento un array de claves primarias.

8.4.5. Obtener registros mediante Criteria

Cuando se quiere obtener más de un registro, se debe utilizar el método doSelect() de la clase peer correspondiente a los objetos que se quieren obtener. Por ejemplo, para obtener objetos de la clase Articulo, se llama al método ArticuloPeer::doSelect().

El primer parámetro del método doSelect() es un objeto de la clase Criteria, que es una clase para definir consultas simples sin utilizar SQL, para conseguir la abstracción de base de datos.

Un objeto Criteria vacío devuelve todos los objetos de la clase. Por ejemplo, el código del listado 8-12 obtiene todos los artículos de la base de datos.

Listado 8-12 - Obtener registros mediante Criteria con el método doSelect() (Criteria vacío)

$c = new Criteria();
$articulos = ArticuloPeer::doSelect($c);

// Genera la siguiente consulta SQL
SELECT blog_articulo.ID, blog_articulo.TITLE, blog_articulo.CONTENIDO,
       blog_articulo.CREATED_AT
FROM   blog_articulo;

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 Criteria dispone de métodos y parámetros para indicar todas estas condiciones. Por ejemplo, para obtener todos los comentarios escritos por el usuario Steve y ordenados por fecha, se puede construir un objeto Criteria como el del listado 8-13.

Listado 8-13 - Obtener registros mediante Criteria con el método doSelect() (Criteria con condiciones)

$c = new Criteria();
$c->add(ComentarioPeer::AUTOR, 'Steve');
$c->addAscendingOrderByColumn(ComentarioPeer::CREATED_AT);
$comentarios = ComentarioPeer::doSelect($c);

// Genera la siguiente consulta SQL
SELECT blog_comentario.ARTICULO_ID, blog_comentario.AUTOR, blog_comentario.CONTENIDO,
       blog_comentario.CREATED_AT
FROM   blog_comentario
WHERE  blog_comentario.autor = 'Steve'
ORDER BY blog_comentario.CREATED_AT ASC;

Las constantes de clase que se pasan como parámetros a los métodos add() hacen referencia a los nombres de las propiedades. Su nombre se genera a partir del nombre de las columnas en mayúsculas. Por ejemplo, para indicar la columna contenido de la tabla blog_articulo, se utiliza la constante de clase llamada ArticuloPeer::CONTENIDO.

Nota ¿Por qué se utiliza ComentarioPeer::AUTOR en vez de blog_comentario.AUTOR, que es en definitiva el valor que se va a utilizar en la consulta SQL? Supon que se debe modificar el nombre del campo de la tabla y en vez de autor ahora se llama contributor. Si se hubiera utilizado el valor blog_comentario.AUTOR, es necesario modificar ese valor en cada llamada al modelo. Sin embargo, si se utiliza el valorComentarioPeer::AUTOR, solo es necesario cambiar el nombre de la columna en el archivo schema.yml, manteniendo el atributo phpName a un valor igual a AUTOR y reconstruir el modelo.

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

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

SQL Criteria
WHERE columna = valor ->add(columna, valor);
WHERE columna <> valor ->add(columna, valor, Criteria::NOT_EQUAL);
Otros operadores de comparación
> , < Criteria::GREATER_THAN, Criteria::LESS_THAN
>=, <= Criteria::GREATER_EQUAL, Criteria::LESS_EQUAL
IS NULL, IS NOT NULL Criteria::ISNULL, Criteria::ISNOTNULL
LIKE, ILIKE Criteria::LIKE, Criteria::ILIKE
IN, NOT IN Criteria::IN, Criteria::NOT_IN
Otras palabras clave de SQL
ORDER BY columna ASC ->addAscendingOrderByColumn(columna);
ORDER BY columna DESC ->addDescendingOrderByColumn(columna);
LIMIT limite ->setLimit(limite)
OFFSET desplazamiento ->setOffset(desplazamiento)
FROM tabla1, tabla2 WHERE tabla1.col1 = tabla2.col2 ->addJoin(col1, col2)
FROM tabla1 LEFT JOIN tabla2 ON tabla1.col1 = tabla2.col2 ->addJoin(col1, col2, Criteria::LEFT_JOIN)
FROM tabla1 RIGHT JOIN tabla2 ON tabla1.col1 = tabla2.col2 ->addJoin(col1, col2, Criteria::RIGHT_JOIN)

Truco La mejor forma de descubrir y entender los métodos disponibles en las clases generadas automáticamente es echar un vistazo a los archivos Base del directorio lib/model/om/. Los nombres de los métodos son bastante explícitos, aunque si se necesitan más comentarios sobre esos métodos, se puede establecer el parámetro propel.builder.addComentarios a true en el archivo de configuración config/propel.ini y después volver a reconstruir el modelo.

El listado 8-14 muestra otro ejemplo del uso de Criteria 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 Criteria con el método doSelect() (Criteria con condiciones)

$c = new Criteria();
$c->add(ComentarioPeer::AUTOR, 'Steve');
$c->addJoin(ComentarioPeer::ARTICULO_ID, ArticuloPeer::ID);
$c->add(ArticuloPeer::CONTENIDO, '%enjoy%', Criteria::LIKE);
$c->addAscendingOrderByColumn(ComentarioPeer::CREATED_AT);
$comentarios = ComentarioPeer::doSelect($c);

// Genera la siguiente consulta SQL
SELECT blog_comentario.ID, blog_comentario.ARTICULO_ID, blog_comentario.AUTOR,
       blog_comentario.CONTENIDO, blog_comentario.CREATED_AT
FROM   blog_comentario, blog_articulo
WHERE  blog_comentario.AUTOR = 'Steve'
       AND blog_articulo.CONTENIDO LIKE '%enjoy%'
       AND blog_comentario.ARTICULO_ID = blog_articulo.ID
ORDER BY blog_comentario.CREATED_AT ASC

De la misma forma que el lenguaje SQL es sencillo pero permite construir consultas muy complejas, el objeto Criteria permite manejar condiciones de cualquier nivel de complejodad. 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 Criteria cuando se utiliza las primeras veces. La mejor forma de aprender es mediante ejemplos y aplicaciones de prueba. El sitio web del proyecto Symfony esá lleno de ejemplos de cómo construir objetos de tipo Criteria para todo tipo de situaciones.

Además del método doSelect(), todas las clases peer tienen un método llamado doCount(), que simplemente cuenta el número de registros que satisfacen las condiciones pasadas como parámetro y devuelve ese número como un entero. Como no se devuelve ningún objeto, no se produce el proceso de hydrating y por tanto el método doCount() es mucho más rápido que doSelect().

Las clases peer también incluyen métodos doDelete(), doInsert() y doUpdate() (todos ellos requieren como parámetro un objeto de tipo Criteria). Estos métodos permiten realizar consultas de tipo DELETE, INSERT y UPDATE a la base de datos. Se pueden consultar las clases peer generadas automáticamente para descubrir más detalles de estos métodos de Propel.

Por último, si solo se quiere obtener el primer objeto, se puede reemplazar el método doSelect() por doSelectOne(). Es muy útil cuando se sabe que las condiciones de Criteria solo van a devolver un resultado, y su ventaja es que el método devuelve directamente un objeto en vez de un array de objetos.

Truco Cuando una consulta doSelect() devuelve un número muy grande de resultados, normalmente sólo se quieren mostrar unos pocos en la respuesta. Symfony incluye una clase especial para paginar resultados llamada sfPropelPager, que realiza la paginación de forma automática y cuya documentación y ejemplos de uso se puede encontrar en http://www.symfony-project.org/cookbook/1_1/en/pager

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 (Propel) pero no la abstracción de bases de datos (Creole).

Para realizar consultas a la base de datos con Creole, 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 Creole

$conexion = Propel::getConnection();
$consulta = 'SELECT MAX(%s) AS max FROM %s';
$consulta = sprintf($consulta, ArticuloPeer::CREATED_AT, ArticuloPeer::TABLE_NAME);
$sentencia = $conexion->prepareStatement($consulta);
$resultset = $sentencia->executeQuery();
$resultset->next();
$max = $resultset->getInt('max');

Al igual que sucede con las selecciones realizadas con Propel, las consultas con Creole 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.

Truco Si se salta esa forma de acceder y se intenta acceder de forma directa a la base de datos, se corre el riesgo de perder la seguridad y la abstracción proporcionadas por Creole. Aunque es más largo hacerlo con Creole, es la forma de utilizar las buenas prácticas que aseguran un buen rendimiento, portabilidad y seguridad a la aplicación. Esta recomendación es especialmente útil para las consultas que contienen parámetros cuyo origen no es confiable (como por ejemplo un usuario de Internet). Creole se encarga de escapar los datos para mantener la seguridad de la base de datos. Si se accede directamente a la base de datos, se corre 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 Symfony reconoce estos nombres de columna y 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. Lo mismo se aplica a las columnas llamadas created_on y updated_on.

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]

Además, los getters de las columnas de fechas permiten indicar el formato de la fecha como argumento:

echo $comentario->getCreatedAt('Y-m-d');