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 Article();
$articulo->setTitle('Mi primer artículo');
$articulo->setContent('Este es mi primer artículo. \n Espero que te guste.');
$titulo = $articulo->getTitle();
$contenido = $articulo->getContent();
Nota La clase objeto generada se llama Article
, que es el valor de la propiedad phpName
para la tabla blog_article
. Si no se hubiera definido la propiedad phpName
, la clase se habría llamado BlogArticle
. Los métodos accesores (get
y set
) utilizan una variante de camelCase aplicada al nombre de las columnas, por lo que el método getTitle()
obtiene el valor de la columna title
.
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(
'title' => 'Mi primer artículo',
'content' => 'Este es mi primer artículo. \n Espero que te guste.'
));
8.4.2. Obtener los registros relacionados
La columna article_id
de la tabla blog_comment
define implícitamente una clave externa a la tabla blog_article
. 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->getArticle()
: para obtener el objetoArticle
relacionado$comentario->getArticleId()
: para obtener el ID del objetoArticle
relacionado$comentario->setArticle($articulo)
: para definir el objetoArticle
relacionado$comentario->setArticleId($id)
: para definir el objetoArticle
relacionado a partir de un ID$articulo->getComments()
: para obtener los objetosComment
relacionados
Los métodos getArticleId()
y setArticleId()
demuestran que se puede utilizar la columna article_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 Comment();
$comentario->setAuthor('Steve');
$comentario->setContent('¡Es el mejor artículo que he leído nunca!');
// Añadir este comentario al anterior objeto $articulo
$comentario->setArticle($articulo);
// Sintaxis alternativa
// Solo es correcta cuando el objeto artículo ya
// ha sido guardado anteriormente en la base de datos
$comentario->setArticleId($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->getArticle()->getTitle();
=> Mi primer artículo
echo $comentario->getArticle()->getContent();
=> Este es mi primer artículo.
Espero que te guste.
// Relación "uno a muchos"
$comentarios = $articulo->getComments();
El método getArticle()
devuelve un objeto de tipo Article
, que permite utilizar el método accesor getTitle()
. 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->getArticleId()
).
La variable $comentarios
del listado 8-10 contiene un array de objetos de tipo Comment
. 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_comment
crea el método getComments()
, cuyo nombre se crea añadiendo una letra s
al nombre del objeto Comment
. Si el nombre del modelo fuera plural, la generación automática llamaría getCommentss()
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_article
. 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->getComments() as $comentario)
{
$comentario->delete();
}
Truco 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 = ArticlePeer::retrieveByPk(7);
El archivo schema.yml
define el campo id
como clave primaria de la tabla blog_article
, 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 Article
.
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 Article
, se llama al método ArticlePeer::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 = ArticlePeer::doSelect($c);
// Genera la siguiente consulta SQL
SELECT blog_article.ID, blog_article.TITLE, blog_article.CONTENT,
blog_article.CREATED_AT
FROM blog_article;
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(CommentPeer::AUTHOR, 'Steve');
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$comentarios = CommentPeer::doSelect($c);
// Genera la siguiente consulta SQL
SELECT blog_comment.ARTICLE_ID, blog_comment.AUTHOR, blog_comment.CONTENT,
blog_comment.CREATED_AT
FROM blog_comment
WHERE blog_comment.author = 'Steve'
ORDER BY blog_comment.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 content
de la tabla blog_article
, se utiliza la constante de clase llamada ArticlePeer::CONTENT
.
Nota ¿Por qué se utiliza CommentPeer::AUTHOR
en vez de blog_comment.AUTHOR
, 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 author
ahora se llama contributor
. Si se hubiera utilizado el valor blog_comment.AUTHOR
, es necesario modificar ese valor en cada llamada al modelo. Sin embargo, si se utiliza el valorCommentPeer::AUTHOR
, solo es necesario cambiar el nombre de la columna en el archivo schema.yml
, manteniendo el atributo phpName
a un valor igual a AUTHOR
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.addComments
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(CommentPeer::AUTHOR, 'Steve');
$c->addJoin(CommentPeer::ARTICLE_ID, ArticlePeer::ID);
$c->add(ArticlePeer::CONTENT, '%enjoy%', Criteria::LIKE);
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$comentarios = CommentPeer::doSelect($c);
// Genera la siguiente consulta SQL
SELECT blog_comment.ID, blog_comment.ARTICLE_ID, blog_comment.AUTHOR,
blog_comment.CONTENT, blog_comment.CREATED_AT
FROM blog_comment, blog_article
WHERE blog_comment.AUTHOR = 'Steve'
AND blog_article.CONTENT LIKE '%enjoy%'
AND blog_comment.ARTICLE_ID = blog_article.ID
ORDER BY blog_comment.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_0/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:
- Obtener la conexión con la base de datos.
- Construir la consulta.
- Crear una sentencia con esa consulta.
- 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, ArticlePeer::CREATED_AT, ArticlePeer::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 Comment();
$comentario->setAuthor('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');