Updated documentation on chosing inverse-owning side for many-to-many associations and came up with a significantly enhanced example for the working with associations chapter
This commit is contained in:
parent
298773b2f8
commit
62c4f3e6fb
@ -573,6 +573,46 @@ Here is a similar many-to-many relationship as above except this one is bidirect
|
||||
|
||||
The MySQL schema is exactly the same as for the Many-To-Many uni-directional case above.
|
||||
|
||||
+++ Picking the Owning and Inverse Side
|
||||
|
||||
For Many-To-Many associations you can chose which entity is the owning and which the inverse side. There is
|
||||
a very simple semantic rule to decide which side is more suitable to be the owning side from a developers perspective.
|
||||
You only have to ask yourself, which entity is responsible for the connection management and pick that as the owning side.
|
||||
|
||||
Take an example of two entities `Article` and `Tag`. Whenever you want to connect an Article to a Tag and vice-versa, it is mostly
|
||||
the Article that is responsible for this relation. Whenever you add a new article, you want to connect it with existing or new tags.
|
||||
Your create Article form will probably support this notion and allow to specify the tags directly. This is why you should
|
||||
pick the Article as owning side, as it makes the code more understandable:
|
||||
|
||||
[php]
|
||||
class Article
|
||||
{
|
||||
private $tags;
|
||||
|
||||
public function addTag(Tag $tag)
|
||||
{
|
||||
$tag->addArticle($this); // synchronously updating inverse side
|
||||
$this->tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
class Tag
|
||||
{
|
||||
private $articles;
|
||||
|
||||
public function addArticle(Article $article)
|
||||
{
|
||||
$this->articles[] = $article;
|
||||
}
|
||||
}
|
||||
|
||||
This allows to group the tag adding on the `Article` side of the association:
|
||||
|
||||
[php]
|
||||
$article = new Article();
|
||||
$article->addTag($tagA);
|
||||
$article->addTag($tagB);
|
||||
|
||||
++ Many-To-Many, Self-referencing
|
||||
|
||||
You can even have a self-referencing many-to-many association. A common scenario is where a `User` has friends and the target entity of that relationship is a `User` so it is self referencing. In this example it is bidirectional so `User` has a field named `$friendsWithMe` and `$myFriends`.
|
||||
|
@ -180,29 +180,168 @@ and you want to modify and persist such an entity.
|
||||
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 concept of owning and inverse sides in bidirectional associations as described [here](http://www.doctrine-project.org/documentation/manual/2_0/en/association-mapping#owning-side-and-inverse-side).
|
||||
* A collection of entities always only represents the association to the containing entities. If an entity is removed from a collection, the association is removed, not the entity itself.
|
||||
* Collection-valued persistent fields and properties must be defined in terms of the Doctrine\Common\Collections\Collection interface. [See here](http://www.doctrine-project.org/documentation/manual/2_0/en/architecture#entities:persistent-fields) for more details.
|
||||
* 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 persistent fields have to be instances of the `Doctrine\Common\Collections\Collection` interface. [See here](http://www.doctrine-project.org/documentation/manual/2_0/en/architecture#entities:persistent-fields) for more details.
|
||||
|
||||
+++ Example Associations
|
||||
|
||||
We will use a simple comment system with Users and Comments as entity to show examples of association management:
|
||||
|
||||
[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):
|
||||
|
||||
[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:
|
||||
Establishing an association between two entities is straight-forward. Here are some examples for the unidirectional
|
||||
relations of the `User`:
|
||||
|
||||
[php]
|
||||
// Article <- one-to-many -> Comment
|
||||
$article->getComments()->add($comment);
|
||||
$comment->setArticle($article);
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
public function getReadComments() {
|
||||
return $this->commentsRead;
|
||||
}
|
||||
|
||||
public function setFirstComment(Comment $c) {
|
||||
$this->firstComment = $c;
|
||||
}
|
||||
}
|
||||
|
||||
// unidirectional many to many
|
||||
$user->getReadComments()->add($comment);
|
||||
|
||||
// unidirectional many to one
|
||||
$user->setFirstComment($myFirstComment);
|
||||
|
||||
In the case of bi-directional associations you have to update the fields on both sides:
|
||||
|
||||
[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);
|
||||
|
||||
// User <- many-to-many -> Groups
|
||||
$user->getGroups()->add($group);
|
||||
$group->getUsers()->add($user);
|
||||
|
||||
// User <- one-to-one -> Address
|
||||
$user->setAddress($address);
|
||||
$address->setUser($user);
|
||||
// Many-To-One / One-To-Many Bidirectional
|
||||
$user->getAuthoredComments()->add($newComment);
|
||||
$newComment->setAuthor($user);
|
||||
|
||||
|
||||
Notice how always both sides of the bidirectional association are updated. Unidirectional associations are consequently simpler to handle.
|
||||
Notice how always both sides of the bidirectional association are updated. The previous unidirectional associations were simpler to handle.
|
||||
|
||||
++ Removing Associations
|
||||
|
||||
@ -211,30 +350,31 @@ to do so, by key and by element. Here are some examples:
|
||||
|
||||
[php]
|
||||
// Remove by Elements
|
||||
// Article <- one-to-many -> Comment
|
||||
$article->getComments()->removeElement($comment);
|
||||
$comment->setArticle(null);
|
||||
|
||||
// User <- many-to-many -> Group
|
||||
$user->getGroups()->removeElement($group);
|
||||
$group->getUsers()->removeElement($user);
|
||||
$user->getComments()->removeElement($comment);
|
||||
$comment->setAuthor(null);
|
||||
|
||||
// Remove by key
|
||||
$article->getComments()->remove($ithComment);
|
||||
$comment->setArticle(null);
|
||||
$user->getFavorites()->removeElement($comment);
|
||||
$comment->getUserFavorites()->removeElement($user);
|
||||
|
||||
// User <- one-to-one -> Address
|
||||
$user->setAddress(null);
|
||||
$address->setUser(null);
|
||||
// Remove by Key
|
||||
$user->getComments()->removeElement($ithComment);
|
||||
$comment->setAuthor(null);
|
||||
|
||||
|
||||
Notice how both sides of the bidirectional association are always updated. Unidirectional associations are consequently simpler to handle. Also note that if you type-hint your methods, i.e. `setAddress(Address $address)`, then PHP does not allow null values and 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.
|
||||
Notice how both sides of the bidirectional association are always updated. Unidirectional associations are consequently
|
||||
simpler to handle. Also note that if you type-hint your methods, i.e. `setAddress(Address $address)`, then PHP does only
|
||||
allows 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, where n is the size of the map.
|
||||
that has O(n) complexity using `array_search`, where n is the size of the map.
|
||||
|
||||
Since Doctrine always only looks at the owning side of a bidirectional association, it is essentially not necessary 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.
|
||||
> **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.
|
||||
|
||||
> **NOTE*
|
||||
>
|
||||
@ -250,38 +390,75 @@ Since Doctrine always only looks at the owning side of a bidirectional associati
|
||||
|
||||
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 a simple, idiomatic example for a bidirectional one-to-many association between an Article and its Comments.
|
||||
The following code shows updates to the previous User and Comment example that encapsulate much of
|
||||
the association management code:
|
||||
|
||||
[php]
|
||||
// Mappings not shown.
|
||||
class Article {
|
||||
// The comments of the article.
|
||||
private $comments;
|
||||
// ... constructor omitted ...
|
||||
class User
|
||||
{
|
||||
//...
|
||||
public function markCommentRead(Comment $comment) {
|
||||
// Collections implement ArrayAccess
|
||||
$this->commentsRead[] = $comment;
|
||||
}
|
||||
|
||||
public function addComment(Comment $comment) {
|
||||
$this->comments->add($comment);
|
||||
$comment->setArticle($this);
|
||||
if (count($this->commentsAuthored) == 0) {
|
||||
$this->setFirstComment($comment);
|
||||
}
|
||||
$this->comments[] = $comment;
|
||||
$comment->setAuthor($this);
|
||||
}
|
||||
public function getComments() {
|
||||
return $this->comments;
|
||||
|
||||
private function setFirstComment(Comment $c) {
|
||||
$this->firstComment = $c;
|
||||
}
|
||||
}
|
||||
class Comment {
|
||||
// The article the comment refers to.
|
||||
private $article;
|
||||
// ... constructor omitted ...
|
||||
public function setArticle($article) {
|
||||
$this->article = $article;
|
||||
|
||||
public function addFavorite(Comment $comment) {
|
||||
$this->favorites->add($comment);
|
||||
$comment->addUserFavorite($this);
|
||||
}
|
||||
public function getArticle() {
|
||||
return $this->article;
|
||||
|
||||
public function removeFavorite(Comment $comment) {
|
||||
$this->favorites->removeElement($comment);
|
||||
$comment->removeUserFavorite($this);
|
||||
}
|
||||
}
|
||||
|
||||
With the above implementation, it is always ensured that at least the owning side from Doctrine's point of view (Comment) is properly updated. You will notice that `setArticle` does not call `addComment`, thus the bidirectional association is strictly-speaking still incomplete, if a user of the class only invokes `setArticle`. If you naively call `addComment` in `setArticle`, however, 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.
|
||||
class Comment
|
||||
{
|
||||
// ..
|
||||
|
||||
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.
|
||||
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. Example:
|
||||
>
|
||||
> [php]
|
||||
> class User {
|
||||
> public function getReadComments() {
|
||||
> return $this->commentsRead->toArray();
|
||||
> }
|
||||
> }
|
||||
|
||||
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.
|
||||
|
||||
++ Transitive persistence
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user