Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve orphan removal documentation - recommend using cascade=persist #9848

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 53 additions & 46 deletions docs/en/reference/working-with-associations.rst
Original file line number Diff line number Diff line change
@@ -37,51 +37,51 @@ information about its type and if it's the owning or inverse side.
{
/** @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")
*/
private $favorites;

/**
* Unidirectional - Many users have marked many comments as read
*
* @ManyToMany(targetEntity="Comment")
* @JoinTable(name="user_read_comments")
*/
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)
*
@@ -100,19 +100,19 @@ definitions omitted):
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,
@@ -135,7 +135,7 @@ relations of the ``User``:
public function getReadComments() {
return $this->commentsRead;
}

public function setFirstComment(Comment $c) {
$this->firstComment = $c;
}
@@ -148,17 +148,17 @@ The interaction code would then look like in the following snippet

<?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();

@@ -171,40 +171,40 @@ fields on both sides:
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();

@@ -225,10 +225,10 @@ element. Here are some examples:
// 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);
@@ -240,7 +240,7 @@ 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, you will
Also note that if you use type-hinting in your methods, you will
have to specify a nullable type, i.e. ``setAddress(?Address $address)``,
otherwise ``setAddress(null)`` will fail to remove the association.
Another way to deal with this is to provide a special method, like
@@ -271,8 +271,8 @@ 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
``$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
@@ -296,38 +296,38 @@ example that encapsulate much of the association management code:
// 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);
}
@@ -373,7 +373,7 @@ as your preferences.
Synchronizing Bidirectional Collections
---------------------------------------

In the case of Many-To-Many associations you as the developer have the
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
@@ -387,7 +387,7 @@ can show the possible caveats you can encounter:
<?php
$user->getFavorites()->add($favoriteComment);
// not calling $favoriteComment->getUserFavorites()->add($user);

$user->getFavorites()->contains($favoriteComment); // TRUE
$favoriteComment->getUserFavorites()->contains($user); // FALSE

@@ -422,7 +422,7 @@ comment might look like in your controller (without ``cascade: persist``):
$user = new User();
$myFirstComment = new Comment();
$user->addComment($myFirstComment);

$em->persist($user);
$em->persist($myFirstComment); // required, if `cascade: persist` is not set
$em->flush();
@@ -480,7 +480,7 @@ If you then set up the cascading to the ``User#commentsAuthored`` property...
<?php
$user = new User();
$user->comment('Lorem ipsum', new DateTime());

$em->persist($user);
$em->flush();

@@ -559,6 +559,13 @@ OrphanRemoval works with one-to-one, one-to-many and many-to-many associations.
If you neglect this assumption your entities will get deleted by Doctrine even if
you assigned the orphaned entity to another one.

.. note::

``orphanRemoval=true`` option should be used in combination with ``cascade=["persist"]`` option
as the child entity, that is manually persisted, will not be deleted automatically by Doctrine
when a collection is still an instance of ArrayCollection (before first flush / hydration).
This is a Doctrine limitation since ArrayCollection does not have access to a UnitOfWork.

As a better example consider an Addressbook application where you have Contacts, Addresses
and StandingData:

@@ -578,10 +585,10 @@ and StandingData:
/** @Id @Column(type="integer") @GeneratedValue */
private $id;

/** @OneToOne(targetEntity="StandingData", orphanRemoval=true) */
/** @OneToOne(targetEntity="StandingData", cascade={"persist"}, orphanRemoval=true) */
private $standingData;

/** @OneToMany(targetEntity="Address", mappedBy="contact", orphanRemoval=true) */
/** @OneToMany(targetEntity="Address", mappedBy="contact", cascade={"persist"}, orphanRemoval=true) */
private $addresses;

public function __construct()
@@ -612,10 +619,10 @@ Now two examples of what happens when you remove the references:

$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
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: