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}
),
...
)