From 51729cbaa71d8aacf5765b51b58a2e738d5fb9a2 Mon Sep 17 00:00:00 2001 From: beberlei Date: Thu, 10 Jun 2010 21:06:23 +0200 Subject: [PATCH 1/2] Enhanced Description of how the different events work and what restrictions apply to them. --- manual/en/events.txt | 151 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/manual/en/events.txt b/manual/en/events.txt index 254a43427..951ed153d 100644 --- a/manual/en/events.txt +++ b/manual/en/events.txt @@ -267,16 +267,167 @@ Although you get passed the EntityManager in all of these events, you have to fo carefully since operations in the wrong event may produce lots of different errors, such as inconsistent data and lost updates/persists/removes. +For the described events that are also lifecycle callback events the restrictions +apply aswell, with the additional restriction that you do not have access to the EntityManager +or UnitOfWork APIs inside these events. + +++ prePersist +There are two ways for the `prePersist` event to be triggered. One is obviously +when you call `EntityManager#persist()`. The event is also called for all +cascaded associations. + +There is another way for `prePersist` to be called, inside the `flush()` +method when changes to associations are computed and this association +is marked as cascade persist. Any new entity found during this operation +is also persisted and `prePersist` called on it. This is called "persistence by reachability". + +In both cases you get passed a `LifecycleEventArgs` +instance which has access to the entity and the entity manager. + +The following restrictions apply to `prePersist`: + +* If you are using a PrePersist Identity Generator such as sequences the ID value + will *NOT* be available within any PrePersist events. +* Doctrine will not recognize changes made to relations in a pre persist event + called by "reachibility" through a cascade persist unless you use the internal + `UnitOfWork` API. We do not recommend such operations in the persistence by + reachability context, so do this at your own risk and possibly supported by unit-tests. + +++ preRemove +The `preRemove` event is called on every entity when its passed to +the `EntityManager#remove()` method. It is cascaded for all +associations that are marked as cascade delete. + +There are no restrictions to what methods can be called inside +the `preRemove` event, except when the remove method itself was +called during a flush operation. + +++ onFlush +OnFlush is a very powerful event. It is called inside `EntityManager#flush()` +after the changes to all the managed entities and their associations have +been computed. This means, the `onFlush` event has access to the sets of: + +* Entities scheduled for insert +* Entities scheduled for update +* Entities scheduled for removal +* Collections scheduled for update +* Collections scheduled for removal + +To make use of the onFlush event you have to be familiar with interal UnitOfWork API, +which grants you access to the previously mentioned sets. See this example: + + [php] + class FlushExampleListener + { + public function onFlush(OnFlushEventArgs $eventArgs) + { + $em = $eventArgs->getEntityManager(); + $uow = $em->getUnitOfWork(); + + foreach ($uow->getScheduledEntityInsertions() AS $entity) { + + } + + foreach ($uow->getScheduledEntityUpdates() AS $entity) { + + } + + foreach ($uow->getScheduledEntityDeletions() AS $entity) { + + } + + foreach ($uow->getScheduledCollectionDeletions() AS $col) { + + } + + foreach ($uow->getScheduledCollectionUpdates() AS $col) { + + } + } + } + +The following restrictions apply to the onFlush event: + +* Calling `EntityManager#persist()` does not suffice to trigger a persist on an entity. + You have to execute an additional call to `$unitOfWork->computeChangeSet($classMetadata, $entity)`. +* Changing primitive fields or associations requires you to explicitly trigger + a re-computation of the changeset of the affected entity. This can be done + by either calling `$unitOfWork->computeChangeSet($classMetadata, $entity)` + or `$unitOfWork->recomputeSingleEntityChangeSet($classMetadata, $entity)`. The second + method has lower overhead, but only re-computes primitive fields, never associations. + +++ preUpdate +PreUpdate is the most restrictive to use event, since it is called right before +an update statement is called for an entity inside the `EntityManager#flush()` +method. + +Changes to associations of the updated entity are never allowed in this event, since Doctrine cannot guarantee to +correctly handle referential integrity at this point of the flush operation. This +event has a powerful feature however, it is executed with a `PreUpdateEventArgs` +instance, which contains a reference to the computed change-set of this entity. + +This means you have access to all the fields that have changed for this entity +with their old and new value. The following methods are available on the `PreUpdateEventArgs`: + +* `getEntity()` to get access to the actual entity. +* `getEntityChangeSet()` to get a copy of the changeset array. Changes to this returned array do not affect updating. +* `hasChangedField($fieldName)` to check if the given field name of the current entity changed. +* `getOldValue($fieldName)` and `getNewValue($fieldName)` to access the values of a field. +* `setNewValue($fieldName, $value)` to change the value of a field to be updated. + +A simple example for this event looks like: + + [php] + class NeverAliceOnlyBobListener + { + public function preUpdate(PreUpdateEventArgs $eventArgs) + { + if ($eventArgs->getEntity() instanceof User) { + if ($eventArgs->hasChangedField('name') && $eventArgs->getNewValue() == 'Alice') { + $eventArgs->setNewValue('name', 'Bob'); + } + } + } + } + +You could also use this listener to implement validation of all the fields that have changed. +This is more efficient than using a lifecycle callback when there are expensive validations +to call: + + [php] + class ValidCreditCardListener + { + public function preUpdate(PreUpdateEventArgs $eventArgs) + { + if ($eventArgs->getEntity() instanceof Account) { + if ($eventArgs->hasChangedField('creditCard')) { + $this->validateCreditCard($eventArgs->getNewValue('creditCard')); + } + } + } + + private function validateCreditCard($no) + { + // throw an exception to interupt flush event. Transaction will be rolled back. + } + } + +Restrictions for this event: + +* Changes to associations of the passed entities are not recognized by the flush operation anymore. +* Changes to fields of the passed entities are not recognized by the flush operation anymore, use the computed change-set passed to the event to modify primitive field values. +* Any calls to `EntityManager#persist()` or `EntityManager#remove()`, even in combination with the UnitOfWork API are strongly discouraged and don't work as expected outside the flush operation. + +++ postUpdate, postRemove, postPersist +The three post events are called inside `EntityManager#flush()`. Changes in here +are not relevant to the persistence in the database, but you can use this events +to + ++ Load ClassMetadata Event When the mapping information for an entity is read, it is populated in to a From d872d7f0b194fa01fd6ea1365b844ff72e79e41e Mon Sep 17 00:00:00 2001 From: beberlei Date: Thu, 10 Jun 2010 21:38:22 +0200 Subject: [PATCH 2/2] Updated Cookbook Getting Started XML Edition Tutorial with Array and Scalar Hydration examples, tweaked several passages a little bit --- cookbook/en/getting-started-xml-edition.txt | 84 ++++++++++++++++++--- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/cookbook/en/getting-started-xml-edition.txt b/cookbook/en/getting-started-xml-edition.txt index 8669d0ba6..93851cc13 100644 --- a/cookbook/en/getting-started-xml-edition.txt +++ b/cookbook/en/getting-started-xml-edition.txt @@ -17,7 +17,7 @@ that contains the data which is persisted and retrieved by Doctrine's data mappi ## An Example Model: Bug Tracker For this Getting Started Guide for Doctrine we will implement the Bug Tracker domain model from the [Zend_Db_Table](http://framework.zend.com/manual/en/zend.db.table.html) -documentation. Reading that documentat we can extract the requirements to be: +documentation. Reading their documentation we can extract the requirements to be: * A Bugs has a description, creation date, status, reporter and engineer * A bug can occour on different products (platforms) @@ -491,7 +491,7 @@ Having created the schema we can now start and save entities in the database. Fo $entityManager->persist($user); $entityManager->flush(); -Having a user, he can create products: +Products can also be created: [php] $newProductName = "My Product"; @@ -509,12 +509,16 @@ You have to explicitly call `flush()` to have the EntityManager write those two You might wonder why does this distinction between persist notification and flush exist? Doctrine 2 uses the UnitOfWork pattern to aggregate all writes (INSERT, UDPATE, DELETE) into one single fast transaction, which -is executed when flushing. This way the write-performance is unbelievable fast. Also in more complex scenarios -than those two before you can request updates on many different entities and all flush them at once. +is executed when flush is called. Using this approach the write-performance is significantly faster than +in a scenario where updates are done for each entity in isolation. In more complex scenarios than the +previous two, you are free to request updates on many different entities and all flush them at once. -Doctrine's UnitOfWork even detects entities that have been retrieved from the database and changed when calling -flush, so that you only have to keep track of those entities that are new or to be removed and pass them to -`EntityManager#persist()` and `EntityManager#remove()` respectively. +Doctrine's UnitOfWork detects entities that have changed after retrieval from the database automatically when +the flush operation is called, so that you only have to keep track of those entities that are new or to be removed and pass them to +`EntityManager#persist()` and `EntityManager#remove()` respectively. This comparison to find dirty +entities that need updating is using a very efficient algorithm that has almost no additional +memory overhead and can even save you computing power by only updating those database columns +that really changed. We are now getting to the "Create a New Bug" requirement and the code for this scenario may look like this: @@ -550,6 +554,8 @@ is called and relate them in the database appropriately. ## Queries for Application Use-Cases +### List of Bugs + Using the previous examples we can fill up the database quite a bit, however we now need to discuss how to query the underlying mapper for the required view representations. When opening the application, bugs can be paginated through a list-view, which is the first read-only use-case: @@ -605,6 +611,38 @@ in one single SQL statement. The console output of this script is then: > Using Native SQL you could even use stored procedures for data retrieval, or make use of advanced non-portable > database queries like PostgreSql's recursive queries. +### Array Hydration of the Bug List + +In the previous use-case we retrieved the result as their respective object instances. +We are not limitied to retrieving objects only from Doctrine however. For a simple list view +like the previous one we only need read access to our entities and can switch the hydration +from objects to simple PHP arrays instead. This can obviously yiel considerable performance benefits for read-only requests. + +Implementing the same list view as before using array hydration we can rewrite our code: + + [php] + $dql = "SELECT b, e, r, p FROM Bug b JOIN b.engineer e ". + "JOIN b.reporter r JOIN b.products p ORDER BY b.created DESC"; + $query = $em->createQuery($dql); + $bugs = $query->getArrayResult(); + + foreach ($bugs AS $bug) { + echo $bug['description'] . " - " . $bug['created']->format('d.m.Y')."\n"; + echo " Reported by: ".$bug['reporter']['name']."\n"; + echo " Assigned to: ".$bug['engineer']['name']."\n"; + foreach($bug['products'] AS $product) { + echo " Platform: ".$product['name']."\n"; + } + echo "\n"; + } + +There is one significant difference in the DQL query however, we have +to add an additional fetch-join for the products connected to a bug. The resulting +SQL query for this single select statement is pretty large, however still +more efficient to retrieve compared to hydrating objects. + +### Find by Primary Key + The next Use-Case is displaying a Bug by primary key. This could be done using DQL as in the previous example with a where clause, however there is a convenience method on the Entity Manager that handles loading by primary key, which we have already seen in the write scenarios: @@ -619,8 +657,10 @@ However we will soon see another problem with our entities using this approach. echo "Engineer: ".$bug->getEngineer()->name."\n"; It will be null! What is happening? It worked in the previous example, so it can't be a problem with the persistance -code of Doctrine. You walked in the public property trap. Since we only retrieved the bug by primary key both the -engineer and reporter are not immediately loaded from the database but are replaced by LazyLoading proxies. Sample +code of Doctrine. What is it then? You walked in the public property trap. + +Since we only retrieved the bug by primary key both the engineer and reporter are not immediately loaded +from the database but are replaced by LazyLoading proxies. Sample code of this proxy generated code can be found in the specified Proxy Directory, it looks like: [php] @@ -647,8 +687,8 @@ code of this proxy generated code can be found in the specified Proxy Directory, } See how upon each method call the proxy is lazily loaded from the database? Using public properties however -we never call a method and Doctrine has no way to hook into the PHP Engine to detect this access and trigger -the lazy load. We need to revise our entities, make all the properties private or protected and add getters +we never call a method and Doctrine has no way to hook into the PHP Engine to detect a direct access to a public property +and trigger the lazy load. We need to rewrite our entities, make all the properties private or protected and add getters and setters to get a working example: [php] @@ -660,6 +700,11 @@ and setters to get a working example: Engineer: beberlei */ +Being required to use private or protected properties Doctrine 2 actually enforces you to encapsulate +your objects according to object-oriented best-practices. + +## Dashboard of the User + For the next use-case we want to retrieve the dashboard view, a list of all open bugs the user reported or was assigned to. This will be achieved using DQL again, this time with some WHERE clauses and usage of bound parameters: @@ -679,6 +724,23 @@ was assigned to. This will be achieved using DQL again, this time with some WHER That is it for the read-scenarios of this example, we will continue with the last missing bit, engineers being able to close a bug. +## Number of Bugs + +Until now we only retrieved entities or their array representation. Doctrine also supports the retrieval +of non-entities through DQL. These values are called "scalar result values" and may even be aggregate +values using COUNT, SUM, MIN, MAX or AVG functions. + +We will need this knowledge to retrieve the number of open bugs grouped by product: + + [php] + $dql = "SELECT p.id, p.name, count(b.id) AS openBugs FROM Bug b ". + "JOIN b.products p WHERE b.status = 'OPEN' GROUP BY p.id"; + $productBugs = $em->createQuery($dql)->getScalarResult(); + + foreach($productBugs as $productBug) { + echo $productBug['name']." has " . $productBug['openBugs'] . " open bugs!\n"; + } + ## Updating Entities There is a single use-case missing from the requirements, Engineers should be able to close a bug. This looks like: