1
0
mirror of synced 2024-12-05 03:06:05 +03:00

Add Changelog/Migration to 2.5 documentation chapter.

This commit is contained in:
Benjamin Eberlei 2015-01-18 15:18:18 +01:00
parent 61e07e5907
commit 79ba8c6be8
3 changed files with 700 additions and 1 deletions

View File

@ -26,11 +26,15 @@ As of 2.5, ``EntityManager`` will follow configured cascades, providing a better
memory management since associations will be garbage collected, optimizing
resources consumption on long running jobs.
## BC BREAK: NamingStrategy has a new method ``embeddedFieldToColumnName($propertyName, $embeddedColumnName)``
## BC BREAK: NamingStrategy interface changes
1. A new method ``embeddedFieldToColumnName($propertyName, $embeddedColumnName)``
This method generates the column name for fields of embedded objects. If you implement your custom NamingStrategy, you
now also need to implement this new method.
2. A change to method ``joinColumnName()`` to include the $className
## Updates on entities scheduled for deletion are no longer processed
In Doctrine 2.4, if you modified properties of an entity scheduled for deletion, UnitOfWork would

View File

@ -0,0 +1,690 @@
What is new in Doctrine ORM 2.5?
================================
This document describes changes between Doctrine ORM 2.4 and 2.5 (currently in
Beta). It contains a description of all the new features and sections
about behavioral changes and potential backwards compatibility breaks.
Please review this document carefully when updating to Doctrine 2.5.
First note, that with the ORM 2.5 release we are dropping support
for PHP 5.3. We are enforcing this with Composer, servers without
at least PHP 5.4 will not allow installing Doctrine 2.5.
New Features and Improvements
-----------------------------
Events: PostLoad now triggered after associations are loaded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before Doctrine 2.5 if you had an entity with a ``@PostLoad`` event
defined then Doctrine would trigger listeners after the fields were
loaded, but before assocations are available.
- `DDC-54 <http://doctrine-project.org/jira/browse/DDC-54>`_
- `Commit <https://github.com/doctrine/doctrine2/commit/a906295c65f1516737458fbee2f6fa96254f27a5>`_
Events: Add API to programatically add event listeners to Entity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When developing third party libraries or decoupled applications
it can be interesting to develop an entity listener without knowing
the entities that require this listener.
You can now attach entity listeners to entities using the
``AttachEntityListenersListener`` class, which is listening to the
``loadMetadata`` event that is fired once for every entity during
metadata generation:
.. code-block:: php
<?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'));
}
}
Embeddedable Objects
~~~~~~~~~~~~~~~~~~~~
Doctrine now supports creating multiple PHP objects from one database table
implementing a feature called "Embeddedable Objects". Next to an ``@Entity``
class you can now define a class that is embeddable into a database table of an
entity using the ``@Embeddable`` annotation. Embeddable objects can never be
saved, updated or deleted on their own, only as part of an entity (called
"root-entity" or "aggregate"). Consequently embeddables don't have a primary
key, they are identified only by their values.
Example of defining and using embeddables classes:
.. code-block:: php
<?php
/** @Entity */
class Product
{
/** @Id @Column(type="integer") @GeneratedValue */
private $id;
/** @Embedded(class = "Money") */
private $price;
}
/** @Embeddable */
class Price
{
/** @Column(type = "decimal") */
private $value;
/** @Column(type = "string") */
private $currency = 'EUR';
}
You can read more on the features of Embeddables objects `in the documentation
<http://docs.doctrine-project.org/en/latest/tutorials/embeddables.html>`_.
This feature was developed by external contributor `Johannes Schmitt
<https://twitter.com/schmittjoh>`_
- `DDC-93 <http://doctrine-project.org/jira/browse/DDC-93>`_
- `Pull Request <https://github.com/doctrine/doctrine2/pull/835>`_
Second-Level-Cache
~~~~~~~~~~~~~~~~~~
Since version 2.0 of Doctrine, fetching the same object twice by primary key
would result in just one query. This was achieved by the identity map pattern
(first-level-cache) that kept entities in memory.
The newly introduced second-level-cache works a bit differently. Instead
of saving objects in memory, it saves them in a fast in-memory cache such
as Memcache, Redis, Riak or MongoDB. Additionally it allows saving the result
of more complex queries than by primary key. Summarized this feature works
like the existing Query result cache, but it much more powerful.
As an example lets cache an entity Country that is a relation to the User
entity. We always want to display the country, but avoid the additional
query to this table.
.. code-block:: php
<?php
/**
* @Entity
* @Cache(usage="READ_ONLY", region="country_region")
*/
class Country
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
protected $name;
}
In this example we have specified a caching region name called
``country_region``, which we have to configure now on the EntityManager:
.. code-block:: php
$config = new \Doctrine\ORM\Configuration();
$config->setSecondLevelCacheEnabled();
$cacheConfig = $config->getSecondLevelCacheConfiguration();
$regionConfig = $cacheConfig->getRegionsConfiguration();
$regionConfig->setLifetime('country_region', 3600);
Now Doctrine will first check for the data of any country in the cache
instead of the database.
- `Documentation
<http://docs.doctrine-project.org/en/latest/reference/second-level-cache.html>`_
- `Pull Request <https://github.com/doctrine/doctrine2/pull/808>`_
Criteria API: Support for ManyToMany assocations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We introduced support for querying collections using the `Criteria API
<http://docs.doctrine-project.org/en/latest/reference/working-with-associations.html#filtering-collections>`_
in 2.4. This only worked efficently for One-To-Many assocations, not for
Many-To-Many. With the start of 2.5 also Many-To-Many associations get queried
instead of loading them into memory.
Criteria API: Add new contains() expression
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It is now possible to use the Criteria API to check for string contains needle
using ``contains()``. This translates to using a ``column LIKE '%needle%'`` SQL
condition.
.. code-block:: php
<?php
use \Doctrine\Common\Collections\Criteria;
$criteria = Criteria::create()
->where(Criteria::expr()->contains('name', 'Benjamin'));
$users = $repository->matching($criteria);
Criteria API: Support for EXTRA_LAZY
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A collection that is marked as ``fetch="EXTRA_LAZY"`` will now return another
lazy collection when using ``Collection::matching($criteria)``:
.. code-block:: php
<?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);
The lazy criteria currently supports the ``count()`` and ``contains()``
functionality lazily. All other operations of the ``Collection`` interface
trigger a full load of the collection.
This feature was contributed by `Michaël Gallego <https://github.com/bakura10>`_.
- `Pull Request #1 <https://github.com/doctrine/doctrine2/pull/882>`_
- `Pull Request #2 <https://github.com/doctrine/doctrine2/pull/1032>`_
Mapping: Allow configuring Index flags
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It is now possible to control the index flags in the DBAL
schema abstraction from the ORM using metadata. This was possible
only with a schema event listener before.
.. code-block:: php
<?php
/**
* @Table(name="product", indexes={@Index(columns={"description"},flags={"fulltext"})})
*/
class Product
{
private $description;
}
This feature was contributed by `Adrian Olek <https://github.com/adrianolek>`_.
- `Pull Request <https://github.com/doctrine/doctrine2/pull/973>`_
SQLFilter API: Check if parameter isset
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can now check in your SQLFilter if a parameter was set. This allows
to more easily control which features of a filter to enable or disable.
Extending on the locale example of the documentation:
.. code-block:: php
<?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');
}
}
This feature was contributed by `Miroslav Demovic <https://github.com/mdemo>`_
- `Pull Request <https://github.com/doctrine/doctrine2/pull/963>`_
EXTRA_LAZY Improvements
~~~~~~~~~~~~~~~~~~~~~~~
1. Efficient query when using EXTRA_LAZY and containsKey
When calling ``Collection::containsKey($key)`` on one-to-many and many-to-many
collections using ``indexBy`` and ``EXTRA_LAZY`` a query is now executed to check
for the existance for the item. Prevoiusly this operation was performed in memory
by loading all entities of the collection.
.. code-block:: php
<?php
class User
{
/** @OneToMany(targetEntity="Group", indexBy="id") */
private $groups;
}
if ($user->getGroups()->containsKey($groupId)) {
echo "User is in group $groupId\n";
}
This feature was contributed by `Asmir Mustafic <https://github.com/goetas>`_
- `Pull Request <https://github.com/doctrine/doctrine2/pull/937>`_
2. Add EXTRA_LAZY Support for get() for owning and inverse many-to-many
This was contributed by `Sander Marechal <https://github.com/sandermarechal>`_.
Improve efficiency of One-To-Many EAGER
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When marking a one-to-many association with ``fetch="EAGER"`` it will now
execute one query less than before and work correctly in combination with
``indexBy``.
Full support for EntityManagerInterface
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Most of the locations where previously only the ``Doctrine\ORM\EntityManager``
was allowed are now changed to accept the ``EntityManagerInterface`` that was
introduced in 2.4. This allows you to more easily use the decorator pattern
to extend the EntityManager if you need.
DQL Improvements
~~~~~~~~~~~~~~~~
1. It is now possible to add functions to the ORDER BY clause in DQL statements:
.. code-block:: php
<?php
$dql = "SELECT u FROM User u ORDER BY CONCAT(u.username, u.name)";
2. Support for functions in IS NULL expressions:
.. code-block:: php
<?php
$dql = "SELECT u.name FROM User u WHERE MAX(u.name) IS NULL";
3. A LIKE expression is now suported in HAVING clause.
4. Subselects are now supported inside a ``NEW()`` expression:
.. code-block:: php
<?php
$dql = "SELECT new UserDTO(u.name, SELECT count(g.id) FROM Group g WHERE g.id = u.id) FROM User u";
5. MEMBER OF expression now allows to filter for more than one result
.. code-block:: php
<?php
$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. Expressions inside COUNT() now allowed
.. code-block:: php
<?php
$dql = SELECT COUNT(DISTINCT CONCAT(u.name, u.lastname)) FROM User u";
7. Add support for HOUR in DATE_ADD()/DATE_SUB() functions
Custom DQL Functions: Add support for factories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Previoiusly custom DQL functions could only be provided with their
full-qualified class-name, preventing runtime configuration through
dependency injection.
A simplistic approach has been contributed by `Matthieu Napoli
<https://github.com/mnapoli>`_ to pass a callback instead that resolves
the function:
.. code-block:: php
<?php
$config = new \Doctrine\ORM\Configuration();
$config->addCustomNumericFunction(
'IS_PUBLISHED', function($funcName) use ($currentSiteId) {
return new IsPublishedFunction($currentSiteId);
}
);
Query API: WHERE IN Query using a Collection as parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When performing a WHERE IN query for a collection of entities you can
now pass the array collection of entities as a parameter value to the query
object:
.. code-block:: php
<?php
$categories = $rootCategory->getChildren();
$queryBuilder
->select('p')
->from('Product', 'p')
->where('p.category IN (:categories)')
->setParameter('categories', $categories)
;
This feature was contributed by `Michael Perrin
<https://github.com/michaelperrin>`_.
- `Pull Request <https://github.com/doctrine/doctrine2/pull/590>`_
- `DDC-2319 <http://doctrine-project.org/jira/browse/DDC-2319>`_
Query API: Add suport for default Query Hints
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To configure multiple different features such as custom AST Walker, fetch modes,
locking and other features affecting DQL generation we have had a feature
called "query hints" since version 2.0.
It is now possible to add query hints that are always enabled for every Query:
.. code-block:: php
<?php
$config = new \Doctrine\ORM\Configuration();
$config->setDefaultQueryHints(
'doctrine.customOutputWalker' => 'MyProject\CustomOutputWalker'
);
This feature was contributed by `Artur Eshenbrener
<https://github.com/Strate>`_.
- `Pull Request <https://github.com/doctrine/doctrine2/pull/863>`_
ResultSetMappingBuilder: Add support for Single-Table Inheritance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before 2.5 the ResultSetMappingBuilder did not work with entities
that are using Single-Table-Inheritance. This restriction was lifted
by adding the missing support.
YAML Mapping: Many-To-Many doesnt require join column definition
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In Annotations and XML it was not necessary using conventions for naming
the many-to-many join column names, in YAML it was not possible however.
A many-to-many definition in YAML is now possible using this minimal
definition:
.. code-block:: yaml
manyToMany:
groups:
targetEntity: Group
joinTable:
name: users_groups
Schema Validator Command: Allow to skip sub-checks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Schema Validator command executes two independent checks
for validity of the mappings and if the schema is synchronized
correctly. It is now possible to skip any of the two steps
when executing the command:
::
$ php vendor/bin/doctrine orm:validate-schema --skip-mapping
$ php vendor/bin/doctrine orm:validate-schema --skip-sync
This allows you to write more specialized continuous integration and automation
checks. When no changes are found the command returns the exit code 0
and 1, 2 or 3 when failing because of mapping, sync or both.
EntityGenerator Command: Avoid backups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When calling the EntityGenerator for an existing entity, Doctrine would
create a backup file every time to avoid loosing changes to the code.
You can now skip generating the backup file by passing the ``--no-backup``
flag:
::
$ php vendor/bin/doctrine orm:generate-entities src/ --no-backup
Support for Objects as Identifiers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It is now possible to use Objects as identifiers for Entities
as long as they implement the magic method ``__toString()``.
.. code-block:: php
<?php
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');
Behavioral Changes (BC Breaks)
------------------------------
NamingStrategy interface changed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``Doctrine\ORM\Mapping\NamingStrategyInterface`` changed slightly
to pass the Class Name of the entity into the join column name generation:
::
- function joinColumnName($propertyName);
+ function joinColumnName($propertyName, $className = null);
It also received a new method for supporting embeddables:
::
public function embeddedFieldToColumnName($propertyName, $embeddedColumnName);
Minor BC BREAK: EntityManagerInterface instead of EntityManager in type-hints
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As of 2.5, classes requiring the ``EntityManager`` in any method signature will now require
an ``EntityManagerInterface`` instead.
If you are extending any of the following classes, then you need to check following
signatures:
- ``Doctrine\ORM\Tools\DebugUnitOfWorkListener#dumpIdentityMap(EntityManagerInterface $em)``
- ``Doctrine\ORM\Mapping\ClassMetadataFactory#setEntityManager(EntityManagerInterface $em)``
Minor BC BREAK: Custom Hydrators API change
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As of 2.5, `AbstractHydrator` does not enforce the usage of cache as part of
API, and now provides you a clean API for column information through the method
`hydrateColumnInfo($column)`.
Cache variable being passed around by reference is no longer needed since
Hydrators are per query instantiated since Doctrine 2.4.
- `DDC-3060 <http://doctrine-project.org/jira/browse/DDC-3060>`_
Minor BC BREAK: Entity based EntityManager#clear() calls follow cascade detach
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Whenever ``EntityManager#clear()`` method gets called with a given entity class
name, until 2.4, it was only detaching the specific requested entity.
As of 2.5, ``EntityManager`` will follow configured cascades, providing a better
memory management since associations will be garbage collected, optimizing
resources consumption on long running jobs.
Updates on entities scheduled for deletion are no longer processed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In Doctrine 2.4, if you modified properties of an entity scheduled for deletion, UnitOfWork would
produce an UPDATE statement to be executed right before the DELETE statement. The entity in question
was therefore present in ``UnitOfWork#entityUpdates``, which means that ``preUpdate`` and ``postUpdate``
listeners were (quite pointlessly) called. In ``preFlush`` listeners, it used to be possible to undo
the scheduled deletion for updated entities (by calling ``persist()`` if the entity was found in both
``entityUpdates`` and ``entityDeletions``). This does not work any longer, because the entire changeset
calculation logic is optimized away.
Minor BC BREAK: Default lock mode changed from LockMode::NONE to null in method signatures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A misconception concerning default lock mode values in method signatures lead to unexpected behaviour
in SQL statements on SQL Server. With a default lock mode of ``LockMode::NONE`` throughout the
method signatures in ORM, the table lock hint ``WITH (NOLOCK)`` was appended to all locking related
queries by default. This could result in unpredictable results because an explicit ``WITH (NOLOCK)``
table hint tells SQL Server to run a specific query in transaction isolation level READ UNCOMMITTED
instead of the default READ COMMITTED transaction isolation level.
Therefore there now is a distinction between ``LockMode::NONE`` and ``null`` to be able to tell
Doctrine whether to add table lock hints to queries by intention or not. To achieve this, the following
method signatures have been changed to declare ``$lockMode = null`` instead of ``$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()``
You should update signatures for these methods if you have subclassed one of the above classes.
Please also check the calling code of these methods in your application and update if necessary.
.. note::
This in fact is really a minor BC BREAK and should not have any affect on database vendors
other than SQL Server because it is the only one that supports and therefore cares about
``LockMode::NONE``. It's really just a FIX for SQL Server environments using ORM.
Minor BC BREAK: __clone method not called anymore when entities are instantiated via metadata API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As of PHP 5.6, instantiation of new entities is deferred to the
`doctrine/instantiator <https://github.com/doctrine/instantiator>`_ library, which will avoid calling ``__clone``
or any public API on instantiated objects.
BC BREAK: DefaultRepositoryFactory is now final
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Please implement the ``Doctrine\ORM\Repository\RepositoryFactory`` interface instead of extending
the ``Doctrine\ORM\Repository\DefaultRepositoryFactory``.
BC BREAK: New object expression DQL queries now respects user provided aliasing and not return consumed fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When executing DQL queries with new object expressions, instead of returning
DTOs numerically indexes, it will now respect user provided aliases. Consider
the following query:
::
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
Previously, your result would be similar to this:
::
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},
),
...
)
From now on, the resultset will look like this:
::
array(
0=>array(
'user'=>{UserDTO object},
'address'=>{AddressDTO object},
'addressId'=>{a.id scalar}
),
...
)

View File

@ -92,6 +92,11 @@ Tutorials
* :doc:`Override Field/Association Mappings In Subclasses <tutorials/override-field-association-mappings-in-subclasses>`
* :doc:`Embeddables <tutorials/embeddables>`
Changelogs
----------
* :doc:`Migration to 2.5 <changelog/migration_2_5>`
Cookbook
--------