220 lines
6.0 KiB
Plaintext
220 lines
6.0 KiB
Plaintext
|
Migrating Schemas
|
||
|
=================
|
||
|
|
||
|
Even though MongoDB is schemaless, introducing some kind of object mapper means
|
||
|
that your object definitions become your schema. You may have a situation where
|
||
|
you rename a property in your object model but need to load values from older
|
||
|
documents where the field is still using the former name. While you could use
|
||
|
MongoDB's `$rename`_ operator to migrate everything, sometimes a lazy migration
|
||
|
is preferable. Doctrine offers a few different methods for dealing with this
|
||
|
problem!
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
The features in this chapter were inspired by `Objectify`_, an object mapper
|
||
|
for the Google App Engine datastore. Additional information may be found in
|
||
|
the `Objectify schema migration`_ documentation.
|
||
|
|
||
|
Renaming a Field
|
||
|
----------------
|
||
|
|
||
|
Let's say you have a simple document that starts off with the following fields:
|
||
|
|
||
|
.. code-block:: php
|
||
|
|
||
|
<?php
|
||
|
|
||
|
/** @Document */
|
||
|
class Person
|
||
|
{
|
||
|
/** @Id */
|
||
|
public $id;
|
||
|
|
||
|
/** @Field(type="string") */
|
||
|
public $name;
|
||
|
}
|
||
|
|
||
|
Later on, you need rename ``name`` to ``fullName``; however, you'd like to
|
||
|
hydrate ``fullName`` from ``name`` if the new field doesn't exist.
|
||
|
|
||
|
.. code-block:: php
|
||
|
|
||
|
<?php
|
||
|
|
||
|
/** @Document */
|
||
|
class Person
|
||
|
{
|
||
|
/** @Id */
|
||
|
public $id;
|
||
|
|
||
|
/** @Field(type="string") @AlsoLoad("name") */
|
||
|
public $fullName;
|
||
|
}
|
||
|
|
||
|
When a Person is loaded, the ``fullName`` field will be populated with the value
|
||
|
of ``name`` if ``fullName`` is not found. When the Person is persisted, this
|
||
|
value will then be stored in the ``fullName`` field.
|
||
|
|
||
|
.. caution::
|
||
|
|
||
|
A caveat of this feature is that it only affects hydration. Queries will not
|
||
|
know about the rename, so a query on ``fullName`` will only match documents
|
||
|
with the new field name. You can still query using the ``name`` field to
|
||
|
find older documents. The `$or`_ query operator could be used to match both.
|
||
|
|
||
|
Transforming Data
|
||
|
-----------------
|
||
|
|
||
|
You may have a situation where you want to migrate a Person's name to separate
|
||
|
``firstName`` and ``lastName`` fields. This is also possible by specifying the
|
||
|
``@AlsoLoad`` annotation on a method, which will then be invoked immediately
|
||
|
before normal hydration.
|
||
|
|
||
|
.. code-block:: php
|
||
|
|
||
|
<?php
|
||
|
|
||
|
/** @Document @HasLifecycleCallbacks */
|
||
|
class Person
|
||
|
{
|
||
|
/** @Id */
|
||
|
public $id;
|
||
|
|
||
|
/** @Field(type="string") */
|
||
|
public $firstName;
|
||
|
|
||
|
/** @Field(type="string") */
|
||
|
public $lastName;
|
||
|
|
||
|
/** @AlsoLoad({"name", "fullName"}) */
|
||
|
public function populateFirstAndLastName($fullName)
|
||
|
{
|
||
|
list($this->firstName, $this->lastName) = explode(' ', $fullName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
The annotation is defined with one or a list of field names. During hydration,
|
||
|
these fields will be checked in order and, for each field present, the annotated
|
||
|
method will be invoked with its value as a single argument. Since the
|
||
|
``firstName`` and ``lastName`` fields are mapped, they would then be updated
|
||
|
when the Person was persisted back to MongoDB.
|
||
|
|
||
|
Unlike lifecycle callbacks, the ``@AlsoLoad`` method annotation does not require
|
||
|
the :ref:`haslifecyclecallbacks` class annotation to be present.
|
||
|
|
||
|
Moving Fields
|
||
|
-------------
|
||
|
|
||
|
Migrating your schema can be a difficult task, but Doctrine provides a few
|
||
|
different methods for dealing with it:
|
||
|
|
||
|
- **@AlsoLoad** - load values from old fields or transform data through methods
|
||
|
- **@NotSaved** - load values into fields without saving them again
|
||
|
- **@PostLoad** - execute code after all fields have been loaded
|
||
|
- **@PrePersist** - execute code before your document gets saved
|
||
|
|
||
|
Imagine you have some address-related fields on a Person document:
|
||
|
|
||
|
.. code-block:: php
|
||
|
|
||
|
<?php
|
||
|
|
||
|
/** @Document */
|
||
|
class Person
|
||
|
{
|
||
|
/** @Id */
|
||
|
public $id;
|
||
|
|
||
|
/** @Field(type="string") */
|
||
|
public $name;
|
||
|
|
||
|
/** @Field(type="string") */
|
||
|
public $street;
|
||
|
|
||
|
/** @Field(type="string") */
|
||
|
public $city;
|
||
|
}
|
||
|
|
||
|
Later on, you may want to migrate this data into an embedded Address document:
|
||
|
|
||
|
.. code-block:: php
|
||
|
|
||
|
<?php
|
||
|
|
||
|
/** @EmbeddedDocument */
|
||
|
class Address
|
||
|
{
|
||
|
/** @Field(type="string") */
|
||
|
public $street;
|
||
|
|
||
|
/** @Field(type="string") */
|
||
|
public $city;
|
||
|
|
||
|
public function __construct($street, $city)
|
||
|
{
|
||
|
$this->street = $street;
|
||
|
$this->city = $city;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @Document @HasLifecycleCallbacks */
|
||
|
class Person
|
||
|
{
|
||
|
/** @Id */
|
||
|
public $id;
|
||
|
|
||
|
/** @Field(type="string") */
|
||
|
public $name;
|
||
|
|
||
|
/** @NotSaved */
|
||
|
public $street;
|
||
|
|
||
|
/** @NotSaved */
|
||
|
public $city;
|
||
|
|
||
|
/** @EmbedOne(targetDocument="Address") */
|
||
|
public $address;
|
||
|
|
||
|
/** @PostLoad */
|
||
|
public function postLoad()
|
||
|
{
|
||
|
if ($this->street !== null || $this->city !== null)
|
||
|
{
|
||
|
$this->address = new Address($this->street, $this->city);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Person's ``street`` and ``city`` fields will be hydrated, but not saved. Once
|
||
|
the Person has loaded, the ``postLoad()`` method will be invoked and construct
|
||
|
a new Address object, which is mapped and will be persisted.
|
||
|
|
||
|
Alternatively, you could defer this migration until the Person is saved:
|
||
|
|
||
|
.. code-block:: php
|
||
|
|
||
|
<?php
|
||
|
|
||
|
/** @Document @HasLifecycleCallbacks */
|
||
|
class Person
|
||
|
{
|
||
|
// ...
|
||
|
|
||
|
/** @PrePersist */
|
||
|
public function prePersist()
|
||
|
{
|
||
|
if ($this->street !== null || $this->city !== null)
|
||
|
{
|
||
|
$this->address = new Address($this->street, $this->city);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
The :ref:`haslifecyclecallbacks` annotation must be present on the class in
|
||
|
which the method is declared for the lifecycle callback to be registered.
|
||
|
|
||
|
.. _`$rename`: https://docs.mongodb.com/manual/reference/operator/update/rename/
|
||
|
.. _`Objectify`: https://github.com/objectify/objectify
|
||
|
.. _`Objectify schema migration`: https://github.com/objectify/objectify/wiki/SchemaMigration
|
||
|
.. _`$or`: https://docs.mongodb.com/manual/reference/operator/query/or/
|