Split Working with Objects into two chapters, adding Working with Associations
This commit is contained in:
parent
99533afd11
commit
23efc790c1
@ -5,6 +5,7 @@
|
|||||||
+ Association Mapping
|
+ Association Mapping
|
||||||
+ Inheritance Mapping
|
+ Inheritance Mapping
|
||||||
+ Working with objects
|
+ Working with objects
|
||||||
|
+ Working with associations
|
||||||
+ Transactions and Concurrency
|
+ Transactions and Concurrency
|
||||||
+ Events
|
+ Events
|
||||||
+ Batch processing
|
+ Batch processing
|
||||||
|
@ -57,7 +57,9 @@ In all the examples of many-valued associations in this manual we will make use
|
|||||||
|
|
||||||
++ Mapping Defaults
|
++ Mapping Defaults
|
||||||
|
|
||||||
The @JoinColumn and @JoinTable definitions are usually optional and have sensible default values. The defaults for a join column in a one-to-one/many-to-one association is as follows:
|
Before we introduce all the association mappings in detailyou should note that the @JoinColumn and @JoinTable
|
||||||
|
definitions are usually optional and have sensible default values.
|
||||||
|
The defaults for a join column in a one-to-one/many-to-one association is as follows:
|
||||||
|
|
||||||
name: "<fieldname>_id"
|
name: "<fieldname>_id"
|
||||||
referencedColumnName: "id"
|
referencedColumnName: "id"
|
||||||
@ -573,7 +575,7 @@ 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.
|
The MySQL schema is exactly the same as for the Many-To-Many uni-directional case above.
|
||||||
|
|
||||||
+++ Picking the Owning and Inverse Side
|
+++ Picking Owning and Inverse Side
|
||||||
|
|
||||||
For Many-To-Many associations you can chose which entity is the owning and which the inverse side. There is
|
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.
|
a very simple semantic rule to decide which side is more suitable to be the owning side from a developers perspective.
|
||||||
|
@ -289,4 +289,8 @@ but we wanted to introduce you to DQL at this point. Can you **find** the easier
|
|||||||
> schema with the command `doctrine orm:schema-tool --drop` followed by
|
> schema with the command `doctrine orm:schema-tool --drop` followed by
|
||||||
> `doctrine orm:schema-tool --create`.
|
> `doctrine orm:schema-tool --create`.
|
||||||
|
|
||||||
6) Explore Doctrine 2!
|
6) Explore Doctrine 2!
|
||||||
|
|
||||||
|
See the following links if you want to start with more complex tutorials rather than reading the manual:
|
||||||
|
|
||||||
|
* Doctrine2 Cookbook: [Getting Started XML Edition](http://www.doctrine-project.org/projects/orm/2.0/docs/cookbook/getting-started-xml-edition/en)
|
||||||
|
417
manual/en/working-with-associations.txt
Normal file
417
manual/en/working-with-associations.txt
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
++ 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 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).
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
Changes to associations in your code are not synchronized to the database directly, but upon calling `EntityManager#flush()`.
|
||||||
|
|
||||||
|
+++ Example: Entities with Associations
|
||||||
|
|
||||||
|
We will use a simple comment system with Users and Comments as entities to show examples of association management.
|
||||||
|
See the docblocks of each association in the following example for information about its type and if its the owning or inverse side.
|
||||||
|
|
||||||
|
[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 for the unidirectional
|
||||||
|
relations of the `User`:
|
||||||
|
|
||||||
|
[php]
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
public function getReadComments() {
|
||||||
|
return $this->commentsRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFirstComment(Comment $c) {
|
||||||
|
$this->firstComment = $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Interaction would then look like the following code parts, `$em` here is an instance of the EntityManager:
|
||||||
|
|
||||||
|
$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:
|
||||||
|
|
||||||
|
[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();
|
||||||
|
$m->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:
|
||||||
|
|
||||||
|
[php]
|
||||||
|
// Remove by Elements
|
||||||
|
$user->getComments()->removeElement($comment);
|
||||||
|
$comment->setAuthor(null);
|
||||||
|
|
||||||
|
$user->getFavorites()->removeElement($comment);
|
||||||
|
$comment->getUserFavorites()->removeElement($user);
|
||||||
|
|
||||||
|
// Remove by Key
|
||||||
|
$user->getComments()->removeElement($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 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 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.
|
||||||
|
|
||||||
|
> **NOTE**
|
||||||
|
>
|
||||||
|
> 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 tag being already added before and issue
|
||||||
|
> two 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:
|
||||||
|
|
||||||
|
[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:
|
||||||
|
|
||||||
|
[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 are responsible to keep the collections on the
|
||||||
|
owning and inverse side up 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:
|
||||||
|
|
||||||
|
[php]
|
||||||
|
$user->getFavorites()->add($favoriteComment);
|
||||||
|
// not calling $favoriteComment->getUserFavorites()->add($user);
|
||||||
|
|
||||||
|
$user->getFavorites()->contains($favoriteComment); // TRUE
|
||||||
|
$favoriteComment->getUerFavorites()->contains($user); // FALSE
|
||||||
|
|
||||||
|
There are to 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 larger object graph with collections 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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
[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 a users comments when he is removed from the system:
|
||||||
|
|
||||||
|
$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.
|
||||||
|
|
||||||
|
[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.
|
@ -9,6 +9,15 @@ A Unit of Work can be manually closed by calling EntityManager#close(). Any
|
|||||||
changes to objects within this Unit of Work that have not yet been persisted
|
changes to objects within this Unit of Work that have not yet been persisted
|
||||||
are lost.
|
are lost.
|
||||||
|
|
||||||
|
> **NOTE**
|
||||||
|
>
|
||||||
|
> It is very important to understand that only `EntityManager#flush()` ever causes
|
||||||
|
> write operations against the database to be executed. Any other methods such
|
||||||
|
> as `EntityManager#persist($entity)` or `EntityManager#remove($entity)` only
|
||||||
|
> notify the UnitOfWork to perform these operations during flush.
|
||||||
|
>
|
||||||
|
> Not calling `EntityManager#flush()` will lead to all changes during that request being lost.
|
||||||
|
|
||||||
++ Persisting entities
|
++ Persisting entities
|
||||||
|
|
||||||
An entity can be made persistent by passing it to the `EntityManager#persist($entity)`
|
An entity can be made persistent by passing it to the `EntityManager#persist($entity)`
|
||||||
@ -174,352 +183,6 @@ and you want to modify and persist such an entity.
|
|||||||
> there is no need to use `merge`. I.e. you can simply pass detached objects from a cache
|
> there is no need to use `merge`. I.e. you can simply pass detached objects from a cache
|
||||||
> directly to the view.
|
> directly to the view.
|
||||||
|
|
||||||
|
|
||||||
++ 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 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).
|
|
||||||
* 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 for the unidirectional
|
|
||||||
relations of the `User`:
|
|
||||||
|
|
||||||
[php]
|
|
||||||
class User
|
|
||||||
{
|
|
||||||
// ...
|
|
||||||
public function getReadComments() {
|
|
||||||
return $this->commentsRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setFirstComment(Comment $c) {
|
|
||||||
$this->firstComment = $c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Interaction would then look like the following code parts, `$em` here is an instance of the EntityManager:
|
|
||||||
|
|
||||||
$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:
|
|
||||||
|
|
||||||
[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();
|
|
||||||
$m->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:
|
|
||||||
|
|
||||||
[php]
|
|
||||||
// Remove by Elements
|
|
||||||
$user->getComments()->removeElement($comment);
|
|
||||||
$comment->setAuthor(null);
|
|
||||||
|
|
||||||
$user->getFavorites()->removeElement($comment);
|
|
||||||
$comment->getUserFavorites()->removeElement($user);
|
|
||||||
|
|
||||||
// Remove by Key
|
|
||||||
$user->getComments()->removeElement($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 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 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.
|
|
||||||
|
|
||||||
> **NOTE**
|
|
||||||
>
|
|
||||||
> 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 tag being already added before and issue
|
|
||||||
> two 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:
|
|
||||||
|
|
||||||
[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:
|
|
||||||
|
|
||||||
[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.
|
|
||||||
|
|
||||||
++ Transitive persistence
|
|
||||||
|
|
||||||
Persisting, removing, detaching and merging individual entities can become pretty
|
|
||||||
cumbersome, especially when a larger object graph with collections 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.
|
|
||||||
|
|
||||||
The following example shows an association to a number of addresses. If persist()
|
|
||||||
or remove() is invoked on any User entity, it will be cascaded to all associated
|
|
||||||
Address entities in the $addresses collection.
|
|
||||||
|
|
||||||
[php]
|
|
||||||
class User
|
|
||||||
{
|
|
||||||
//...
|
|
||||||
/**
|
|
||||||
* @OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "remove"})
|
|
||||||
*/
|
|
||||||
private $addresses;
|
|
||||||
//...
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
++ Synchronization with the Database
|
++ Synchronization with the Database
|
||||||
|
|
||||||
The state of persistent entities is synchronized with the database on flush of an `EntityManager`
|
The state of persistent entities is synchronized with the database on flush of an `EntityManager`
|
||||||
@ -528,11 +191,21 @@ persistent entities and their relationships to the database. Thereby bidirection
|
|||||||
are persisted based on the references held by the owning side of the relationship as explained
|
are persisted based on the references held by the owning side of the relationship as explained
|
||||||
in the Association Mapping chapter.
|
in the Association Mapping chapter.
|
||||||
|
|
||||||
|
When `EntityManager#flush()` is called, Doctrine inspects all managed, new and removed entities
|
||||||
|
and will perform the following operations.
|
||||||
|
|
||||||
|
+++ Synchronizing New and Managed Entities
|
||||||
|
|
||||||
The flush operation applies to a managed entity with the following semantics:
|
The flush operation applies to a managed entity with the following semantics:
|
||||||
|
|
||||||
* The entity itself is synchronized to the database, if it has changed.
|
* The entity itself is synchronized to the database using a SQL UPDATE statement, only if at least one persistent field has changed.
|
||||||
|
* No SQL updates are executed if the entity did not change.
|
||||||
|
|
||||||
For all (initialized) relationships of the entity the following semantics apply to each
|
The flush operation applies to a new entity with the following semantics:
|
||||||
|
|
||||||
|
* The entity itself is synchronized to the database using a SQL INSERT statement.
|
||||||
|
|
||||||
|
For all (initialized) relationships of the new or managed entity the following semantics apply to each
|
||||||
associated entity X:
|
associated entity X:
|
||||||
|
|
||||||
* If X is new and persist operations are configured to cascade on the relationship,
|
* If X is new and persist operations are configured to cascade on the relationship,
|
||||||
@ -542,10 +215,13 @@ associated entity X:
|
|||||||
* If X is removed and persist operations are configured to cascade on the relationship,
|
* If X is removed and persist operations are configured to cascade on the relationship,
|
||||||
an exception will be thrown as this indicates a programming error (X would be re-persisted by the cascade).
|
an exception will be thrown as this indicates a programming error (X would be re-persisted by the cascade).
|
||||||
* If X is detached and persist operations are configured to cascade on the relationship,
|
* If X is detached and persist operations are configured to cascade on the relationship,
|
||||||
an exception will be thrown (This is semantically the same as passing X to persist()).
|
an exception will be thrown (This is semantically the same as passing X to persist()).
|
||||||
|
|
||||||
|
+++ Synchronizing Removed Entities
|
||||||
|
|
||||||
The flush operation applies to a removed entity by deleting its persistent state from the database.
|
The flush operation applies to a removed entity by deleting its persistent state from the database.
|
||||||
No cascade options are relevant for removed entities on flush.
|
No cascade options are relevant for removed entities on flush, the cascade remove option is already
|
||||||
|
executed during `EntityManager#remove($entity)`.
|
||||||
|
|
||||||
+++ The size of a Unit of Work
|
+++ The size of a Unit of Work
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user