1
0
mirror of synced 2024-12-15 07:36:03 +03:00
doctrine2/en/reference/working-with-associations.rst
2011-10-04 10:33:48 -07:00

611 lines
20 KiB
ReStructuredText

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 :ref:`concept of owning and inverse sides <association-mapping-owning-inverse>`
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="authoredComments")
*/
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 both one-to-one and one-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.