Las novedades de Doctrine ORM 2.5 (Primera parte)

4 de febrero de 2015

Antes de actualizar Doctrine ORM a su versión 2.5, primero lee atentamente esta guía donde se explican todos sus cambios, especialmente aquellos que son incompatibles con las versiones anteriores de Doctrine.

Después, actualiza Doctrine entrando con la consola de comandos en el directorio de tu proyecto y ejecutando el siguiente comando:

$ composer update doctrine/orm

Para que el comando anterior funcione correctamente, la versión de Doctrine indicada en tu archivo composer.json debe permitir actualizar a 2.5. Si usas la versión por defecto que proporciona Symfony ("doctrine/orm": "~2.2,>=2.2.3") la actualización funcionará bien sin tener que hacer nada.

A continuación, se detallan las principales nuevas funcionalidades que incluye Doctrine ORM 2.5:

Se requiere PHP 5.4 o superior

El primer cambio importante introducido por Doctrine ORM 2.5 es la obligación de usar PHP 5.4 o superior. Así que si tu aplicación todavía usa PHP 5.3, no podrás actualizar a Doctrine 2.5.

El evento PostLoad ahora se lanza después de cargar las asociaciones

En las versiones anteriores, cuando una entidad definía el evento @PostLoad, Doctrine ejecutaba los listeners después de que los campos de la entidad se hubieran cargado, pero antes de que las entidades relacionadas estuvieran disponibles.

Posibilidad de añadir event listeners a las entidades dinámicamente

Cuando se crean librerías y aplicaciones desacopladas, puede ser interesante definir un event listener sin saber qué entidades lo utilizarán realmente.

Por eso se ha creado una nueva API que permite asociar listeners a las entidades mediante la clase AttachEntityListenersListener, que a su vez escucha al evento loadMetadata, que se lanza una vez para cada entidad cuando se generan sus metadatos:

<?php

use Doctrine\ORM\Tools\AttachEntityListenersListener;
use Doctrine\ORM\Events;

$listener = new AttachEntityListenersListener();
$listener->addEntityListener(
    'MyProject\Entity\User', 'MyProject\Listener\TimestampableListener',
    Events::prePersist, 'onPrePersist'
);

$evm->addEventListener(Events::loadClassMetadata, $listener);

class TimestampableListener
{
    public function onPrePersist($event)
    {
        $entity = $event->getEntity();
        $entity->setCreated(new \DateTime('now'));
    }
}

Objetos embebidos

Doctrine ahora permite crear varios objetos PHP a partir de una única tabla, gracias a una nueva funcionalidad llamada "Embeddedable Objects". Para ello, dentro de una clase de tipo @Entity puedes definir otra clase de tipo "embebible" mediante la anotación @Embeddable para hacer que sus datos se guarden en la misma tabla de la primera entidad.

Los objetos embebidos no se pueden guardar, actualizar o borrar por sí mismos, sino siempre a través de la entidad en la que están embebidos (a esta entidad "padre" se le suele llamar "root-entity" o "aggregate"). Así que los objetos embebidos no tienen una clave primaria, sino que se identifican mediante los propios valores que contienen.

Ejemplo de cómo definir y utilizar objetos embebidos:

<?php

/** @Entity */
class Product
{
    /** @Id @Column(type="integer") @GeneratedValue */
    private $id;

    /** @Embedded(class = "Money") */
    private $price;
}

/** @Embeddable */
class Money
{
    /** @Column(type = "decimal") */
    private $value;

    /** @Column(type = "string") */
    private $currency = 'EUR';
}

Puedes leer más sobre los objetos embebidos en este tutorial de Doctrine: Separating Concerns using Embeddables.

Esta funcionalidad ha sido desarrollada por Johannes Schmitt.

Caché de segundo nivel

Desde la versión 2.0 de Doctrine, cuando realizas varias búsquedas de un mismo objeto utilizando su clave primaria, Doctrine solamente hace la consulta la primera vez y cachea el objeto para devolverlo las siguientes veces. Esta funcionalidad se denomina caché de primer nivel, guarda las entidades en memoria e implementa el patrón identity map.

La nueva caché de segundo nivel que introduce Doctrine 2.5 funciona de manera diferente. En vez de guardar las entidades en memoria, se guardan en un sistema de caché en memoria como Memcache, Redis, Riak o MongoDB. Además, esta caché guarda el resultado de búsquedas más complejas, no solo aquellas que buscan entidades a partir de su clave primaria. En otras palabras, esta caché es muy parecida a la Query Result Cache que ya existía, pero es mucho más potente.

El siguiente ejemplo muestra cómo cachear una entidad Country relacionada con una entidad User. La aplicación muestra la información del país del usuario continuamente, así que es mejor cachear esa información para poder reutilizarla en todos los usuarios de la aplicación que pertenezcan al mismo país:

<?php

/**
 * @Entity
 * @Cache(usage="READ_ONLY", region="country_region")
 */
class Country
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="integer")
     */
    protected $id;

    /**
     * @Column(unique=true)
     */
    protected $name;
}

En este ejemplo, se define un nuevo segmento de caché llamado country_region, que también hay que configurar en el EntityManager:

$config = new \Doctrine\ORM\Configuration();
$config->setSecondLevelCacheEnabled();

$cacheConfig  =  $config->getSecondLevelCacheConfiguration();
$regionConfig =  $cacheConfig->getRegionsConfiguration();
$regionConfig->setLifetime('country_region', 3600);

A partir de ahora, Doctrine siempre buscará esta información primero en la caché y después en la base de datos.

Soporte para asociaciones ManyToMany en Criteria

La API para definir Criteria soportaba la consulta de colecciones (leer documentación relacionada) desde Doctrine 2.4. Pero en esta nueva versión, además de las asociaciones one-to-many, ahora también se soportan las asociaciones many-to-many.

Soporte para la expresión contains() en Criteria

La expresión contains() ahora se puede usar en la API de Criteria para buscar si una cadena de texto contiene otra cadena. En la práctica esta expresión se traduce a una condición columna LIKE '%cadena-a-buscar%' de SQL.

<?php
use \Doctrine\Common\Collections\Criteria;

$criteria = Criteria::create()
    ->where(Criteria::expr()->contains('name', 'Benjamin'));

$users = $repository->matching($criteria);

Soporte de la opción EXTRA_LAZY en Criteria

Si una colección está definida como fetch="EXTRA_LAZY", cuando se utiliza Collection::matching($criteria) ahora se devuelve una colección de tipo lazy:

<?php

class Post
{
    /** @OneToMany(targetEntity="Comment", fetch="EXTRA_LAZY") */
    private $comments;
}

$criteria = Criteria::create()
    ->where(Criteria->expr()->eq("published", 1));

$publishedComments = $post->getComments()->matching($criteria);

echo count($publishedComments);

La colección asociada no se carga cuando se utiliza count() o contains(), pero si se realiza cualquier otra operación sobre ella, sí que se carga completamente la colección.

Esta funcionalidad ha sido desarrollada por Michaël Gallego.

Posibilidad de configurar opciones de los índices

Mediante la configuración del ORM ahora es posible establecer las opciones de DBAL para los índices de la base de datos. Antes sólo era posible hacerlo mediante un listener:

<?php

/**
 * @Table(name="product", indexes={@Index(columns={"description"},flags={"fulltext"})})
 */
class Product
{
    private $description;
}

Esta funcionalidad ha sido desarrollada por Adrian Olek.

Comprobar los parámetros definidos en la API de SQLFilter

Ahora es posible comprobar dentro de un filtro de tipo SQLFilter si un parámetro está definido o no. Esto permite controlar más fácilmente qué opciones del filtro activar o desactivar.

El siguiente ejemplo utiliza el mismo código que el se usa en la documentación de los filtros SQLFilter:

<?php
class MyLocaleFilter extends SQLFilter
{
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
    {
        if (!$targetEntity->reflClass->implementsInterface('LocaleAware')) {
            return "";
        }

        if (!$this->hasParameter('locale')) {
            return "";
        }

        return $targetTableAlias.'.locale = ' . $this->getParameter('locale');
    }
}

Esta funcionalidad ha sido desarrollada por Miroslav Demovic.

Mejoras en la opción EXTRA_LAZY

Las consultas que utilizan EXTRA_LAZY y containsKey ahora son más eficientes. Cuando se invoca el método Collection::containsKey($key) en colecciones de tipo one-to-many y many-to-many que utilizan indexBy y EXTRA_LAZY, ahora se ejecuta una consulta para comprobar si existe el item. Antes esta operación se hacía en memoria después de cargar todas las entidades de la colección.

<?php

class User
{
    /** @OneToMany(targetEntity="Group", indexBy="id") */
    private $groups;
}

if ($user->getGroups()->containsKey($groupId)) {
    echo "User is in group $groupId\n";
}

Esta funcionalidad ha sido desarrollada por Asmir Mustafic.

Además, Sander Marechal ha desarrollado una funcionalidad que añade soporte para EXTRA_LAZY en el método get() de las asociaciones many-to-many, tanto owning como inverse.

Mejorada la eficiencia de la opción EAGER

Cuando se define una asociación one-to-many como fetch="EAGER", ahora se ejecuta una consulta menos que antes. Además, ahora también funciona bien cuando se utiliza junto con indexBy.

Mejorado el soporte de EntityManagerInterface

La versión 2.4 de Doctrine introdujo la interfaz EntityManagerInterface. Esta nueva versión 2.5 hace que se utilice en muchos sitios donde antes solamente se usaba la clase Doctrine\ORM\EntityManager.

Este cambio hace que sea más sencillo usar el patrón decorator para extender la clase EntityManager cuando sea necesario. En cualquier caso, la nueva interfaz todavía no se utiliza en todas las partes del código de Doctrine, así que debes tener cuidado.

Recursos