220 lines
6.0 KiB
ReStructuredText
220 lines
6.0 KiB
ReStructuredText
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/
|