Doctrine MongoDB ODM Schema Migrations post

Posted on 2010-07-30 by jwage


MongoDB is a schema-less database so as your domain model changes in Doctrine, you’ll have newer documents with different fields than older documents. Since we don’t have a way to rename a field internally in MongoDB, yet, the only other option is to fetch all the documents and rename it in your application and update the document. This could take a really long time depending on how big your database is and will require downtime.

Doctrine provides ways for you to “eventually” migrate all your documents at application run-time. You have several options for working with database schema changes and this blog post will try and demonstrate them!

Renaming a Field

Imagine you have a document in your domain named Person and it looked like this:

/** @Document */
class Person
{
    /** @Id */
    public $id;

    /** @String */
    public $name;
}

Then imagine later you decide you want to rename $name to $fullName like this:

/** @Document */
class Person
{
    /** @Id */
    public $id;

    /** @String */
    public $fullName;
}

All documents from now on will be created with a fullName property but you’ll still have old documents with the name field. You can use the @AlsoLoad annotation here to also load another fields value in that property if it exists:

/** @Document */
class Person
{
    /** @Id */
    public $id;

    /**
     * @String
     * @AlsoLoad("name")
     */
    public $fullName;
}

Transforming Data

Another situation might be you want to load the name and fullName fields in to individual first and last name fields. We can handle this using the @AlsoLoad annotation on a method:

/** @Document */
class Person
{
    /** @Id */
    public $id;

    /** @String */
    public $firstName;

    /** @String */
    public $lastName;

    /** @AlsoLoad({"name", "fullName"}) */
    public function populateFirstAndLastName($fullName)
    {
        $e = explode(' ', $fullName);
        $this->firstName = $e[0];
        $this->lastName = $e[1];
    }
}

So when a document has a field named name, or fullName it will execute the populateFirstAndLastName() method to handle the change when the document is loaded.

Moving Fields

You also have a few other options for dealing with changes in your model:

  • @PostLoad - execute code after all fields have been loaded.
  • @PrePersist - execute code before your document gets saved.
  • @NotSaved - load values into fields without saving them again.

Imagine you have some address fields on a Person document:

/** @Document(collection="people") */
class Person
{
    /** @Id */
    public $id;

    /** @String */
    public $name;

    /** @String */
    public $street;

    /** @String */
    public $city;
}

Then later you want to store a persons address in another object as an embedded document:

/** @EmbeddedDocument */
class Address
{
    /** @String */
    public $street;

    /** @String */
    public $city;

    public function __construct($street, $city)
    {
        $this->street = $street;
        $this->city = $city;
    }
}

/**
 * @Document(collection="people")
 * @HasLifecycleCallbacks
 */
class Person
{
    /** @Id */
    public $id;

    /** @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);
        }
    }
}

The above will change the document each time it is loaded. If you want to change it permanently in the database you can do it when the document is being updated:

/**
  * @Document(collection="people")
  * @HasLifecycleCallbacks
  */
class Person
{
    // ...

    /** @PreUpdate */
    public function preUpdate()
    {
        if ($this->street !== null || $this->city !== null)
        {
            $this->address = new Address($this->street, $this->city);
        }
    }
}

Categories: articles