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
|
||||
+ Inheritance Mapping
|
||||
+ Working with objects
|
||||
+ Working with associations
|
||||
+ Transactions and Concurrency
|
||||
+ Events
|
||||
+ Batch processing
|
||||
|
@ -57,7 +57,9 @@ In all the examples of many-valued associations in this manual we will make use
|
||||
|
||||
++ 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"
|
||||
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.
|
||||
|
||||
+++ 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
|
||||
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
|
||||
> `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
|
||||
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
|
||||
|
||||
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
|
||||
> 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
|
||||
|
||||
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
|
||||
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 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:
|
||||
|
||||
* 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,
|
||||
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,
|
||||
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.
|
||||
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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user