mongodb-odm-docs-dash/build.docset/Contents/Resources/Documents/_sources/reference/migrating-schemas.rst.txt
2017-12-01 19:35:11 -08:00

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/