ZendCon2010 Doctrine MongoDB ODM

Post on 17-May-2015

4.582 views 3 download

Tags:

Transcript of ZendCon2010 Doctrine MongoDB ODM

Jonathan H. Wage OpenSky

Doctrine MongoDBObject Document Mapper

+

Who am I?Jonathan H. Wage

PHP Developer for over 10 yearsSymfony ContributorDoctrine ContributorPublished AuthorBusiness OwnerNashville, TN Resident

http://www.twitter.com/jwagehttp://www.facebook.com/jwage

I work at

What is OpenSky?“a social commerce platform”

Based in New York and is a major opensource software advocate

http://www.shopopensky.com

OpenSky TechnologiesPHP 5.3.2Apache2Symfony2Doctrine2jQuerymule, stomp, hornetqMongoDBnginxvarnish

What is Doctrine?- Open Source PHP Project started in

2006

- Specializes in database functionality

Doctrine Libraries- Database Abstraction Layer

- Database Migrations

- Object Relational Mapper

- MongoDB Object Document Manager

- CouchDB Object Document Manager

Who is on the team?

• Roman S. Borschel

• Guilherme Blanco

• Benjamin Eberlei

• Bulat Shakirzyanov

• Jonathan H. Wage

Project History- First commit April 13th 2006

- First stable version finished and Released September 1st 2008

- One of the first ORM implementations for PHP

- 1.0 is First LTS(long term support) release. Maintained until March 1st 2010

- Integrated with many popular frameworks: Symfony, Zend Framework, Code Igniter

What is MongoDB?

http://en.wikipedia.org/wiki/MongoDB

“MongoDB (from "humongous") is an open source,scalable, high-performance, schema free, document-oriented database written in the C++ programming language.

The goal of MongoDB is to bridge the gap between key-value stores - which are fast and highly scalable - and traditional RDBMS systems - which provide rich queries and deep functionality. It is designed for problems that aren't easily solved by traditional RDBMSs, for example if databases span many servers.”

Database Terminology

RDBMS MongoDBDatabase Database

Table Collection

Row Document

Mapper Terminology

ORM ODMDatabase Database

Repository Repository

Entity Document

Using MongoDB in PHPDownload and run MongoDB Server

http://www.mongodb.org/display/DOCS/Downloads

Install Mongo PECL extensionhttp://www.php.net/manual/en/mongo.installation.php

$ /path/to/mongodb/bin/mongod

$ pecl install mongo

ConnectingCreate new connection instances withthe Mongo class:

$mongo = new Mongo('mongodb://localhost');

Selecting DatabasesMongoDB intances can be selectedusing the selectDB() method:

$mongo = new Mongo('mongodb://localhost');$db = $mongo->selectDB('dbname');

Selecting CollectionsMongoCollection instances can beselected using the selectCollection()method:$mongo = new Mongo('mongodb://localhost');$db = $mongo->selectDB('dbname');$coll = $db->selectCollection('users');

Inserting DocumentsInsert a regular PHP array:

$user = array( 'username' => 'jwage', 'password' => md5('changeme') 'active' => true);$coll->insert($user);

echo $user['_id'];

Finding a DocumentWe can easily find the document we justinserted using the findOne() method:

$user = $coll->findOne(array('username' => 'jwage'));

Updating a DocumentYou can find a document, change it andsave the entire document:

$user = $coll->findOne(array('username' => 'jwage'));$user['username'] = 'jonwage';$user['password'] = md5('newpassword');$coll->save($user);

Atomic UpdatesThe following is faster and safer:

$coll->update(array( 'username' => 'jwage'), array( '$set' => array( 'username' => 'jonwage', 'password' => md5('newpassword') )));

Atomic Updates$inc - increments field by the number value$set - sets field to value$unset - deletes a given field$push - appends value to an array$pushAll - appends multiple values to an array$addToSet - appends multiple new values to an array$pop - removes the last element in an array$pull - removes all occurrences of a value from an array$pullAll - removes all occurrences of multiple values from an array

Atomic ExamplesIncrement num_comments with $inc:

// $inc a blog posts number of comments by 1$db->posts->update($id, array('$inc' => array('num_comments' => 1)))

Atomic ExamplesPush new comment on to end of array:

// $push a new comment on to a blog posts comments$db->posts->update($id, array('$push' => array( 'comments' => array('Hi how are you?'))));

Atomic ExamplesSet an individual field value:

// $set a lock on a blog post$db->posts->update($id, array('$set' => array('locked' => 1)));

Atomic ExamplesUnset a field from a document:

// $unset a lock on a blog post$db->posts->update($id, array('$unset' => array('locked' => 1)));

Removing DocumentsYou can remove documents using theremove() method:

$coll->remove(array('username' => 'jwage'));

+

Doctrine + MongoDB

=

What is it?

- Layer on top of Mongo PECL extension

- Work with objects instead of arrays

- Create rich OO PHP domain and persist transparently using Doctrine

What does it do?- Maps PHP classes to MongoDB

- Manages the state of objects

- Tracks changes and persists them

- Constructs regular PHP objects from MongoDB data

How does it do it?- Implements UnitOfWork

http://martinfowler.com/eaaCatalog/unitOfWork.html

- Tracks changes to managed objects by maintaining a copy of its original data

- Computes changesets and persists them transparently

Easier to build than ORM- Less work to convert objects to arrays

for persistence to MongoDB

- Less work to convert the data in the database to objects since it is already stored in a object-like structure instead of flat like in a RDBMS

ArchitectureDocuments - Lightweight persistent domain object - Regular PHP class - Does not extend any base Doctrine class - Cannot be final or contain final methods - Any two documents in a hierarchy of classes must not

have a mapped property with the same name - Supports inheritance, polymorphic associations and

polymorphic queries. - Both abstract and concrete classes can be documents - Documents may extend non-document classes as well

as document classes, and non-document classes may extend document classes

Architecture- No more base class required

- Values stored in object properties

- Doctrine is transparentnamespace Documents;

class User{ private $id; private $name;}

ArchitectureThe DocumentManager

- Central access point to the ODM functionality provided by Doctrine. API is used to manage the persistence of your objects and to query for persistent objects.

- Employes transactional write behind strategy that delays the execution of queries in order to execute them in the most efficient way

- Internally a DocumentManager uses a UnitOfWork to keep track of your objects

Managing DocumentsTo manage documents with Doctrineyou need a DocumentManager:

$config = new \Doctrine\ODM\MongoDB\Configuration();$config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache);$driverImpl = $config->newDefaultAnnotationDriver(array(__DIR__."/Documents"));$config->setMetadataDriverImpl($driverImpl);

$config->setProxyDir(__DIR__ . '/Proxies');$config->setProxyNamespace('Proxies');

$dm = \Doctrine\ODM\MongoDB\DocumentManager::create($mongo, $config);

DocumentManagerThe DocumentManager is the centralplace for managing the state of PHPobjects. It has methods like:

persist($document)find()findOne()refresh($document)remove($document)detach($document)merge($document)flush()

Document ClassesA Doctrine MongoDB Document is just a regular old PHP object:

class User{ private $id; private $username; private $password;

public function getId() { return $this->id; }

public function getUsername() { return $this->username; }

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

public function getPassword() { return $this->password; }

public function setPassword($password) { $this->password = md5($password); }}

Mapping Information- Annotations

- YAML

- XML

- PHP

/** @Document */class User{ /** @Id */ private $id; /** @String */ private $username; /** @String */ private $password; // ...}

Inserting Documents

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

$dm->persist($user);$dm->flush(); // inserts document

$users->batchInsert(array( array( 'username' => 'jwage', 'password' => 'changeme' )));

Finding Documents

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

Updating Documents

Always uses atomic operators

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

$user->setUsername('jonwage');$user->setPassword('newpassword');

$dm->flush(); // updates document

$coll->update( array('_id' => 'theid'), array('$set' => array( 'username' => 'jonwage', 'password' => '5e9d11a14ad1c8dd77e98ef9b53fd1ba' ));

Removing Documents

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

$dm->remove($user);$dm->flush(); // removes document

$coll->remove(array('_id' => 'theid'));

Query BuilderNormally queries are just arrays thatlook like the following:

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

Problems- Not OO

- Not re-usable

- Can become difficult to read as query gets more complex

Query BuilderConstruct a new query instance andstart adding to it:

$q = $dm->createQuery('User') ->field('username')->equals('jwage');

Query BuilderFind posts within a range of dates:

$q = $dm->createQuery('Post') ->field('createdAt')->range($startDate, $endDate);

Query BuilderUpdating documents:

$q = $dm->createQuery('User') ->update() ->field('username')->set('jonwage') ->field('password')->set('newpassword') ->field('username')->equals('jwage');

Query BuilderRemoving documents:

$q = $dm->createQuery('User') ->remove() ->field('username')->equals('jwage');

Query BuilderExecuting queries:

// Execute and get the first result$user = $q->getSingleResult();

// Execute and return an array of results$users = $q->execute();

// Execute update or remove query$q->execute();

// Iterate over cursorforeach ($q->iterate() as $user) { }

Embedded DocumentsInsert document with an embeddeddocument:

$user = array( 'username' => 'jwage', 'password' => 'changeme' 'addresses' => array( array( 'address1' => '6512 Mercomatic Ct.', // ... ) ));$db->users->insert($user);

Embedded DocumentsPush a new address on to the end ofthe addresses array:

$db->users->update($user['_id'], array('$push' => array( 'addresses' => array( 'address1' => 'New address' )

Embedded MappingMap an embedded document using@EmbedOne and @EmbedMany:

class User{ /** @Id */ public $id;

/** @String */ public $name;

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

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

/** @String */ public $city;

/** @String */ public $state;

/** @String */ public $zipcode;

Embedded Mapping

$user = new User();$user->name = 'Jonathan H. Wage';

$address = new Address();$address->address = '6512 Mercomatic Ct';$address->city = 'Nashville';$address->state = 'Tennessee';$address->zipcode = '37209';$user->addresses[] = $address;

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

Persisting

$users = array( array( 'name' => 'Jonathan H. Wage', 'addresses' => array( array( 'address' => '6512 Mercomatic Ct', 'city' => 'Nashville', 'state' => 'Tennesseee', 'zipcode' => '37209' ) ) ));$coll->batchInsert($users);

Updating Embedded Documents

$user = $dm->findOne('User', array('name' => 'Jonathan H. Wage'));$user->addresses[0]->zipcode = '37205';

$coll->update( array('_id' => 'theuserid'), array('$set' => array('addresses.0.zipcode' => '37209')));

Adding new Address$user = $dm->findOne('User', array('name' => 'Jonathan H. Wage'));$address = new Address();$address->address = '475 Buckhead Ave.';$address->city = 'Atlanta';$address->state = 'Georgia';$address->zipcode = '30305';$user->addresses[] = $address;$dm->flush();

$coll->update( array('_id' => 'theuserid'), array('$pushAll' => array( 'addresses' => array( array( 'address' => '475 Buckhead Ave.', 'city' => 'Atlanta', 'state' => 'Georgia', 'zipcode' => '30305' ) ) )));

Unsetting Properties

unset($user->addresses[0]->zipcode);$dm->flush();

$coll->update( array('_id' => 'theuserid'), array( '$unset' => array( 'addresses.0.zipcode' => 1 ) ));

ReferencesReference documents from withinanother document by storing anembedded document with the followingproperties:

$ref - referenced collection$id - value of _id for the object referenced$db - referenced database

References- No JOIN syntax like in a relational DB

- References are resolved in app code

- Store one or many references

Reference Mapping

/** @Document */class Organization{ /** @Id */ private $id;

/** @String */ private $name;

/** @ReferenceMany(targetDocument="User") */ private $users = array();

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

public function addUser(User $user) { $this->users[] = $user; }}

Reference Mapping

/** @Document */class User{ /** @Id */ private $id;

/** @String */ private $name;

/** @ReferenceOne(targetDocument="Organization") */ private $organization;

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

public function setOrganization(Organization $organization) { $this->organization = $organization; $organization->addUser($this); }}

Persisting

$organization = new Organization();$organization->setName('Sensio Labs');

$user = new User();$user->setName('Jonathan H. Wage');$user->setOrganization($organization);

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

Inserted OrganizationResulting organization has a reference to the user:

Array( [_id] => 4c86acd78ead0e8759000000 [name] => Sensio Labs [users] => Array ( [0] => Array ( [$ref] => User [$id] => 4c86acd78ead0e8759010000 [$db] => doctrine_odm_sandbox )

)

)

Inserted UserResulting user has a reference to the organization:

Array( [_id] => 4c86acd78ead0e8759010000 [name] => Jonathan H. Wage [organization] => Array ( [$ref] => Organization [$id] => 4c86acd78ead0e8759000000 [$db] => doctrine_odm_sandbox )

)

Working with ReferencesA single reference is represented by aproxy object:

$user = $dm->find('User', array('name' => 'Jonathan H. Wage'));

// instance of uninitialized OrganizationProxy$organization = $user->getOrganization();

// calling getter or setter for uninitialized proxy// queries the database and initialized the proxy document

// query invoked, organization data loaded and doc initializedecho $organization->getName();

Working with ReferencesWhat does a proxy look like?

class OrganizationProxy extends Organization{ private function initialize() { // ... }

public function getName() { $this->initialize(); return parent::getName(); }}

Working with ReferencesA many reference is represented by anuninitialized PersistentCollection:

$organization = $dm->find('Organization', array('name' => 'Sensio Labs'));$users = $organization->getUsers(); // uninitialized collection

// Queries database for users and initializes collectionforeach ($users as $user){ // ...}

Document Query LanguageSQLike grammar for querying DoctrineMongoDB ODM for document objects.

find all FROM BlogPost

QueryLanguage ::= FindQuery | InsertQuery | UpdateQuery | RemoveQuery

FindQuery ::= FindClause [WhereClause] [MapClause] [ReduceClause] [SortClause] [LimitClause] [SkipClause]FindClause ::= "FIND" all | SelectField {"," SelectField}SelectField ::= DocumentFieldNameSortClause ::= SortClauseField {"," SortClauseField}SortClauseField ::= DocumentFieldName "ASC | DESC"LimitClause ::= "LIMIT" LimitIntegerSkipClause ::= "SKIP" SkipIntegerMapClause ::= "MAP" MapFunctionReduceClause ::= "REDUCE" ReduceFunction

DocumentFieldName ::= DocumentFieldName | EmbeddedDocument "." {"." DocumentFieldName}WhereClause ::= "WHERE" WhereClausePart {"AND" WhereClausePart}WhereClausePart ::= ["all", "not"] DocumentFieldName WhereClauseExpression ValueWhereClauseExpression ::= "=" | "!=" | ">=" | "<=" | ">" | "<" | "in" "notIn" | "all" | "size" | "exists" | "type"Value ::= LiteralValue | JsonObject | JsonArray

UpdateQuery ::= UpdateClause [WhereClause]UpdateClause ::= [SetExpression], [UnsetExpression], [IncrementExpression], [PushExpression], [PushAllExpression], [PullExpression], [PullAllExpression], [AddToSetExpression], [AddManyToSetExpression], [PopFirstExpression], [PopLastExpression]SetExpression ::= "SET" DocumentFieldName "=" Value {"," SetExpression}UnsetExpression ::= "UNSET" DocumentFieldName {"," UnsetExpression}IncrementExpression ::= "INC" DocumentFieldName "=" IncrementInteger {"," IncrementExpression}PushExpression ::= "PUSH" DocumentFieldName Value {"," PushExpression}PushAllExpression ::= "PUSHALL" DocumentFieldName Value {"," PushAllExpression}PullExpression ::= "PULL" DocumentFieldName Value {"," PullExpression}PullAllExpression ::= "PULLALL" DocumentFieldName Value {"," PullAllExpression}AddToSetExpression ::= "ADDTOSET" DocumentFieldName Value {"," AddToSetExpression}AddManyToSetExpression ::= "ADDMANYTOSET" DocumentFieldName Value {"," AddManyToSetExpression}PopFirstExpression ::= "POPFIRST" DocumentFieldName {"," PopFirstExpression}PopLastExpression ::= "POPLAST" DocumentFieldName {"," PopLastExpression}

InsertQuery ::= InsertClause InsertSetClause {"," InsertSetClause}InsertSetClause ::= DocumentFieldName "=" Value

RemoveQuery ::= RemoveClause [WhereClause]RemoveClause ::= "REMOVE" DocumentClassName

BNF

Creating QueriesFrom the DocumentManager you canexecute DQL queries using the query()method:

$users = $dm->query('find all FROM User');

$users = $db->user->find();

Selecting Fields

$users = $dm->query('find username FROM User');

$users = $db->user->find(null, array('username'));

Paging Results

$users = $dm->query('find all FROM User limit 30 skip 30');

$users = $db->user->find();$users->limit(30);$users->skip(30);

Selecting Embedded Slice

$post = $dm->query('find title, comments limit 20 skip 10 FROM BlogPost WHERE id = ?', array($id));

$users = $db->user->find( array('_id' => $id), array('title', 'comments' => array( '$slice' => array(20, 10) )));

More Examples

// Find all posts greater than or equal to a date$posts = $dm->query('find all FROM BlogPost WHERE createdAt >= ?', array($date));

// Find all posts sorted by created at desc$posts = $dm->query('find all FROM BlogPost sort createdAt desc');

// Update user$dm->query('update Documents\User set password = ? where username = ?', array('newpassword', 'jwage'));

EventsDoctrine triggers events throughout thelifecycle of objects it manages:

- preRemove- postRemove- prePersist- postPersist- preUpdate- postUpdate- preLoad- postLoad

Maintaining Timestamps/** * @Document * @HasLifecycleCallbacks */class BlogPost{ /** @Id */ public $id; /** @Date */ public $createdAt; /** @Date */ public $updatedAt;

/** @PreUpdate */ public function prePersist() { $this->createdAt = new DateTime(); }

/** @PreUpdate */ public function preUpdate() { $this->updatedAt = new DateTime(); }}

Migrating ChangesSometimes your documents change and you will need to migrate your data.

Sample ScenarioOur original document looks like this:

/** * @Document */class User{ /** @Id */ public $id; /** @String */ public $name;}

Renaming FieldsLater we want to store first and lastnames in separate fields:

/** * @Document */class User{ /** @Id */ public $id; /** @String */ public $firstName; /** @String */ public $lastName;}

Renaming FieldsHandle documents with only a name:

/** * @Document * @HasLifecycleCallbacks */class User{ /** @Id */ public $id; /** @String */ public $firstName; /** @String */ public $lastName;

/** @PreLoad */ public function preLoad(array &$data) { if (isset($data['name'])) { $e = explode(' ', $data['name']); unset($data['name']); $data['firstName'] = $e[0]; $data['lastName'] = $e[1]; } }}

Why use an object mapper?

Encapsulate your domain in an object oriented interface

Encapsulation

The organization of your domain logic in an OO way improved maintainability

Maintainability

Keeping a clean OO domain model makes your business logic easily testable for improved stability

Testability

Write portable and thin application controller code and fat models.

Portability

Questions?

- http://www.twitter.com/jwage- http://www.facebook.com/jwage- http://www.jwage.com

OpenSky is hiring! Inquire via e-mail at jwage@theopenskyproject.com or in person after this presentation!