Доктрина "Много-ко-многим" Саморегуляция и взаимность

По умолчанию, отношения с привязкой ManyToMany в рамках Doctrine включают в себя принадлежащую стороне и обратную сторону, как описано в документации.

Есть ли способ реализовать взаимную связь без разницы между обеими сторонами?

Следуя примеру в документах:

<?php
/** @Entity **/
class User
{
    // ...

    /**
     * @ManyToMany(targetEntity="User")
     **/
    private $friends;

    public function __construct() {
        $this->friends = new \Doctrine\Common\Collections\ArrayCollection();
    }

    // ...
}

Итак, добавление entity1 в entity2 friends подразумевает, что entity2 будет находиться в entity1 друзьях.

Ответы

Ответ 1

Существует несколько способов решения этой проблемы: все зависит от того, каковы требования для отношения "друзья".

Однонаправленный

Простым подходом было бы использовать однонаправленную ассоциацию ManyToMany и относиться к ней так, как если бы она была двунаправленной (удерживая обе стороны в синхронизации):

/**
 * @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);
        }
    }

    // ...

}

Когда вы вызываете $userA->addFriend($userB), $userB будет добавлен в коллекцию друзей в $userA, а $userA будет добавлен в коллекцию друзей в $userB.

Это также приведет к добавлению 2 записей в таблицу "друзей" (1,2 и 2,1). Хотя это можно рассматривать как дубликаты данных, это значительно упростит ваш код. Например, когда вам нужно найти всех друзей $userA, вы можете просто сделать:

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

Не нужно проверять 2 разных свойства, как с двунаправленной ассоциацией.

Двунаправленный

При использовании двунаправленной ассоциации объект User будет иметь 2 свойства, например, $myFriends и $friendsWithMe. Вы можете синхронизировать их так же, как описано выше.

Основное отличие состоит в том, что на уровне базы данных у вас будет только одна запись, представляющая отношение (1,2 или 2,1). Это делает запросы "найти всех друзей" более сложными, потому что вам нужно будет проверить оба свойства.

Конечно, вы могли бы использовать 2 записи в базе данных, убедившись, что addFriend() обновит как $myFriends, так и $friendsWithMe (и сохранит другую сторону в синхронизации). Это добавит некоторую сложность в ваших сущностях, но запросы станут немного менее сложными.

OneToMany/ManyToOne

Если вам нужна система, в которой пользователь может добавить друга, но этот друг должен подтвердить, что они действительно друзья, вам нужно сохранить это подтверждение в таблице соединений. Тогда у вас больше нет ассоциации ManyToMany, но что-то вроде User < - OneToMany → Friendship < - ManyToOne → User.

Вы можете прочитать мои сообщения в блоге на эту тему: