MongoDB Object Document Mapper Annotations, Cache, PHPCS (standards)
PHPCR ODM ¿?¿?¿?¿?
Collections Refelction Instantiator
PHP Doctrine MongoDB Object Document Mapper (ODM) provides transpare per MongoDB
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.
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();
Doctrine ODM is build fro PHP 5.3+
If we update a property and call ->flush()
again we'll get an efficient update query using the atomic operators:¿?
composer require "doctrine/mongodb-odm"
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);
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.
composer config "platform.ext-mongo" "1.6.16" && composer require "alcaeus/mongo-php-adapter"
A Document is a persistent domain object, a PHP class following these restrictions:
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".
The Doctrine MongoDB ODM defines its own set of docblock annotations for supplying object document mapping metadata.
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.
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)
/** @Id */ You can configure custom ID strategies if you don't want to use the default MongoId.
/** @Field(type="string") */
<?php /** * @Document(collection="my_documents") * @DiscriminatorField("type") * @DiscriminatorMap({"article"="Article", "album"="Album"}) **/ class Article { // ... } /** * @Document(collection="my_documents") * @DiscriminatorField("type") * @DiscriminatorMap({"article"="Article", "album"="Album"}) **/ class Album { // ... }
Many-valued References use Collection interface and a corresponding ArrayCollection implementation. Defined in Doctrine\Common\Collections, They are decoupled from the ODM.
/** @ReferenceMany */ private $favorites = array();
Now the $favorites
property can store a reference to any type of document
The name of the field within the DBRef object can be customized via the discriminatorField
option
@ReferenceMany(discriminatorField="type")
$ref
, $id
, and $db
fields (this is the default)$ref
and $id
id
field
@ReferenceOne(targetDocument="Profile", storeAs="id")
Now, the profile
field will only store the MongoId
of the referenced Profile document.
By default, Doctrine will not cascade any UnitOfWork
operations to referenced documents. You must explicitly enable this functionality.
@ReferenceOne(targetDocument="Profile", cascade={"persist"})
Valid values: all, detach, merge, refresh, remove, persist
If a Document of type A
contains references to privately owned Documents B
then if the reference from A
to B
is removed the document B
should also be removed, because it is not used anymore.
OrphanRemoval works with both reference one and many mapped fields.
When using the orphanRemoval=true
option Doctrine makes the assumption that the documents are privately owned and will NOT be reused by other documents.
When flush is called not only are the references removed but both the old standing data and the one address documents are also deleted from the database.
By default when you map a bi-directional reference, the reference is maintained on both sides of the relationship and there is not a single "owning side". Both sides are considered owning and changes are tracked and persisted separately. When you persist, the references (DRefs) would exist on both sides.
A user may have lots of posts and we don't need to store a reference to each post on the user. In order to map this you can use the inversedBy
and mappedBy
options
/** @Document */ class BlogPost { // ... /** @ReferenceOne(targetDocument="User", inversedBy="posts") */ private $user; } /** @Document */ class User { // ... /** @ReferenceMany(targetDocument="BlogPost", mappedBy="user") */ private $posts; } $user = $dm->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.
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.
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.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.
Embed One, Embed Many All operations on embedded documents are automatically cascaded.
// Ver Mongo
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.
$uowSize = $dm->getUnitOfWork()->size();
This size affects the performance of flush() operations.
$uow = $dm->getUnitOfWork(); // modifying it is not recommended
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.
SAME for REMOVED state
Each reference to another document or a collection of documents can be configured to automatically cascade certain operations. By default, no operations are cascaded.
$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
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
A custom repository allows filtering logic to be consolidated into a single class instead of spreading it throughout a project
/** @Document(repositoryClass="Repositories\UserRepository") */ // ------ class UserRepository extends DocumentRepository { public function findDisabled() { return $this->findBy(['disabled' => true, 'activated' => true]); } }
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
/** @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 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
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.
You need rename name
to fullName
; however, you'd like to hydrate fullName
from name
if the new field doesn't exist.
/** @Field(type="string") @AlsoLoad("name") */ public $fullName;
You cannot query all values you have to query with and or for both properties
Complete name to separate first and lastname
/** @AlsoLoad({"name", "fullName"}) */ public function populateFirstAndLastName($fullName) { list($this->firstName, $this->lastName) = explode(' ', $fullName); }
FIND, FIND_AND_UPDATE, FIND_AND_REMOVE, INSERT, UPDATE, REMOVE, GROUP, MAP_REDUCE, DISTINCT_FIELD, GEO_LOCATION
$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
$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; }");
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)
$qb = $dm->createQueryBuilder('Document') ->language('it') ->text('parole che stai cercando');
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)
$dm->createQueryBuilder('User') ->updateMany()
->remove()
$builder = $dm->createAggregationBuilder(\Documents\Orders::class); $builder ->match() ->field('purchaseDate') ->gte($from) ->lt($to) ->field('user') ->references($user) ->group() // ...
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 { ... }
$count, $limit, $out ...
$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()
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.
$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
/** @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
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:
$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
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 /** * @Document(collection={ * "name"="collname", * "capped"=true, * "size"=100000, * "max"=1000 * }) */ class Category { }
Different strategies for storing, floats, ints, embeds and references. increment, addToSet, set, setArray, pushAll, atomicSet, atomicSetArray
Written with StackEdit.