Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Doctrine's Many-To-Many Self-Referencing and reciprocity

Tags:

By default, self-referencing ManyToMany relationships under Doctrine involve an owning side and an inverse side, as explained in the documentation.

Is there a way to implement a reciprocal association whithout difference between both sides?

Following the example in the docs:

<?php /** @Entity **/ class User {     // ...      /**      * @ManyToMany(targetEntity="User")      **/     private $friends;      public function __construct() {         $this->friends = new \Doctrine\Common\Collections\ArrayCollection();     }      // ... } 

So, adding entity1 to entity2s friends implies that entity2 will be in entity1s friends.

like image 695
albertedevigo Avatar asked Jan 20 '14 21:01

albertedevigo


1 Answers

There are a number of ways to solve this problem, all depending on what the requirements for the "friends" relation are.

Unidirectional

A simple approach would be to use a unidirectional ManyToMany association, and treat it as if it where a bidirectional one (keeping both sides in sync):

/**  * @Entity  */ class User {     /**      * @Id      * @Column(type="integer")      */     private $id;      /**      * @ManyToMany(targetEntity="User")      * @JoinTable(name="friends",      *     joinColumns={@JoinColumn(name="user_a_id", referencedColumnName="id")},      *     inverseJoinColumns={@JoinColumn(name="user_b_id", referencedColumnName="id")}      * )      * @var \Doctrine\Common\Collections\ArrayCollection      */     private $friends;      /**      * Constructor.      */     public function __construct()     {         $this->friends = new \Doctrine\Common\Collections\ArrayCollection();     }      /**      * @return array      */     public function getFriends()     {         return $this->friends->toArray();     }      /**      * @param  User $user      * @return void      */     public function addFriend(User $user)     {         if (!$this->friends->contains($user)) {             $this->friends->add($user);             $user->addFriend($this);         }     }      /**      * @param  User $user      * @return void      */     public function removeFriend(User $user)     {         if ($this->friends->contains($user)) {             $this->friends->removeElement($user);             $user->removeFriend($this);         }     }      // ...  } 

When you call $userA->addFriend($userB), $userB will be added to the friends-collection in $userA, and $userA will be added to the friends-collection in $userB.

It will also result in 2 records added to the "friends" table (1,2 and 2,1). While this can be seen as duplicate data, it will simplify your code a lot. For example when you need to find all friends of $userA, you can simply do:

SELECT u FROM User u JOIN u.friends f WHERE f.id = :userId 

No need to check 2 different properties as you would with a bidirectional association.

Bidirectional

When using a bidirectional association the User entity will have 2 properties, $myFriends and $friendsWithMe for example. You can keep them in sync the same way as described above.

The main difference is that on a database level you'll only have one record representing the relationship (either 1,2 or 2,1). This makes "find all friends" queries a bit more complex because you'll have to check both properties.

You could of course still use 2 records in the database by making sure addFriend() will update both $myFriends and $friendsWithMe (and keep the other side in sync). This will add some complexity in your entities, but queries become a little less complex.

OneToMany / ManyToOne

If you need a system where a user can add a friend, but that friend has to confirm that they are indeed friends, you'll need to store that confirmation in the join-table. You then no longer have a ManyToMany association, but something like User <- OneToMany -> Friendship <- ManyToOne -> User.

You can read my blog-posts on this subject:

  • Doctrine 2: How to handle join tables with extra columns
  • More on one-to-many/many-to-one associations in Doctrine 2
like image 200
Jasper N. Brouwer Avatar answered Sep 21 '22 23:09

Jasper N. Brouwer