123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712 |
- Working with Associations
- =========================
- Associations between entities are represented just like in regular
- object-oriented PHP, with references to other objects or
- collections of objects. When it comes to persistence, it is
- important to understand three main things:
- - The :doc:`concept of owning and inverse sides <unitofwork-associations>`
- in bidirectional associations.
- - If an entity is removed from a collection, the association is
- removed, not the entity itself. A collection of entities always
- only represents the association to the containing entities, not the
- entity itself.
- - Collection-valued :ref:`persistent fields <architecture_persistent_fields>` have to be instances of the
- ``Doctrine\Common\Collections\Collection`` interface.
- Changes to associations in your code are not synchronized to the
- database directly, but upon calling ``EntityManager#flush()``.
- To describe all the concepts of working with associations we
- introduce a specific set of example entities that show all the
- different flavors of association management in Doctrine.
- Association Example Entities
- ----------------------------
- We will use a simple comment system with Users and Comments as
- entities to show examples of association management. See the PHP
- docblocks of each association in the following example for
- information about its type and if it's the owning or inverse side.
- .. code-block:: php
- <?php
- /** @Entity */
- class User
- {
- /** @Id @GeneratedValue @Column(type="string") */
- private $id;
-
- /**
- * Bidirectional - Many users have Many favorite comments (OWNING SIDE)
- *
- * @ManyToMany(targetEntity="Comment", inversedBy="userFavorites")
- * @JoinTable(name="user_favorite_comments",
- * joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
- * inverseJoinColumns={@JoinColumn(name="favorite_comment_id", referencedColumnName="id")}
- * )
- */
- private $favorites;
-
- /**
- * Unidirectional - Many users have marked many comments as read
- *
- * @ManyToMany(targetEntity="Comment")
- * @JoinTable(name="user_read_comments",
- * joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
- * inverseJoinColumns={@JoinColumn(name="comment_id", referencedColumnName="id")}
- * )
- */
- private $commentsRead;
-
- /**
- * Bidirectional - One-To-Many (INVERSE SIDE)
- *
- * @OneToMany(targetEntity="Comment", mappedBy="author")
- */
- private $commentsAuthored;
-
- /**
- * Unidirectional - Many-To-One
- *
- * @ManyToOne(targetEntity="Comment")
- */
- private $firstComment;
- }
-
- /** @Entity */
- class Comment
- {
- /** @Id @GeneratedValue @Column(type="string") */
- private $id;
-
- /**
- * Bidirectional - Many comments are favorited by many users (INVERSE SIDE)
- *
- * @ManyToMany(targetEntity="User", mappedBy="favorites")
- */
- private $userFavorites;
-
- /**
- * Bidirectional - Many Comments are authored by one user (OWNING SIDE)
- *
- * @ManyToOne(targetEntity="User", inversedBy="commentsAuthored")
- */
- private $author;
- }
- This two entities generate the following MySQL Schema (Foreign Key
- definitions omitted):
- .. code-block:: sql
- CREATE TABLE User (
- id VARCHAR(255) NOT NULL,
- firstComment_id VARCHAR(255) DEFAULT NULL,
- PRIMARY KEY(id)
- ) ENGINE = InnoDB;
-
- CREATE TABLE Comment (
- id VARCHAR(255) NOT NULL,
- author_id VARCHAR(255) DEFAULT NULL,
- PRIMARY KEY(id)
- ) ENGINE = InnoDB;
-
- CREATE TABLE user_favorite_comments (
- user_id VARCHAR(255) NOT NULL,
- favorite_comment_id VARCHAR(255) NOT NULL,
- PRIMARY KEY(user_id, favorite_comment_id)
- ) ENGINE = InnoDB;
-
- CREATE TABLE user_read_comments (
- user_id VARCHAR(255) NOT NULL,
- comment_id VARCHAR(255) NOT NULL,
- PRIMARY KEY(user_id, comment_id)
- ) ENGINE = InnoDB;
- Establishing Associations
- -------------------------
- Establishing an association between two entities is
- straight-forward. Here are some examples for the unidirectional
- relations of the ``User``:
- .. code-block:: php
- <?php
- class User
- {
- // ...
- public function getReadComments() {
- return $this->commentsRead;
- }
-
- public function setFirstComment(Comment $c) {
- $this->firstComment = $c;
- }
- }
- The interaction code would then look like in the following snippet
- (``$em`` here is an instance of the EntityManager):
- .. code-block:: php
- <?php
- $user = $em->find('User', $userId);
-
- // unidirectional many to many
- $comment = $em->find('Comment', $readCommentId);
- $user->getReadComments()->add($comment);
-
- $em->flush();
-
- // unidirectional many to one
- $myFirstComment = new Comment();
- $user->setFirstComment($myFirstComment);
-
- $em->persist($myFirstComment);
- $em->flush();
- In the case of bi-directional associations you have to update the
- fields on both sides:
- .. code-block:: php
- <?php
- class User
- {
- // ..
-
- public function getAuthoredComments() {
- return $this->commentsAuthored;
- }
-
- public function getFavoriteComments() {
- return $this->favorites;
- }
- }
-
- class Comment
- {
- // ...
-
- public function getUserFavorites() {
- return $this->userFavorites;
- }
-
- public function setAuthor(User $author = null) {
- $this->author = $author;
- }
- }
-
- // Many-to-Many
- $user->getFavorites()->add($favoriteComment);
- $favoriteComment->getUserFavorites()->add($user);
-
- $em->flush();
-
- // Many-To-One / One-To-Many Bidirectional
- $newComment = new Comment();
- $user->getAuthoredComments()->add($newComment);
- $newComment->setAuthor($user);
-
- $em->persist($newComment);
- $em->flush();
- Notice how always both sides of the bidirectional association are
- updated. The previous unidirectional associations were simpler to
- handle.
- Removing Associations
- ---------------------
- Removing an association between two entities is similarly
- straight-forward. There are two strategies to do so, by key and by
- element. Here are some examples:
- .. code-block:: php
- <?php
- // Remove by Elements
- $user->getComments()->removeElement($comment);
- $comment->setAuthor(null);
-
- $user->getFavorites()->removeElement($comment);
- $comment->getUserFavorites()->removeElement($user);
-
- // Remove by Key
- $user->getComments()->remove($ithComment);
- $comment->setAuthor(null);
- You need to call ``$em->flush()`` to make persist these changes in
- the database permanently.
- Notice how both sides of the bidirectional association are always
- updated. Unidirectional associations are consequently simpler to
- handle. Also note that if you use type-hinting in your methods, i.e.
- ``setAddress(Address $address)``, PHP will only allow null
- values if ``null`` is set as default value. Otherwise
- setAddress(null) will fail for removing the association. If you
- insist on type-hinting a typical way to deal with this is to
- provide a special method, like ``removeAddress()``. This can also
- provide better encapsulation as it hides the internal meaning of
- not having an address.
- When working with collections, keep in mind that a Collection is
- essentially an ordered map (just like a PHP array). That is why the
- ``remove`` operation accepts an index/key. ``removeElement`` is a
- separate method that has O(n) complexity using ``array_search``,
- where n is the size of the map.
- .. note::
- Since Doctrine always only looks at the owning side of a
- bidirectional association for updates, it is not necessary for
- write operations that an inverse collection of a bidirectional
- one-to-many or many-to-many association is updated. This knowledge
- can often be used to improve performance by avoiding the loading of
- the inverse collection.
- You can also clear the contents of a whole collection using the
- ``Collections::clear()`` method. You should be aware that using
- this method can lead to a straight and optimized database delete or
- update call during the flush operation that is not aware of
- entities that have been re-added to the collection.
- Say you clear a collection of tags by calling
- ``$post->getTags()->clear();`` and then call
- ``$post->getTags()->add($tag)``. This will not recognize the tag having
- already been added previously and will consequently issue two separate database
- calls.
- Association Management Methods
- ------------------------------
- It is generally a good idea to encapsulate proper association
- management inside the entity classes. This makes it easier to use
- the class correctly and can encapsulate details about how the
- association is maintained.
- The following code shows updates to the previous User and Comment
- example that encapsulate much of the association management code:
- .. code-block:: php
- <?php
- class User
- {
- //...
- public function markCommentRead(Comment $comment) {
- // Collections implement ArrayAccess
- $this->commentsRead[] = $comment;
- }
-
- public function addComment(Comment $comment) {
- if (count($this->commentsAuthored) == 0) {
- $this->setFirstComment($comment);
- }
- $this->comments[] = $comment;
- $comment->setAuthor($this);
- }
-
- private function setFirstComment(Comment $c) {
- $this->firstComment = $c;
- }
-
- public function addFavorite(Comment $comment) {
- $this->favorites->add($comment);
- $comment->addUserFavorite($this);
- }
-
- public function removeFavorite(Comment $comment) {
- $this->favorites->removeElement($comment);
- $comment->removeUserFavorite($this);
- }
- }
-
- class Comment
- {
- // ..
-
- public function addUserFavorite(User $user) {
- $this->userFavorites[] = $user;
- }
-
- public function removeUserFavorite(User $user) {
- $this->userFavorites->removeElement($user);
- }
- }
- You will notice that ``addUserFavorite`` and ``removeUserFavorite``
- do not call ``addFavorite`` and ``removeFavorite``, thus the
- bidirectional association is strictly-speaking still incomplete.
- However if you would naively add the ``addFavorite`` in
- ``addUserFavorite``, you end up with an infinite loop, so more work
- is needed. As you can see, proper bidirectional association
- management in plain OOP is a non-trivial task and encapsulating all
- the details inside the classes can be challenging.
- .. note::
- If you want to make sure that your collections are perfectly
- encapsulated you should not return them from a
- ``getCollectionName()`` method directly, but call
- ``$collection->toArray()``. This way a client programmer for the
- entity cannot circumvent the logic you implement on your entity for
- association management. For example:
- .. code-block:: php
- <?php
- class User {
- public function getReadComments() {
- return $this->commentsRead->toArray();
- }
- }
- This will however always initialize the collection, with all the
- performance penalties given the size. In some scenarios of large
- collections it might even be a good idea to completely hide the
- read access behind methods on the EntityRepository.
- There is no single, best way for association management. It greatly
- depends on the requirements of your concrete domain model as well
- as your preferences.
- Synchronizing Bidirectional Collections
- ---------------------------------------
- In the case of Many-To-Many associations you as the developer have the
- responsibility of keeping the collections on the owning and inverse side
- in sync when you apply changes to them. Doctrine can only
- guarantee a consistent state for the hydration, not for your client
- code.
- Using the User-Comment entities from above, a very simple example
- can show the possible caveats you can encounter:
- .. code-block:: php
- <?php
- $user->getFavorites()->add($favoriteComment);
- // not calling $favoriteComment->getUserFavorites()->add($user);
-
- $user->getFavorites()->contains($favoriteComment); // TRUE
- $favoriteComment->getUserFavorites()->contains($user); // FALSE
- There are two approaches to handle this problem in your code:
- 1. Ignore updating the inverse side of bidirectional collections,
- BUT never read from them in requests that changed their state. In
- the next Request Doctrine hydrates the consistent collection state
- again.
- 2. Always keep the bidirectional collections in sync through
- association management methods. Reads of the Collections directly
- after changes are consistent then.
- Transitive persistence / Cascade Operations
- -------------------------------------------
- Persisting, removing, detaching and merging individual entities can
- become pretty cumbersome, especially when a highly interweaved object graph
- is involved. Therefore Doctrine 2 provides a
- mechanism for transitive persistence through cascading of these
- operations. Each association to another entity or a collection of
- entities can be configured to automatically cascade certain
- operations. By default, no operations are cascaded.
- The following cascade options exist:
- - persist : Cascades persist operations to the associated
- entities.
- - remove : Cascades remove operations to the associated entities.
- - merge : Cascades merge operations to the associated entities.
- - detach : Cascades detach operations to the associated entities.
- - all : Cascades persist, remove, merge and detach operations to
- associated entities.
- .. note::
- Cascade operations are performed in memory. That means collections and related entities
- are fetched into memory, even if they are still marked as lazy when
- the cascade operation is about to be performed. However this approach allows
- entity lifecycle events to be performed for each of these operations.
- However, pulling objects graph into memory on cascade can cause considerable performance
- overhead, especially when cascading collections are large. Makes sure
- to weigh the benefits and downsides of each cascade operation that you define.
- To rely on the database level cascade operations for the delete operation instead, you can
- configure each join column with the **onDelete** option. See the respective
- mapping driver chapters for more information.
- The following example is an extension to the User-Comment example
- of this chapter. Suppose in our application a user is created
- whenever he writes his first comment. In this case we would use the
- following code:
- .. code-block:: php
- <?php
- $user = new User();
- $myFirstComment = new Comment();
- $user->addComment($myFirstComment);
-
- $em->persist($user);
- $em->persist($myFirstComment);
- $em->flush();
- Even if you *persist* a new User that contains our new Comment this
- code would fail if you removed the call to
- ``EntityManager#persist($myFirstComment)``. Doctrine 2 does not
- cascade the persist operation to all nested entities that are new
- as well.
- More complicated is the deletion of all of a user's comments when he is
- removed from the system:
- .. code-block:: php
- $user = $em->find('User', $deleteUserId);
-
- foreach ($user->getAuthoredComments() AS $comment) {
- $em->remove($comment);
- }
- $em->remove($user);
- $em->flush();
- Without the loop over all the authored comments Doctrine would use
- an UPDATE statement only to set the foreign key to NULL and only
- the User would be deleted from the database during the
- flush()-Operation.
- To have Doctrine handle both cases automatically we can change the
- ``User#commentsAuthored`` property to cascade both the "persist"
- and the "remove" operation.
- .. code-block:: php
- <?php
- class User
- {
- //...
- /**
- * Bidirectional - One-To-Many (INVERSE SIDE)
- *
- * @OneToMany(targetEntity="Comment", mappedBy="author", cascade={"persist", "remove"})
- */
- private $commentsAuthored;
- //...
- }
- Even though automatic cascading is convenient it should be used
- with care. Do not blindly apply cascade=all to all associations as
- it will unnecessarily degrade the performance of your application.
- For each cascade operation that gets activated Doctrine also
- applies that operation to the association, be it single or
- collection valued.
- Persistence by Reachability: Cascade Persist
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- There are additional semantics that apply to the Cascade Persist
- operation. During each flush() operation Doctrine detects if there
- are new entities in any collection and three possible cases can
- happen:
- 1. New entities in a collection marked as cascade persist will be
- directly persisted by Doctrine.
- 2. New entities in a collection not marked as cascade persist will
- produce an Exception and rollback the flush() operation.
- 3. Collections without new entities are skipped.
- This concept is called Persistence by Reachability: New entities
- that are found on already managed entities are automatically
- persisted as long as the association is defined as cascade
- persist.
- Orphan Removal
- --------------
- There is another concept of cascading that is relevant only when removing entities
- from collections. If an Entity of type ``A`` contains references to privately
- owned Entities ``B`` then if the reference from ``A`` to ``B`` is removed the
- entity ``B`` should also be removed, because it is not used anymore.
- OrphanRemoval works with one-to-one, one-to-many and many-to-many associations.
- .. note::
- When using the ``orphanRemoval=true`` option Doctrine makes the assumption
- that the entities are privately owned and will **NOT** be reused by other entities.
- If you neglect this assumption your entities will get deleted by Doctrine even if
- you assigned the orphaned entity to another one.
- As a better example consider an Addressbook application where you have Contacts, Addresses
- and StandingData:
- .. code-block:: php
- <?php
- namespace Addressbook;
- use Doctrine\Common\Collections\ArrayCollection;
- /**
- * @Entity
- */
- class Contact
- {
- /** @Id @Column(type="integer") @GeneratedValue */
- private $id;
- /** @OneToOne(targetEntity="StandingData", orphanRemoval=true) */
- private $standingData;
- /** @OneToMany(targetEntity="Address", mappedBy="contact", orphanRemoval=true) */
- private $addresses;
- public function __construct()
- {
- $this->addresses = new ArrayCollection();
- }
- public function newStandingData(StandingData $sd)
- {
- $this->standingData = $sd;
- }
- public function removeAddress($pos)
- {
- unset($this->addresses[$pos]);
- }
- }
- Now two examples of what happens when you remove the references:
- .. code-block:: php
- <?php
- $contact = $em->find("Addressbook\Contact", $contactId);
- $contact->newStandingData(new StandingData("Firstname", "Lastname", "Street"));
- $contact->removeAddress(1);
- $em->flush();
- In this case you have not only changed the ``Contact`` entity itself but
- you have also removed the references for standing data and as well as one
- address reference. When flush is called not only are the references removed
- but both the old standing data and the one address entity are also deleted
- from the database.
- Filtering Collections
- ---------------------
- .. filtering-collections:
- Collections have a filtering API that allows to slice parts of data from
- a collection. If the collection has not been loaded from the database yet,
- the filtering API can work on the SQL level to make optimized access to
- large collections.
- .. code-block:: php
- <?php
- use Doctrine\Common\Collections\Criteria;
- $group = $entityManager->find('Group', $groupId);
- $userCollection = $group->getUsers();
- $criteria = Criteria::create()
- ->where(Criteria::expr()->eq("birthday", "1982-02-17"))
- ->orderBy(array("username" => "ASC"))
- ->setFirstResult(0)
- ->setMaxResults(20)
- ;
- $birthdayUsers = $userCollection->matching($criteria);
- .. tip::
- You can move the access of slices of collections into dedicated methods of
- an entity. For example ``Group#getTodaysBirthdayUsers()``.
- The Criteria has a limited matching language that works both on the
- SQL and on the PHP collection level. This means you can use collection matching
- interchangeably, independent of in-memory or sql-backed collections.
- .. code-block:: php
- <?php
- use Doctrine\Common\Collections;
- class Criteria
- {
- /**
- * @return Criteria
- */
- static public function create();
- /**
- * @param Expression $where
- * @return Criteria
- */
- public function where(Expression $where);
- /**
- * @param Expression $where
- * @return Criteria
- */
- public function andWhere(Expression $where);
- /**
- * @param Expression $where
- * @return Criteria
- */
- public function orWhere(Expression $where);
- /**
- * @param array $orderings
- * @return Criteria
- */
- public function orderBy(array $orderings);
- /**
- * @param int $firstResult
- * @return Criteria
- */
- public function setFirstResult($firstResult);
- /**
- * @param int $maxResults
- * @return Criteria
- */
- public function setMaxResults($maxResults);
- public function getOrderings();
- public function getWhereExpression();
- public function getFirstResult();
- public function getMaxResults();
- }
- You can build expressions through the ExpressionBuilder. It has the following
- methods:
- * ``andX($arg1, $arg2, ...)``
- * ``orX($arg1, $arg2, ...)``
- * ``eq($field, $value)``
- * ``gt($field, $value)``
- * ``lt($field, $value)``
- * ``lte($field, $value)``
- * ``gte($field, $value)``
- * ``neq($field, $value)``
- * ``isNull($field)``
- * ``in($field, array $values)``
- * ``notIn($field, array $values)``
|