PHP objects in MongoDB with Doctrine

An is equivalent to an Object-Relational Mapper, but with its targets are documents of a NoSQL database instead of table rows. No one said that a Data Mapper must always rely on a relational database as its back end.

In the PHP world, probably the Doctrine ODM for MongoDB is the most successful. This followes to the opularity of Mongo, which is a transitional product between SQL and NoSQL, still based on some relational concepts like queries.

Lots of features

The Doctrine Mongo ODM supports mapping of objects via annotations placed in the class source code, or via external XML or YAML files. In this and in many aspects it is based on the same concepts as the Doctrine ORM: it features a Facade DocumentManager object and a Unit Of Work that batches changes to the database when objects are added to it.

Moreover, two different types of relationships between objects are supported: references and embedded documents. The first is the equivalent of the classical pointer to another row which ORM always transform object references into; the second actually stores an object inside another one, like you would do with a Value Object. Thus, at least in Doctrine's case, it is easier to map objects as documents that as rows.

As said before, the ODM borrows some concepts and classes from the ORM, in particular from the Doctrine\Common package which features a standard collection class. So if you have built objects mapped with the Doctrine ORM nothing changes for persisting them in MongoDB, except for the mapping metadata itself.

Advantages

If an ORM is sometimes a leaky abstraction, an ODM probably becomes an issue less often. It has less overhead than an ORM, since there is no schema to define and the ability to embed objects means there should be no compromises between the object model and the capabilities of the database. How many times we have renounced introducing a potential Value Object because of the difficulty in persisting it?

The case for an ODM over a plain Mongo connection object is easy to make: you will still be able to use objects with proper encapsulation (like private fields and associations) and behavior (many methods) instead of extracting just a JSON package from your database.

Installation

A prerequisite for the ODM is the presence of the mongo extension, that can be installed via pecl.

After having verified the extension is present, grab the Doctrine\Common as the 2.2.x package, and a zip of the doctrine-mongodb and doctrine-mongodb-odm projects from Github. Decompress everything into a Doctrine/ folder.

After having setup autoloading for classes in Doctrine\, use this bootstrap to get a DocumentManager (the equivalent of EntityManager):

use Doctrine\Common\Annotations\AnnotationReader,
    Doctrine\ODM\MongoDB\DocumentManager,
    Doctrine\MongoDB\Connection,
    Doctrine\ODM\MongoDB\Configuration,
    Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver;

    private function getADm()
    {
        $config = new Configuration();
        $config->setProxyDir(__DIR__ . '/mongocache');
        $config->setProxyNamespace('MongoProxies');

        $config->setDefaultDB('test');
        $config->setHydratorDir(__DIR__ . '/mongocache');
        $config->setHydratorNamespace('MongoHydrators');

        $reader = new AnnotationReader();
        $config->setMetadataDriverImpl(new AnnotationDriver($reader, __DIR__ . '/Documents'));

        return DocumentManager::create(new Connection(), $config);
    }


You will be able to call persist() and flush() on the DocumentManager, along with a set of other methods for querying like find() and getRepository().

Integration with an ORM

We are researching a solution for versioning objects mapped with the Doctrine ORM. Doing this with a version column would be invasive, and also strange where multiple objects are involved (do you version just the root of an object graph? Duplicate the other ones when they change? How can you detect that?) The idea is taking a snapshot and putting it in a read only MongoDB instance, where all previous versions can be retrieved later for auditing (business reasons).

This has been verified to be technically possible: the DocumentManager and EntityManager are totally separate object graphs, so they won't clash with each other. The only point of conflict is the annotations of model classes, since both use different version of @Id, and can see the other's annotation like @Entity and @Document while parsing.

This can be solved by using aliases for all the annotations, using their parent namespace basename as a prefix:

<?php
namespace Documents;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
// class name example: Doctrine\ODM\MongoDB\Mapping\Annotations\Id
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;

/**
 * @ODM\Document
 * @Entity
 */
class Car
{
    /**
     * @ODM\Id(strategy="AUTO")
     */
    private $document_id;

    /**
     * @Id @Column(type="integer") @GeneratedValue
     */
    private $id;

    /**
     * @ODM\Field
     * @Column
     */
    private $model;

    public function __construct($model)
    {
        $this->model = $model;
    }

    public function __toString()
    {
        return "Car #$this->document_id: $this->id, $this->model";
    }
}

This make us able to save a copy of an ORM object into Mongo:

        $car = new Car('Ford');
        $this->em->persist($car);
        $this->em->flush();
        $this->dm->persist($car);
        $this->dm->flush();
        var_dump($car->__toString());
        $this->assertTrue(strlen($car->__toString()) > 20);

The output produces by this test is:

.string(38) "Car #4f61a8322f762f1121000000: 3, Ford"

When retrieving the object, one of the two ids will be null as it is ignored by the ORM or ODM. I am not using the same field because I want to store multiple copies of a row, so it's id alone won't be unique. If you're interested, checkout my hack on Github.

It contains the running example presented in this post. Remember to create the relational schema with:

$ php doctrine.php orm:schema-tool:create

before running the test with

phpunit --bootstrap bootstrap.php DoubleMappingTest.php

MongoDB won't need the schema setup, of course. There are still some use cases to test, like the behavior in the presence of proxies, but it seems that non-invasive approach of Data Mappers like Doctrine 2 is paying off: try mapping an object in multiple database with Active Records.

 

 

 

 

Top