Las novedades de Doctrine ORM 2.5 (Segunda parte)

Este tutorial es la segunda parte del artículo sobre las novedades de Doctrine 2.5. La primera parte se centró en las nuevas funcionalidades más relevantes, como los objetos embebidos y la caché de segundo nivel para Redis, Memcache y Riak.

En esta segunda parte se explican las mejoras de las funcionalidades que ya existían en Doctrine. Además, se detallan los cambios que Doctrine 2.5 introduce y que son incompatibles con sus versiones anteriores, por lo que te obligarán a cambiar el código de tu aplicación.

Mejoras en el lenguaje DQL

1. La cláusula ORDER BY de las consultas DQL ahora permite utilizar funciones:

$dql = "SELECT u FROM User u ORDER BY CONCAT(u.username, u.name)";

2. Las expresiones IS_NULL también permiten el uso de funciones:

$dql = "SELECT u.name FROM User u WHERE MAX(u.name) IS NULL";

3. La cláusula HAVING ahora permite el uso de expresiones LIKE.

4. Las expresiones NEW() ahora soportan el uso de subconsultas:

$dql = "SELECT new UserDTO(u.name, SELECT count(g.id) FROM Group g WHERE g.id = u.id) FROM User u";

5. La expresión MEMBER OF ahora permite filtrar por más de un resultado:

$dql = "SELECT u FROM User u WHERE :groups MEMBER OF u.groups";
$query = $entityManager->createQuery($dql);
$query->setParameter('groups', array(1, 2, 3));
 
$users = $query->getResult();

6. Ahora es posible utilizar expresiones dentro de COUNT():

$dql = "SELECT COUNT(DISTINCT CONCAT(u.name, u.lastname)) FROM User u";

7. Las funciones DATE_ADD()/DATE_SUB() ahora soportan la expresión HOUR.

Añadido soporte para factorías en las funciones DQL

En las versiones anteriores de Doctrine, había que indicar la clase completa de las funciones DQL propias. Por eso no era posible utilizar la inyección de dependencias para configurar esas funciones en tiempo de ejecución.

Matthieu Napoli ha implementado una nueva funcionalidad muy sencilla que permite pasar un callback que es el que se encarga de resolver la función DQL en tiempo de ejecución:

$config = new \Doctrine\ORM\Configuration();
 
$config->addCustomNumericFunction(
    'IS_PUBLISHED', function($funcName) use ($currentSiteId) {
        return new IsPublishedFunction($currentSiteId);
     }
);

Colecciones como parámetro de las búsquedas WHERE ... IN

Las consultas de tipo WHERE IN ahora permiten usar directamente el array que contiene la colección de entidades a utilizar en la búsqueda:

$categories = $rootCategory->getChildren();
 
$queryBuilder
    ->select('p')
    ->from('Product', 'p')
    ->where('p.category IN (:categories)')
    ->setParameter('categories', $categories)
;

Esta funcionalidad ha sido desarrollada por Michael Perrin.

Posibilidad de definir Query Hints por defecto para todas las consultas

Doctrine soporta los query hints desde la versión 2.0, ya que se utiliza en varios elementos, como el AST, los fetch modes, el locking y otras funcionalidades relacionads con la generación de código DQL.

La novedad es que ahora es posible definir query hints por defecto que están activados para todas las consultas:

$config = new \Doctrine\ORM\Configuration();
$config->setDefaultQueryHints(
    'doctrine.customOutputWalker' => 'MyProject\CustomOutputWalker'
);

Esta funcionalidad ha sido desarrollada por Artur Eshenbrener.

ResultSetMappingBuilder soporta herencia de tipo Single-Table

En las versiones anteriores, ResultSetMappingBuilder no funcionaba con las entidades que utilizaban herencia de tipo single table. En Doctrine ORM 2.5 esta limitación ha desaparecido.

Definición simple de relaciones many-to-many cuando se usa YAML

La configuración basada en XML y la configuración basada en anotaciones permiten definir relaciones de tipo many-to-many muy fácilmente, ya que no requiren definir la columna de tipo join utilizada en la relación.

A partir de Doctrine 2.5, la configuración basada en YAML también permite utilizar este atajo para definir relaciones many-to-many:

manyToMany:
    groups:
        targetEntity: Group
        joinTable:
            name: users_groups

Mayor control sobre el comando que valida el esquema

El comando que valida el esquema ejecuta dos validaciones adicionales para comprobar la validez de los mappings y para comprobar si el esquema está correctamente sincronizado. Ahora es posible saltarse cualquiera de estas dos comprobaciones:

$ php vendor/bin/doctrine orm:validate-schema --skip-mapping
$ php vendor/bin/doctrine orm:validate-schema --skip-sync

La ventaja de deshabilitar estas comprobaciones es que ahora puedes crear por ejemplo scripts de integración continua más especializados. Si no se encuentra ningún error, el comando devuelve 0; si se produce un error de mapping, devuelve 1; si se produce un error de sincronización, devuelve 2; y si se producen ambos errores, se devuelve 3.

Evitar las copias de seguridad al generar las entidades

Cuando se ejecuta el comando que genera las entidades, Doctrine hace una copia de seguridad del archivo que se está modificando para evitar la pérdida de información. Utiliza la nueva opción -no-backup para no generar esas copias de seguridad:

$ php vendor/bin/doctrine orm:generate-entities src/ --no-backup

Uso de objetos como identificadores de entidades

Ahora es posible utilizar objetos como identificadores de las entidades, siempre que implementen el método mágico __toString().

class UserId
{
    private $value;
 
    public function __construct($value)
    {
        $this->value = $value;
    }
 
    public function __toString()
    {
        return (string)$this->value;
    }
}
 
class User
{
    /** @Id @Column(type="userid") */
    private $id;
 
    public function __construct(UserId $id)
    {
        $this->id = $id;
    }
}
 
class UserIdType extends \Doctrine\DBAL\Types\Type
{
    // ...
}
 
Doctrine\DBAL\Types\Type::addType('userid', 'MyProject\UserIdType');

Cambios incompatibles con las versiones anteriores de Doctrine

En esta sección se muestran todos los cambios de tipo BC Break (Backwards compatibility Break) introducidos por Doctrine ORM 2.5. Si utilizas alguna de estas funcionalidades, tendrás que actualizar el código de tu aplicación antes de pasar a Doctrine 2.5.

La interfaz NamingStrategy ha cambiado

La interfaz Doctrine\ORM\Mapping\NamingStrategyInterface ha cambiado ligeramente para pasar el nombre de la clase de la entidad en el método que genera el nombre de la columna join de la relación:

// antes
function joinColumnName($propertyName);
 
// ahora
function joinColumnName($propertyName, $className = null);

También se ha añadido un nuevo método necesario para tratar los objetos embebidos, una de las principales novedades de Doctrine 2.5:

public function embeddedFieldToColumnName($propertyName, $embeddedColumnName);

Los type-hints utilizan EntityManagerInterface en vez de EntityManager

Todas las clases que requieren la clase EntityManager en cualquiera de sus métodos ahora requieren la interfaz EntityManagerInterface.

Así que si tu aplicación extiende cualquiera de las siguientes clases, asegúrate de cambiar estas llamadas a métodos:

  • Doctrine\ORM\Tools\DebugUnitOfWorkListener#dumpIdentityMap(EntityManagerInterface $em)
  • Doctrine\ORM\Mapping\ClassMetadataFactory#setEntityManager(EntityManagerInterface $em)

Modificada la API de los hydrators propios

La API de AbstractHydrator ya no obliga a usar una caché. Además, se ha añadido el método hydrateColumnInfo($column) para obtener información sobre una columna.

Por otra parte, la variable que representaba a la caché ya no se pasa por referencia entre los diferentes métodos, ya que no es necesario hacerlo desde la versión 2.4 de Doctrine en la que los hydrators se instancian para cada consulta.

El inheritance map debe contener todas las clases de una herencia de entidades

Hasta ahora, era posible no añadir en el inheritance map la clase padre de una herencia de entidades. En Doctrine 2.5 esto ya no es posible, por lo que si no quieres persistir las instancias de esa clase padre, debes aplicar una de las dos siguientes soluciones:

  • convierte la clase padre en abstracta.
  • añade la clase padre al inheritance map.

Si no lo haces así, la aplicación lanzará una excepción de tipo Doctrine\ORM\Mapping\MappingException.

El método EntityManager#clear() también se ejecuta en las asociaciones

En Doctrine 2.4 y versiones anteriores, cuando se invoca el método EntityManager#clear() pasándole el nombre de una entidad, sólo se aplica el proceso de detach a la entidad indicada.

En Doctrine 2.5, este mismo método también sigue todos los cascades configurados en la entidad. Esto hace por ejemplo que se reduzca el consumo de memoria en tareas complejas, ya que se aplica el recolector de basura a esas asociaciones configuradas en los cascades.

Las actualizaciones sobre las entidades borradas ya no se aplican

En Doctrine 2.4, cuando modificas una propiedad que se ha marcado como borrada, se ejecuta una sentencia de tipo UPDATE justo antes de la sentencia DELETE. Y lo que es peor, también se ejecutan los listeners preUpdate y postUpdate, que en este caso no sirven para nada más que para penalizar el rendimiento de la aplicación.

Por otra parte, en los listeners de preFlush era posible hacer que una entidad marcada para borrar ya no se borrara. El truco consistía en llamar al método persist() si la entidad se encontraba tanto en entityUpdates como en entityDeletions. En Doctrine 2.5 este truco ya no funciona porque se ha cambiado gran parte de la lógica que determina qué cambios se han producido en las entidades.

El modo de bloqueo por defecto ahora es null en vez de LockMode::NONE

(Este cambio sólo afecta a las bases de datos tipo Microsoft SQL Server)

Debido a una confusión sobre el modo de bloqueo (lock mode) por defecto usado en los métodos, se producían algunos errores no deseados en bases de datos de tipo SQL Server.

Como el modo de bloqueo por defecto (LockMode::NONE) se utilizaba en todos los métodos, todas las consultas relacionadas con el locking añadían automáticamente el WITH (NOLOCK).

El resultado era impredecible porque cuando la consulta incluye el WITH (NOLOCK), la base de datos SQL Server ejecuta la consulta en una transacción de tipo READ UNCOMMITTED en vez de READ COMMITTED.

Por todo ello, ahora se distingue entre el locking de tipo LockMode::NONE y el locking de tipo null. Así Doctrine sabe cuándo añadir la información del locking en las consultas y cuándo no hacerlo. De esta manera, los siguientes métodos han cambiado su declaración para usar $lockMode = null en vez de $lockMode = LockMode::NONE:

  • Doctrine\ORM\Cache\Persister\AbstractEntityPersister#getSelectSQL()
  • Doctrine\ORM\Cache\Persister\AbstractEntityPersister#load()
  • Doctrine\ORM\Cache\Persister\AbstractEntityPersister#refresh()
  • Doctrine\ORM\Decorator\EntityManagerDecorator#find()
  • Doctrine\ORM\EntityManager#find()
  • Doctrine\ORM\EntityRepository#find()
  • Doctrine\ORM\Persisters\BasicEntityPersister#getSelectSQL()
  • Doctrine\ORM\Persisters\BasicEntityPersister#load()
  • Doctrine\ORM\Persisters\BasicEntityPersister#refresh()
  • Doctrine\ORM\Persisters\EntityPersister#getSelectSQL()
  • Doctrine\ORM\Persisters\EntityPersister#load()
  • Doctrine\ORM\Persisters\EntityPersister#refresh()
  • Doctrine\ORM\Persisters\JoinedSubclassPersister#getSelectSQL()

Si has extendido alguna de estas clases, no olvides actualizar la declaración de todos estos métodos. Y por supuesto, comprueba también el código que hace llamadas a estos métodos.

El método __clone() ya no se invoca al instanciar nuevas entidades

En las aplicaciones que utilizan PHP 5.6, la instanciación de nuevas entidades se realiza mediante la librería doctrine/instantiator, que no ejecuta ni el método mágico __clone() ni ningún otro método del objeto instanciado.

DefaultRepositoryFactory se ha declarado como final

Debido a este cambio, en vez de extender la clase Doctrine\ORM\Repository\DefaultRepositoryFactory, ahora debes implementar la interfaz Doctrine\ORM\Repository\RepositoryFactory.

Las consultas que crean objetos respetan los alias indicados

Cuando se ejecutan consultas DQL que crean nuevos objetos, en vez de devolver los datos todos juntos en un array escalar, ahora se respetan los alias utilizados en la consulta. Así por ejemplo, la siguiente consulta DQL:

SELECT NEW UserDTO(u.id, u.name) AS USER,
       NEW AddressDTO(a.street, a.postalCode) AS address,
       a.id AS addressId
FROM USER u
INNER JOIN u.addresses a WITH a.isPrimary = TRUE

Doctrine 2.4 devolvería como resultado el siguiente array:

array(
    0=>array(
        0=>{UserDTO object},
        1=>{AddressDTO object},
        2=>{u.id scalar},
        3=>{u.name scalar},
        4=>{a.street scalar},
        5=>{a.postalCode scalar},
        'addressId'=>{a.id scalar},
    ),
    ...
)

En Doctrine 2.5 el resultado devuelto es el que se espera:

array(
    0=>array(
        'user'=>{UserDTO object},
        'address'=>{AddressDTO object},
        'addressId'=>{a.id scalar}
    ),
    ...
)

Recursos

Comentarios