working-with-associations.rst 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. Working with Associations
  2. =========================
  3. Associations between entities are represented just like in regular
  4. object-oriented PHP, with references to other objects or
  5. collections of objects. When it comes to persistence, it is
  6. important to understand three main things:
  7. - The :doc:`concept of owning and inverse sides <unitofwork-associations>`
  8. in bidirectional associations.
  9. - If an entity is removed from a collection, the association is
  10. removed, not the entity itself. A collection of entities always
  11. only represents the association to the containing entities, not the
  12. entity itself.
  13. - Collection-valued :ref:`persistent fields <architecture_persistent_fields>` have to be instances of the
  14. ``Doctrine\Common\Collections\Collection`` interface.
  15. Changes to associations in your code are not synchronized to the
  16. database directly, but upon calling ``EntityManager#flush()``.
  17. To describe all the concepts of working with associations we
  18. introduce a specific set of example entities that show all the
  19. different flavors of association management in Doctrine.
  20. Association Example Entities
  21. ----------------------------
  22. We will use a simple comment system with Users and Comments as
  23. entities to show examples of association management. See the PHP
  24. docblocks of each association in the following example for
  25. information about its type and if it's the owning or inverse side.
  26. .. code-block:: php
  27. <?php
  28. /** @Entity */
  29. class User
  30. {
  31. /** @Id @GeneratedValue @Column(type="string") */
  32. private $id;
  33. /**
  34. * Bidirectional - Many users have Many favorite comments (OWNING SIDE)
  35. *
  36. * @ManyToMany(targetEntity="Comment", inversedBy="userFavorites")
  37. * @JoinTable(name="user_favorite_comments",
  38. * joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
  39. * inverseJoinColumns={@JoinColumn(name="favorite_comment_id", referencedColumnName="id")}
  40. * )
  41. */
  42. private $favorites;
  43. /**
  44. * Unidirectional - Many users have marked many comments as read
  45. *
  46. * @ManyToMany(targetEntity="Comment")
  47. * @JoinTable(name="user_read_comments",
  48. * joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
  49. * inverseJoinColumns={@JoinColumn(name="comment_id", referencedColumnName="id")}
  50. * )
  51. */
  52. private $commentsRead;
  53. /**
  54. * Bidirectional - One-To-Many (INVERSE SIDE)
  55. *
  56. * @OneToMany(targetEntity="Comment", mappedBy="author")
  57. */
  58. private $commentsAuthored;
  59. /**
  60. * Unidirectional - Many-To-One
  61. *
  62. * @ManyToOne(targetEntity="Comment")
  63. */
  64. private $firstComment;
  65. }
  66. /** @Entity */
  67. class Comment
  68. {
  69. /** @Id @GeneratedValue @Column(type="string") */
  70. private $id;
  71. /**
  72. * Bidirectional - Many comments are favorited by many users (INVERSE SIDE)
  73. *
  74. * @ManyToMany(targetEntity="User", mappedBy="favorites")
  75. */
  76. private $userFavorites;
  77. /**
  78. * Bidirectional - Many Comments are authored by one user (OWNING SIDE)
  79. *
  80. * @ManyToOne(targetEntity="User", inversedBy="commentsAuthored")
  81. */
  82. private $author;
  83. }
  84. This two entities generate the following MySQL Schema (Foreign Key
  85. definitions omitted):
  86. .. code-block:: sql
  87. CREATE TABLE User (
  88. id VARCHAR(255) NOT NULL,
  89. firstComment_id VARCHAR(255) DEFAULT NULL,
  90. PRIMARY KEY(id)
  91. ) ENGINE = InnoDB;
  92. CREATE TABLE Comment (
  93. id VARCHAR(255) NOT NULL,
  94. author_id VARCHAR(255) DEFAULT NULL,
  95. PRIMARY KEY(id)
  96. ) ENGINE = InnoDB;
  97. CREATE TABLE user_favorite_comments (
  98. user_id VARCHAR(255) NOT NULL,
  99. favorite_comment_id VARCHAR(255) NOT NULL,
  100. PRIMARY KEY(user_id, favorite_comment_id)
  101. ) ENGINE = InnoDB;
  102. CREATE TABLE user_read_comments (
  103. user_id VARCHAR(255) NOT NULL,
  104. comment_id VARCHAR(255) NOT NULL,
  105. PRIMARY KEY(user_id, comment_id)
  106. ) ENGINE = InnoDB;
  107. Establishing Associations
  108. -------------------------
  109. Establishing an association between two entities is
  110. straight-forward. Here are some examples for the unidirectional
  111. relations of the ``User``:
  112. .. code-block:: php
  113. <?php
  114. class User
  115. {
  116. // ...
  117. public function getReadComments() {
  118. return $this->commentsRead;
  119. }
  120. public function setFirstComment(Comment $c) {
  121. $this->firstComment = $c;
  122. }
  123. }
  124. The interaction code would then look like in the following snippet
  125. (``$em`` here is an instance of the EntityManager):
  126. .. code-block:: php
  127. <?php
  128. $user = $em->find('User', $userId);
  129. // unidirectional many to many
  130. $comment = $em->find('Comment', $readCommentId);
  131. $user->getReadComments()->add($comment);
  132. $em->flush();
  133. // unidirectional many to one
  134. $myFirstComment = new Comment();
  135. $user->setFirstComment($myFirstComment);
  136. $em->persist($myFirstComment);
  137. $em->flush();
  138. In the case of bi-directional associations you have to update the
  139. fields on both sides:
  140. .. code-block:: php
  141. <?php
  142. class User
  143. {
  144. // ..
  145. public function getAuthoredComments() {
  146. return $this->commentsAuthored;
  147. }
  148. public function getFavoriteComments() {
  149. return $this->favorites;
  150. }
  151. }
  152. class Comment
  153. {
  154. // ...
  155. public function getUserFavorites() {
  156. return $this->userFavorites;
  157. }
  158. public function setAuthor(User $author = null) {
  159. $this->author = $author;
  160. }
  161. }
  162. // Many-to-Many
  163. $user->getFavorites()->add($favoriteComment);
  164. $favoriteComment->getUserFavorites()->add($user);
  165. $em->flush();
  166. // Many-To-One / One-To-Many Bidirectional
  167. $newComment = new Comment();
  168. $user->getAuthoredComments()->add($newComment);
  169. $newComment->setAuthor($user);
  170. $em->persist($newComment);
  171. $em->flush();
  172. Notice how always both sides of the bidirectional association are
  173. updated. The previous unidirectional associations were simpler to
  174. handle.
  175. Removing Associations
  176. ---------------------
  177. Removing an association between two entities is similarly
  178. straight-forward. There are two strategies to do so, by key and by
  179. element. Here are some examples:
  180. .. code-block:: php
  181. <?php
  182. // Remove by Elements
  183. $user->getComments()->removeElement($comment);
  184. $comment->setAuthor(null);
  185. $user->getFavorites()->removeElement($comment);
  186. $comment->getUserFavorites()->removeElement($user);
  187. // Remove by Key
  188. $user->getComments()->remove($ithComment);
  189. $comment->setAuthor(null);
  190. You need to call ``$em->flush()`` to make persist these changes in
  191. the database permanently.
  192. Notice how both sides of the bidirectional association are always
  193. updated. Unidirectional associations are consequently simpler to
  194. handle. Also note that if you use type-hinting in your methods, i.e.
  195. ``setAddress(Address $address)``, PHP will only allow null
  196. values if ``null`` is set as default value. Otherwise
  197. setAddress(null) will fail for removing the association. If you
  198. insist on type-hinting a typical way to deal with this is to
  199. provide a special method, like ``removeAddress()``. This can also
  200. provide better encapsulation as it hides the internal meaning of
  201. not having an address.
  202. When working with collections, keep in mind that a Collection is
  203. essentially an ordered map (just like a PHP array). That is why the
  204. ``remove`` operation accepts an index/key. ``removeElement`` is a
  205. separate method that has O(n) complexity using ``array_search``,
  206. where n is the size of the map.
  207. .. note::
  208. Since Doctrine always only looks at the owning side of a
  209. bidirectional association for updates, it is not necessary for
  210. write operations that an inverse collection of a bidirectional
  211. one-to-many or many-to-many association is updated. This knowledge
  212. can often be used to improve performance by avoiding the loading of
  213. the inverse collection.
  214. You can also clear the contents of a whole collection using the
  215. ``Collections::clear()`` method. You should be aware that using
  216. this method can lead to a straight and optimized database delete or
  217. update call during the flush operation that is not aware of
  218. entities that have been re-added to the collection.
  219. Say you clear a collection of tags by calling
  220. ``$post->getTags()->clear();`` and then call
  221. ``$post->getTags()->add($tag)``. This will not recognize the tag having
  222. already been added previously and will consequently issue two separate database
  223. calls.
  224. Association Management Methods
  225. ------------------------------
  226. It is generally a good idea to encapsulate proper association
  227. management inside the entity classes. This makes it easier to use
  228. the class correctly and can encapsulate details about how the
  229. association is maintained.
  230. The following code shows updates to the previous User and Comment
  231. example that encapsulate much of the association management code:
  232. .. code-block:: php
  233. <?php
  234. class User
  235. {
  236. //...
  237. public function markCommentRead(Comment $comment) {
  238. // Collections implement ArrayAccess
  239. $this->commentsRead[] = $comment;
  240. }
  241. public function addComment(Comment $comment) {
  242. if (count($this->commentsAuthored) == 0) {
  243. $this->setFirstComment($comment);
  244. }
  245. $this->comments[] = $comment;
  246. $comment->setAuthor($this);
  247. }
  248. private function setFirstComment(Comment $c) {
  249. $this->firstComment = $c;
  250. }
  251. public function addFavorite(Comment $comment) {
  252. $this->favorites->add($comment);
  253. $comment->addUserFavorite($this);
  254. }
  255. public function removeFavorite(Comment $comment) {
  256. $this->favorites->removeElement($comment);
  257. $comment->removeUserFavorite($this);
  258. }
  259. }
  260. class Comment
  261. {
  262. // ..
  263. public function addUserFavorite(User $user) {
  264. $this->userFavorites[] = $user;
  265. }
  266. public function removeUserFavorite(User $user) {
  267. $this->userFavorites->removeElement($user);
  268. }
  269. }
  270. You will notice that ``addUserFavorite`` and ``removeUserFavorite``
  271. do not call ``addFavorite`` and ``removeFavorite``, thus the
  272. bidirectional association is strictly-speaking still incomplete.
  273. However if you would naively add the ``addFavorite`` in
  274. ``addUserFavorite``, you end up with an infinite loop, so more work
  275. is needed. As you can see, proper bidirectional association
  276. management in plain OOP is a non-trivial task and encapsulating all
  277. the details inside the classes can be challenging.
  278. .. note::
  279. If you want to make sure that your collections are perfectly
  280. encapsulated you should not return them from a
  281. ``getCollectionName()`` method directly, but call
  282. ``$collection->toArray()``. This way a client programmer for the
  283. entity cannot circumvent the logic you implement on your entity for
  284. association management. For example:
  285. .. code-block:: php
  286. <?php
  287. class User {
  288. public function getReadComments() {
  289. return $this->commentsRead->toArray();
  290. }
  291. }
  292. This will however always initialize the collection, with all the
  293. performance penalties given the size. In some scenarios of large
  294. collections it might even be a good idea to completely hide the
  295. read access behind methods on the EntityRepository.
  296. There is no single, best way for association management. It greatly
  297. depends on the requirements of your concrete domain model as well
  298. as your preferences.
  299. Synchronizing Bidirectional Collections
  300. ---------------------------------------
  301. In the case of Many-To-Many associations you as the developer have the
  302. responsibility of keeping the collections on the owning and inverse side
  303. in sync when you apply changes to them. Doctrine can only
  304. guarantee a consistent state for the hydration, not for your client
  305. code.
  306. Using the User-Comment entities from above, a very simple example
  307. can show the possible caveats you can encounter:
  308. .. code-block:: php
  309. <?php
  310. $user->getFavorites()->add($favoriteComment);
  311. // not calling $favoriteComment->getUserFavorites()->add($user);
  312. $user->getFavorites()->contains($favoriteComment); // TRUE
  313. $favoriteComment->getUserFavorites()->contains($user); // FALSE
  314. There are two approaches to handle this problem in your code:
  315. 1. Ignore updating the inverse side of bidirectional collections,
  316. BUT never read from them in requests that changed their state. In
  317. the next Request Doctrine hydrates the consistent collection state
  318. again.
  319. 2. Always keep the bidirectional collections in sync through
  320. association management methods. Reads of the Collections directly
  321. after changes are consistent then.
  322. Transitive persistence / Cascade Operations
  323. -------------------------------------------
  324. Persisting, removing, detaching and merging individual entities can
  325. become pretty cumbersome, especially when a highly interweaved object graph
  326. is involved. Therefore Doctrine 2 provides a
  327. mechanism for transitive persistence through cascading of these
  328. operations. Each association to another entity or a collection of
  329. entities can be configured to automatically cascade certain
  330. operations. By default, no operations are cascaded.
  331. The following cascade options exist:
  332. - persist : Cascades persist operations to the associated
  333. entities.
  334. - remove : Cascades remove operations to the associated entities.
  335. - merge : Cascades merge operations to the associated entities.
  336. - detach : Cascades detach operations to the associated entities.
  337. - all : Cascades persist, remove, merge and detach operations to
  338. associated entities.
  339. .. note::
  340. Cascade operations are performed in memory. That means collections and related entities
  341. are fetched into memory, even if they are still marked as lazy when
  342. the cascade operation is about to be performed. However this approach allows
  343. entity lifecycle events to be performed for each of these operations.
  344. However, pulling objects graph into memory on cascade can cause considerable performance
  345. overhead, especially when cascading collections are large. Makes sure
  346. to weigh the benefits and downsides of each cascade operation that you define.
  347. To rely on the database level cascade operations for the delete operation instead, you can
  348. configure each join column with the **onDelete** option. See the respective
  349. mapping driver chapters for more information.
  350. The following example is an extension to the User-Comment example
  351. of this chapter. Suppose in our application a user is created
  352. whenever he writes his first comment. In this case we would use the
  353. following code:
  354. .. code-block:: php
  355. <?php
  356. $user = new User();
  357. $myFirstComment = new Comment();
  358. $user->addComment($myFirstComment);
  359. $em->persist($user);
  360. $em->persist($myFirstComment);
  361. $em->flush();
  362. Even if you *persist* a new User that contains our new Comment this
  363. code would fail if you removed the call to
  364. ``EntityManager#persist($myFirstComment)``. Doctrine 2 does not
  365. cascade the persist operation to all nested entities that are new
  366. as well.
  367. More complicated is the deletion of all of a user's comments when he is
  368. removed from the system:
  369. .. code-block:: php
  370. $user = $em->find('User', $deleteUserId);
  371. foreach ($user->getAuthoredComments() AS $comment) {
  372. $em->remove($comment);
  373. }
  374. $em->remove($user);
  375. $em->flush();
  376. Without the loop over all the authored comments Doctrine would use
  377. an UPDATE statement only to set the foreign key to NULL and only
  378. the User would be deleted from the database during the
  379. flush()-Operation.
  380. To have Doctrine handle both cases automatically we can change the
  381. ``User#commentsAuthored`` property to cascade both the "persist"
  382. and the "remove" operation.
  383. .. code-block:: php
  384. <?php
  385. class User
  386. {
  387. //...
  388. /**
  389. * Bidirectional - One-To-Many (INVERSE SIDE)
  390. *
  391. * @OneToMany(targetEntity="Comment", mappedBy="author", cascade={"persist", "remove"})
  392. */
  393. private $commentsAuthored;
  394. //...
  395. }
  396. Even though automatic cascading is convenient it should be used
  397. with care. Do not blindly apply cascade=all to all associations as
  398. it will unnecessarily degrade the performance of your application.
  399. For each cascade operation that gets activated Doctrine also
  400. applies that operation to the association, be it single or
  401. collection valued.
  402. Persistence by Reachability: Cascade Persist
  403. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  404. There are additional semantics that apply to the Cascade Persist
  405. operation. During each flush() operation Doctrine detects if there
  406. are new entities in any collection and three possible cases can
  407. happen:
  408. 1. New entities in a collection marked as cascade persist will be
  409. directly persisted by Doctrine.
  410. 2. New entities in a collection not marked as cascade persist will
  411. produce an Exception and rollback the flush() operation.
  412. 3. Collections without new entities are skipped.
  413. This concept is called Persistence by Reachability: New entities
  414. that are found on already managed entities are automatically
  415. persisted as long as the association is defined as cascade
  416. persist.
  417. Orphan Removal
  418. --------------
  419. There is another concept of cascading that is relevant only when removing entities
  420. from collections. If an Entity of type ``A`` contains references to privately
  421. owned Entities ``B`` then if the reference from ``A`` to ``B`` is removed the
  422. entity ``B`` should also be removed, because it is not used anymore.
  423. OrphanRemoval works with one-to-one, one-to-many and many-to-many associations.
  424. .. note::
  425. When using the ``orphanRemoval=true`` option Doctrine makes the assumption
  426. that the entities are privately owned and will **NOT** be reused by other entities.
  427. If you neglect this assumption your entities will get deleted by Doctrine even if
  428. you assigned the orphaned entity to another one.
  429. As a better example consider an Addressbook application where you have Contacts, Addresses
  430. and StandingData:
  431. .. code-block:: php
  432. <?php
  433. namespace Addressbook;
  434. use Doctrine\Common\Collections\ArrayCollection;
  435. /**
  436. * @Entity
  437. */
  438. class Contact
  439. {
  440. /** @Id @Column(type="integer") @GeneratedValue */
  441. private $id;
  442. /** @OneToOne(targetEntity="StandingData", orphanRemoval=true) */
  443. private $standingData;
  444. /** @OneToMany(targetEntity="Address", mappedBy="contact", orphanRemoval=true) */
  445. private $addresses;
  446. public function __construct()
  447. {
  448. $this->addresses = new ArrayCollection();
  449. }
  450. public function newStandingData(StandingData $sd)
  451. {
  452. $this->standingData = $sd;
  453. }
  454. public function removeAddress($pos)
  455. {
  456. unset($this->addresses[$pos]);
  457. }
  458. }
  459. Now two examples of what happens when you remove the references:
  460. .. code-block:: php
  461. <?php
  462. $contact = $em->find("Addressbook\Contact", $contactId);
  463. $contact->newStandingData(new StandingData("Firstname", "Lastname", "Street"));
  464. $contact->removeAddress(1);
  465. $em->flush();
  466. In this case you have not only changed the ``Contact`` entity itself but
  467. you have also removed the references for standing data and as well as one
  468. address reference. When flush is called not only are the references removed
  469. but both the old standing data and the one address entity are also deleted
  470. from the database.
  471. Filtering Collections
  472. ---------------------
  473. .. filtering-collections:
  474. Collections have a filtering API that allows to slice parts of data from
  475. a collection. If the collection has not been loaded from the database yet,
  476. the filtering API can work on the SQL level to make optimized access to
  477. large collections.
  478. .. code-block:: php
  479. <?php
  480. use Doctrine\Common\Collections\Criteria;
  481. $group = $entityManager->find('Group', $groupId);
  482. $userCollection = $group->getUsers();
  483. $criteria = Criteria::create()
  484. ->where(Criteria::expr()->eq("birthday", "1982-02-17"))
  485. ->orderBy(array("username" => "ASC"))
  486. ->setFirstResult(0)
  487. ->setMaxResults(20)
  488. ;
  489. $birthdayUsers = $userCollection->matching($criteria);
  490. .. tip::
  491. You can move the access of slices of collections into dedicated methods of
  492. an entity. For example ``Group#getTodaysBirthdayUsers()``.
  493. The Criteria has a limited matching language that works both on the
  494. SQL and on the PHP collection level. This means you can use collection matching
  495. interchangeably, independent of in-memory or sql-backed collections.
  496. .. code-block:: php
  497. <?php
  498. use Doctrine\Common\Collections;
  499. class Criteria
  500. {
  501. /**
  502. * @return Criteria
  503. */
  504. static public function create();
  505. /**
  506. * @param Expression $where
  507. * @return Criteria
  508. */
  509. public function where(Expression $where);
  510. /**
  511. * @param Expression $where
  512. * @return Criteria
  513. */
  514. public function andWhere(Expression $where);
  515. /**
  516. * @param Expression $where
  517. * @return Criteria
  518. */
  519. public function orWhere(Expression $where);
  520. /**
  521. * @param array $orderings
  522. * @return Criteria
  523. */
  524. public function orderBy(array $orderings);
  525. /**
  526. * @param int $firstResult
  527. * @return Criteria
  528. */
  529. public function setFirstResult($firstResult);
  530. /**
  531. * @param int $maxResults
  532. * @return Criteria
  533. */
  534. public function setMaxResults($maxResults);
  535. public function getOrderings();
  536. public function getWhereExpression();
  537. public function getFirstResult();
  538. public function getMaxResults();
  539. }
  540. You can build expressions through the ExpressionBuilder. It has the following
  541. methods:
  542. * ``andX($arg1, $arg2, ...)``
  543. * ``orX($arg1, $arg2, ...)``
  544. * ``eq($field, $value)``
  545. * ``gt($field, $value)``
  546. * ``lt($field, $value)``
  547. * ``lte($field, $value)``
  548. * ``gte($field, $value)``
  549. * ``neq($field, $value)``
  550. * ``isNull($field)``
  551. * ``in($field, array $values)``
  552. * ``notIn($field, array $values)``