Multiple levels of Embedded Documents in MongoDB

One of the greatest things about MongoDB is the fact that it is schema-less. It makes for a very flexible domain model persistence layer. For example it is possible to have multiple levels of embedded documents. A useful example might be where you have many profiles and each profile has many addresses. In the Doctrine MongoDB ODM mapping this is trivial.

First create your top level User document:

<?php

/** @Document(collection="users") */
class User
{
    /** @Id */
    private $id;

    /** @String */
    private $username;

    /** @EmbedMany(targetDocument="Profile") */
    private $profiles = array();

    public function setUsername($username)
    {
        $this->username = $username;
    }

    public function addProfile(Profile $profile)
    {
        $this->profiles[] = $profile;
    }
}

As you can see we embed another document class named Profile so lets define that as an embedded document:

<?php

/** @EmbeddedDocument */
class Profile
{
    /** @String */
    private $name;

    /** @EmbedMany(targetDocument="Address") */
    private $addresses = array();

    public function setName($name)
    {
        $this->name = $name;
    }

    public function addAddress(Address $address)
    {
        $this->addresses[] = $address;
    }
}

Finally, we’ve embedded a document in Profile named Address so lets define it:

<?php

/** @EmbeddedDocument */
class Address
{
    /** @String */
    private $number;

    /** @String */
    private $street;

    /** @String */
    private $city;

    /** @String */
    private $state;

    /** @String */
    private $zipcode;

    public function setNumber($number)
    {
        $this->number = $number;
    }

    public function setStreet($street)
    {
        $this->street = $street;
    }

    public function setCity($city)
    {
        $this->city = $city;
    }

    public function setState($state)
    {
        $this->state = $state;
    }

    public function setZipcode($zipcode)
    {
        $this->zipcode = $zipcode;
    }
}

Now you can start working with the PHP objects just like you would if no persistence layer was present at all and persist the objects transparently when you are ready to have the state of the objects managed by Doctrine:

<?php

$user = new User();
$user->setUsername('jwage');

$profile = new Profile();
$profile->setName('Profile #1');

$user->addProfile($profile);

$address = new Address();
$address->setNumber('6512');
$address->setStreet('Mercomatic');
$address->setCity('Nashville');
$address->setState('Tennessee');
$address->setZipcode('37209');

$profile->addAddress($address);

$profile = new Profile();
$profile->setName('Profile #2');

$user->addProfile($profile);

$address = new Address();
$address->setNumber('475');
$address->setStreet('Buckhead Ave');
$address->setCity('Atlanta');
$address->setState('Georgia');
$address->setZipcode('30303');

$profile->addAddress($address);

$dm->persist($user);
$dm->flush();

The above would result in an array being stored in MongoDB like the following:

Array
(
    [_id] => MongoId Object
        (
        )
    [username] => jwage
    [profiles] => Array
        (
            [0] => Array
                (
                    [name] => Profile #1
                    [addresses] => Array
                        (
                            [0] => Array
                                (
                                    [number] => 6512
                                    [street] => Mercomatic
                                    [city] => Nashville
                                    [state] => Tennessee
                                    [zipcode] => 37209
                                )
                        )
                )
            [1] => Array
                (
                    [name] => Profile #2
                    [addresses] => Array
                        (
                            [0] => Array
                                (
                                    [number] => 475
                                    [street] => Buckhead Ave
                                    [city] => Atlanta
                                    [state] => Georgia
                                    [zipcode] => 30303
                                )
                        )
                )
        )
)

We can then later retrieve the documents from MongoDB and our object domain model will be reconstructed as you have mapped it:

<?php

$user = $dm->findOne('User', array('username' => 'jwage'));

You can see the complete working script for this blog post as a gist on github.

Posted in: Doctrine, MongoDB, PHP

Tags: , , ,



6 Comments

rssComments RSS transmitTrackBack Identifier URI


Great to see that in action. Is there any built-in support for referencing the objects rather than nesting them, should that be needed?

Comment by Peter on July 27, 2010 5:27 pm


[...] This post was mentioned on Twitter by Jonathan H. Wage and pborreli, Bulat Shakirzyanov. Bulat Shakirzyanov said: RT @jwage: New blog post: Multiple levels of Embedded Documents in MongoDB http://bit.ly/9JW3BX [...]

Pingback by Tweets that mention Jonathan H. Wage » Archive » Multiple levels of Embedded Documents in MongoDB -- Topsy.com on July 27, 2010 5:29 pm


Peter, yes you can reference them instead. You can have a reference on an embedded document as well. For @ReferenceOne it loads an auto generated DocumentProxy instance and lazily loads the document data when you first access it and for @ReferenceMany it loads an uninitialized PersistentCollection and when you access the collection for the first time the contents of it are lazily loaded.

Comment by jwage on July 27, 2010 5:35 pm


great, just like we discussed ealier in the mailing list

Comment by jrollin on July 27, 2010 5:40 pm


This is nifty, however one problem.

I have a menu, with many menuitems.

/** @Document(collection=”menu”) / class Menu { /* @Id */ private $id;

/** @String */
private $name;

/** @EmbedMany(targetDocument="MenuItem") */
private $menu_items = array();

public function addMenuItem($menuItem){…} } /** @EmbeddedDocument / class MenuItem { /* @String / private $name; /* @String */ private $url;

/** @EmbedMany(targetDocument="MenuItem") */
private $menu_items = array();

public function addMenuItem($menuItem)
{
    ....
}

}

As you can see MenuItem can embed MenuItems, this is recursive and causes php to use up all the memory.

My question is, will this be fixed or is this impossible due to the implementation?

Comment by Kjaer on July 27, 2010 9:14 pm


It should work. Can you create an issue with a test on Jira? http://www.doctrine-project.org/jira

Comment by jwage on July 27, 2010 9:47 pm

addLeave a comment