# DOCTRINE # Projects MongoDB Object Document Mapper Annotations, Cache, PHPCS (standards) PHPCR ODM ¿?¿?¿?¿? Collections Refelction Instantiator # MongoDB ODM PHP Doctrine MongoDB Object Document Mapper (ODM) provides transpare per MongoDB 1. Gettint Started 2. Mapping Objects onto a Database 3. Working with Object 4. Adavanced Topics 5. Cookbook ## 1. Getting Started #### 1.a Getting Started Doctrine: Project that aims to handle the persistende of your domain model in a non-interfering way. You have two PHP classes User and BlogPost, to make them persistent you have to with them some mapping information. ```php use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @ODM\Document */ class User { /** @ODM\Id */ private $id; /** @ODM\Field(type="string") */ private $name; /** @ODM\ReferenceMany(targetDocument="BlogPost", cascade="all") */ private $posts = array(); //.... } //_------------------- // create user $user = new User(); $user->setName('Bulat S.'); $user->setEmail('email@example.com'); // tell Doctrine 2 to save $user on the next flush() $dm->persist($user); // create blog post $post = new BlogPost(); $post->setTitle('My First Blog Post'); $post->setBody('MongoDB + Doctrine 2 ODM = awesomeness!'); $post->setCreatedAt(new DateTime()); $user->addPost($post); // store everything to MongoDB $dm->flush(); ``` #### 1.b Introduction Doctrine ODM is build fro PHP 5.3+ ##### Features - Transparent persistence. - Map one or many embedded documents. - Map one or many referenced documents. - Create references between documents in different databases. - Map documents with Annotations, XML, YAML or plain old PHP code. - Documents can be stored on the MongoGridFS. - Collection per class(concrete) and single collection inheritance supported. - Map your Doctrine 2 ORM Entities to the ODM and use mixed data stores. - Inserts are performed using MongoCollection::batchInsert() - Updates are performed using atomic operators. If we update a property and call `->flush()` again we'll get an efficient update query using the atomic operators:¿? ##### Setup ```bash composer require "doctrine/mongodb-odm" ``` ```php use Doctrine\MongoDB\Connection; use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver; if ( ! file_exists($file = __DIR__.'/vendor/autoload.php')) { throw new RuntimeException('Install dependencies to run this script.'); } $loader = require_once $file; // namespace to match the classes $loader->add('Documents', __DIR__); $connection = new Connection(); $config = new Configuration(); $config->setProxyDir(__DIR__ . '/Proxies'); $config->setProxyNamespace('Proxies'); $config->setHydratorDir(__DIR__ . '/Hydrators'); $config->setHydratorNamespace('Hydrators'); $config->setDefaultDB('doctrine_odm'); // Set annotations for mappings $config->setMetadataDriverImpl(AnnotationDriver::create(__DIR__ . '/Documents')); AnnotationDriver::registerAnnotationClasses(); $dm = DocumentManager::create($connection, $config); ``` ##### PHP 7 Since the legacy driver (referred to as `ext-mongo`) is not available on PHP 7, you will need the new driver (`ext-mongodb`) installed and use a polyfill to provide the API of the legacy driver. ```bash composer config "platform.ext-mongo" "1.6.16" && composer require "alcaeus/mongo-php-adapter" ``` #### 1.c Architecture ##### Documents A Document is a persistent domain object, a PHP class following these restrictions: - NOT final class or final methods - Porperties private or protected to make lazu loading working - Do not implement __clone or __wakeup - Inherint Documents cant have properties with the same name ----- - __contruct is only invoked when you call new - Documents support inheritance, polymorphic associations and polypmorphic queries. Abstract and concrete classes. ##### Document states - NEW: Document instance has no persistent identity, not associated with DocumentManager and UniOfWork - MANAGED: Document instance is an instance with persistent identity associated with a DocumentManager an whose persistence is managed - DETACHED: Document instance is an instance with a persistent identity that is not (or no longer) associated with a DocumentManager and a UnitOfWork. - REMOVED: document instance is an instance with a persistent identity, associated with a DocumentManager, that will be removed from the database upon transaction commit. ##### Persistent fields ##### Serializing documents ##### The DocumentManager ##### Transactional write-behind ##### The unit of work ## 2. Mapping objects onto a Database ### 2.1 Basic Reference #### 2.1.1 Objects and Fields This chapter explains the basic mapping of objects and properties. Mapping of references and embedded documents will be covered in the next chapter "Reference Mapping". ##### Mapping Drivers - Docblock Annotations, XML, YAML, PHP Code ##### Docblock Annotations The Doctrine MongoDB ODM defines its own set of docblock annotations for supplying object document mapping metadata. ##### Persistent classes In order to mark a class for object-relational persistence it needs to be designated as a document. This can be done through the `@Document` marker annotation. ##### Doctrine Mapping Types bin, bin_bytearray, bin_custom, bin_func, bin_md5, bin_uuid, boolean int, float string timestamp (string to MongoTimestamp), date collection (array to MongoDB array) hash (associative array to MongoDB object) custom_id, id(string to MongoId by default), key, object_id raw (any type) ##### Property Mapping ###### Identifiers /** @Id */ You can configure custom ID strategies if you don't want to use the default MongoId. ###### Fields /** @Field(type="string") */ ###### Custom Mapping Types ##### Multiple document Types in a Collection ```php find('User', $user->id); $posts = $user->getPosts(); The above will execute a query like the following to lazily load the collection of posts to iterate over: db.BlogPost.find( { 'user.$id' : user.id } ) ``` Remember that the inverse side, the side which specified `mappedBy` is immutable and any changes to the state of the reference will not be persisted. ##### OneToOne When specifying inverse one-to-one relationships the referenced document is loaded directly when the owning document is hydrated instead of using a proxy. In the example above, loading a `Customer` object from the database would also cause the corresponding `Cart` to be loaded. This can cause performance issues when loading many `Customer` objects at once. ##### Self-Referencing Many to Many #### 2.1.4 Complex references Sometimes you may want to access related documents using custom criteria or from the inverse side of a relationship. - `criteria` - Query criteria to apply to the cursor. - `repositoryMethod` - The repository method used to create the cursor. - `sort` - Sort criteria for the cursor. - `skip` - Skip offset to apply to the cursor. - `limit` - Limit to apply to the cursor. - #### 2.1.5 Indexes Working with indexes in the MongoDB ODM is pretty straight forward. You can have multiple indexes, they can consist of multiple fields, they can be unique and you can give them an order. In this chapter we'll show you examples of indexes using annotations. #### 2.1.6 Inheritance ### 2.2 More #### 2.2.1 Embedded Mapping Embed One, Embed Many All operations on embedded documents are automatically cascaded. #### 2.2.2 Trees // Ver Mongo #### 2.2.3 GridFS #### 2.2.4 aions rence ## 3 Working with Objects ### 3.1 Basic Reference #### 3.1.1 Documents ##### Understanding DM and UoW **Unit of Work**: is similar to an object-level transaction. Is created when a DM is created or after `DocumentManager#flush()`. A Unit of Work is committed (and a new one started) by invoking `DocumentManager#flush()`. Can be manually closed by calling `DocumentManager#close()`. Any changes to objects within this Unit of Work that have not yet been persisted are lost. Size of UoW: Number of managed documents at a particular point in time. ```php $uowSize = $dm->getUnitOfWork()->size(); ``` This size affects the performance of flush() operations. ```php $uow = $dm->getUnitOfWork(); // modifying it is not recommended ``` ##### Persisting `DocumentManager#persist($document)` -> document becomes MANAGED ; persistence is from now on managed by an DocumentManager Will be synchronized with DB when `DocumentManager#flush()` is invoked. Doctrine applies a strategy called "transactional write-behind", which means that it will delay most operations until `DocumentManager#flush()` is invoked which will then issue all necessary queries to synchronize your objects with the database in the most efficient way. The document identifier is generated during `persist` if not previously specified. Users cannot rely on a document identifier being available during the `prePersist` event. ##### Removing SAME for REMOVED state ##### Detaching and Merging ¿? ##### Transitive persistence Each reference to another document or a collection of documents can be configured to automatically cascade certain operations. By default, no operations are cascaded. ##### Querying $user = $dm->find('User', $id); // by primary key $users = $dm->getRepository('User')->findBy(array('age' => 20)); //By simple conditions // A single user by its nickname (__call magic) $user = $dm->getRepository('User')->findOneByNickname('romanb'); by lazy loading Whenever you have a managed document instance at hand, you can traverse and use any associations of that document as if they were in-memory already. Doctrine will automatically load the associated objects on demand through the concept of lazy-loading. $qb = $dm->createQueryBuilder('User') ->field('age')->range(20, 30); // by qb $group = $dm->find('Group', $id); $usersWithGroup = $dm->createQueryBuilder('User') ->field('group')->references($group) ->getQuery()->execute(); // by reference #### 3.1.2 Repositories A repository mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects. The default repository implementation provides the following methods:: find, findBy, findOneBy, findAll, matching ###### Custom repositories A custom repository allows filtering logic to be consolidated into a single class instead of spreading it throughout a project ```php /** @Document(repositoryClass="Repositories\UserRepository") */ // ------ class UserRepository extends DocumentRepository { public function findDisabled() { return $this->findBy(['disabled' => true, 'activated' => true]); } } ``` #### 3.1.1 Events Doctrine features a lightweight event system that is part of the Common package. The DocumentManager and UnitOfWork trigger several events during the life-time of their registered documents: prePersist, preLoad, onFlush These can be hooked into by two different types of event listeners: lifecycle callbacks, listeners ##### Lifecycle callback ```php /** @Document @HasLifecycleCallbacks */ class User { /** @PrePersist */ public function doStuffOnPrePersist(\Doctrine\ODM\MongoDB\Event\LifecycleEventArgs $eventArgs) { $this->createdAt = date('Y-m-d H:i:s'); } } ``` ##### Lifecycle event listeners Lifecycle event listeners are much more powerful than the simple lifecycle callbacks that are defined on the document classes. They allow to implement re-usable behaviours between different document classes, There's a different implementation for each event #### 3.1.2 Migrations Even though MongoDB is schemaless, introducing some kind of object mapper means that your object definitions become your schema. While you could use MongoDB's $rename operator to migrate everything, sometimes a lazy migration is preferable. ##### Renaming a Field You need rename `name` to `fullName`; however, you'd like to hydrate `fullName` from `name` if the new field doesn't exist. ```php /** @Field(type="string") @AlsoLoad("name") */ public $fullName; ``` You cannot query all values you have to query with and or for both properties ##### Transforming Data Complete name to separate first and lastname ```php /** @AlsoLoad({"name", "fullName"}) */ public function populateFirstAndLastName($fullName) { list($this->firstName, $this->lastName) = explode(' ', $fullName); } ``` ##### Moving fields - **@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 ### 3.2 Query Reference #### 3.2.1 Query Builder API FIND, FIND_AND_UPDATE, FIND_AND_REMOVE, INSERT, UPDATE, REMOVE, GROUP, MAP_REDUCE, DISTINCT_FIELD, GEO_LOCATION ```php $qb = $dm->createQueryBuilder('User'); $query = $qb->getQuery(); // Debug $query = $qb->getQuery(); $debug = $query->debug(); // Eager cursor $dm->createQueryBuilder('User') ->eagerCursor(true); foreach ($cursor as $user) { // queries for all users and data is held internally // each User object is hydrated from the data one at a time. } // Distinct, Select // Refreshing Documents ``` The query builder's `refresh()` method may be used to instruct ODM to override the managed document with data from the query result ##### Fetching Documents as Read-Only ¿? ##### Disabling Hydration, limiting results, sorting, ##### Map reduce ```php $qb = $this->dm->createQueryBuilder('Event') ->field('type')->equals('sale') ->map('function() { emit(this.userId, 1); }') ->reduce("function(k, vals) { var sum = 0; for (var i in vals) { sum += vals[i]; } return sum; }"); ``` ##### Conditional Operators where($javascript), in($values), notIn($values), equals($value), notEqual($value), gt($value), gte($value), lt($value), lte($value), range($start, $end), size($size), exists($bool), type($type), all($values), mod($mod), addOr($expr), references($document), includesReferenceTo($document) ##### Text search ```php $qb = $dm->createQueryBuilder('Document') ->language('it') ->text('parole che stai cercando'); ``` ##### Atomic update operators set($name, $value, $atomic = true), setNewObj($newObj), inc($name, $value), unsetField($field), push($field, $value), pushAll($field, array $valueArray), addToSet($field, $value), addManyToSet($field, array $values), popFirst($field), popLast($field), pull($field, $value), pullAll($field, array $valueArray) ##### Update muliple docs $dm->createQueryBuilder('User') ->updateMany() ##### Remove ->remove() #### 3.2.2 Aggregation builder ```php $builder = $dm->createAggregationBuilder(\Documents\Orders::class); $builder ->match() ->field('purchaseDate') ->gte($from) ->lt($to) ->field('user') ->references($user) ->group() // ... ``` ##### Hydration By default, aggregation results are returned as PHP arrays. This is because the result of an aggregation pipeline may look completely different from the source document. In order to get hydrated aggregation results, you first have to map a `QueryResultDocument`. /** @QueryResultDocument */ class UserPurchases { ... } ##### Aggregation pipeline stages: $count, $limit, $out ... #### 3.2.2 Find and modify ```php $job = $dm->createQueryBuilder('Job') // Find the job ->findAndUpdate() ->field('in_progress')->equals(false) ->sort('priority', 'desc') // Update found job ->field('started')->set(new \MongoDate()) ->field('in_progress')->set(true) ->getQuery() ->execute(); // Also => findAndRemove() ``` #### 3.2.3 Priming Priming references allows you to consolidate database queries when working with one and many reference mappings. This is useful for avoiding the n+1 problem in your application. ```php $qb = $dm->createQueryBuilder('User') ->field('accounts')->prime(true) ->limit(100); $query = $qb->getQuery(); /* After querying for the users, ODM will collect the IDs of all referenced * accounts and load them with a single additional query. */ $users = $query->execute(); foreach ($users as $user) { /* Accounts have already been loaded, so iterating through accounts will * not query an additional query. */ foreach ($user->getAccounts() as $account) { } } ``` In this case, priming will allow us to load all users and referenced accounts in two database queries. Also workds for invers references ```php /** @Document */ class User { /** @ReferenceMany(targetDocument="Account", prime={"user"}) */ private $accounts; } ``` Passing `true` to `prime()` instructs ODM to load the referenced document(s) on its own #### 3.2.4 Eager Cursors With a typical MongoDB cursor, it stays open during iteration and fetches batches of documents as you iterate over the cursor. This isn't bad, but sometimes you want to fetch all of the data eagerly. For example when dealing with web applications, and you want to only show 50 documents from a collection you should fetch all the data in your controller first before going on to the view. Benefits: - The cursor stays open for a much shorter period of time. - Data retrieval and hydration are consolidated operations. - Doctrine has the ability to retry the cursor when exceptions during interaction with mongodb are encountered. ```php $qb = $dm->createQueryBuilder('User') ->eagerCursor(true); $query = $qb->getQuery(); $users = $query->execute(); // returns instance of Doctrine\MongoDB\ODM\EagerCursor //At this point all data is loaded from the database and cursors to MongoDB have been closed but hydration //of the data in to objects has not begun. Once insertion starts the data will be hydrated in to PHP //objects. foreach ($users as $user) { echo $user->getUsername()."\n"; } // Not all documents are converted to objects at once, the hydration is still done one document at a time during iteration. The only change is that all data is retrieved first. ``` N+1 Dos colecciones relacionada one to many => Coche y Rueda Se recorren los coches y por cada coche las ruedas => N coches => 1 Consulta para sacar los coches y N para sacar las ruedas => N+1 Sol: Sacar en una query todos los ids de las ruedas y en la siguiente query se le pasa el array ## 3 Advanced topics ### 3.2 Collections #### 3.2.1 Capped Collections Capped collections are fixed sized collections that have a very high performance Capped collections automatically maintain insertion order for the objects in the collection; this is very powerful for certain use cases such as logging. (ex: error logs) ```php Written with [StackEdit](https://stackedit.io/).