Как избежать дублирования записей в отношениях «многие ко многим» с Doctrine?

Я использую встроенную форму Symfony для добавления и удаления Tag объектов справа от редактора статьи. Article — это владеющая сторона в ассоциации:

class Article
{
    /**
     * @ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"})
     */
    private $tags;

    public function addTag(Tag $tags)
    {
        if (!$this->tags->contains($tags)) // It is always true.
            $this->tags[] = $tags;
    }
}

Условие здесь не поможет, так как оно всегда верно, а если бы это было не так, то новые теги вообще не сохранялись бы в базе данных. Вот объект Tag:

class Tag
{
    /**
     * @Column(unique=true)
     */
    private $name

    /**
     * @ManyToMany(targetEntity="Articles", mappedBy="tags")
     */
    private $articles;

    public function addArticle(Article $articles)
    {
        $this->articles[] = $articles;
    }
}

Я установил $name как уникальный, потому что хочу использовать один и тот же тег каждый раз, когда ввожу одно и то же имя в форму. Но это не работает таким образом, и я получаю исключение:

Нарушение ограничения целостности: 1062 Дублирующаяся запись

Что мне нужно изменить, чтобы использовать article_tag, таблицу соединения по умолчанию при отправке имени тега, которое уже находится в таблице Tag?


person Gergő    schedule 11.02.2014    source источник
comment
взгляните на @UniqueEntity для проверки уникальности - symfony.com/doc/current/ ссылка/ограничения/UniqueEntity.html   -  person Michael Radionov    schedule 12.02.2014
comment
это полезно для получения красивой страницы с ошибкой, но не решает проблему   -  person nicolallias    schedule 12.04.2016


Ответы (2)


Я боролся с подобной проблемой в течение нескольких месяцев и, наконец, нашел решение, которое, похоже, очень хорошо работает в моем приложении. Это сложное приложение с довольно большим количеством ассоциаций «многие ко многим», и мне нужно обрабатывать их с максимальной эффективностью.

Решение частично объясняется здесь: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#why-do-i-get-exceptions-about-unique-constraint-failures-during-em-flush

Вы уже были на полпути со своим кодом:

public function addTag(Tag $tags)
{
    if (!$this->tags->contains($tags)) // It is always true.
        $this->tags[] = $tags;
}

По сути, я добавил к этому установку indexedBy=name и fetch=EXTRA_LAZY на стороне-владельце отношений, которой в вашем случае является Article. strong> сущность (вам может потребоваться прокрутить блок кода по горизонтали, чтобы увидеть добавление):

class Article
{
    /**
     * @ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"}, indexedBy="name" fetch="EXTRA_LAZY")
     */
    private $tags;

Вы можете прочитать о fetch=EXTRA_LAZY здесь.

Вы можете прочитать о опции indexBy=name здесь.

Затем я изменил свои версии вашего метода addTag() следующим образом:

public function addTag(Tag $tags)
{
    // Check for an existing entity in the DB based on the given
    // entity's PRIMARY KEY property value
    if ($this->tags->contains($tags)) {
        return $this; // or just return;
    }
    
    // This prevents adding duplicates of new tags that aren't in the
    // DB already.
    $tagKey = $tag->getName() ?? $tag->getHash();
    $this->tags[$tagKey] = $tags;
}

ПРИМЕЧАНИЕ. Для оператора объединения ?? null требуется PHP7+.

Установив стратегию выборки для тегов на EXTRA_LAZY, следующий оператор заставляет Doctrine выполнить запрос SQL, чтобы проверить, существует ли тег с таким же именем в БД (см. EXTRA_LAZY ссылка выше для получения дополнительной информации):

$this->tags->contains($tags)

ПРИМЕЧАНИЕ. Это может вернуть true, только если установлено поле PRIMARY KEY объекта, переданного ему. Doctrine может запрашивать существующие объекты в базе данных/карте объектов только на основе ПЕРВИЧНОГО КЛЮЧА этого объекта при использовании таких методов, как ArrayCollection::contains(). Если свойство name объекта Tag является всего лишь UNIQUE KEY, возможно, именно поэтому оно всегда возвращает значение false. Вам понадобится PRIMARY KEY, чтобы эффективно использовать такие методы, как contains().

Остальная часть кода в методе addTag() после того, как блок if создает ключ для коллекции ArrayCollection тегов либо по значению в свойстве PRIMARY KEY (предпочтительно, если не null) или по хэшу сущности Tag (найдите в Google PHP + spl_object_hash, используемый Doctrine для индексации сущностей). Итак, вы создаете индексированную ассоциацию, так что если вы добавите один и тот же объект дважды перед сбросом, он будет просто повторно добавлен с тем же ключом, но не дублирован.

person garethlawson    schedule 11.01.2017

Два основных решения

Первый

Используйте преобразователь данных

class TagsTransformer implements DataTransformerInterface
{
    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    /**
     * used to give a "form value"
     */
    public function transform($tag)
    {
        if (null === $tag) {
            //do proper actions
        }

        return $issue->getName();
    }

    /**
     * used to give "a db value"
     */
    public function reverseTransform($name)
    {
        if (!$name) {
            //do proper actions
        }

        $issue = $this->om
            ->getRepository('YourBundleName:Tag')
            ->findOneBy(array('name' => $name))
        ;

        if (null === $name) {
            //create a new tag
        }

        return $tag;
    }
}

Второй

Используйте обратный вызов жизненного цикла. В частности, вы можете использовать триггер prePersist для вашего объекта article? Таким образом, вы можете проверить наличие уже существующих tags и позволить вашему entity manager управлять ими за вас (чтобы ему не нужно было постоянно вызывать ошибки).

Подробнее о prePersist можно узнать здесь.

ПОДСКАЗКА ДЛЯ ВТОРОГО РЕШЕНИЯ

Создайте собственный метод репозитория для поиска и извлечения старых тегов (если есть)

person DonCallisto    schedule 11.02.2014
comment
Теги сохраняются перед событием prePersist сущности Article, поэтому я не могу просто перебрать $tags, чтобы проверить, существуют ли они уже (с пользовательским TagRepository). Как тогда мне подключить пользовательский метод репозитория к обратному вызову жизненного цикла? Разве не нужно использовать прослушиватели событий? - person Gergő; 21.02.2014
comment
@Gergő: Я полагаю, что prePersist можно классифицировать как прослушиватель событий, но я не уверен ... Однако, если теги сохраняются перед статьями, вам следует следовать первому предложенному мной решению. - person DonCallisto; 21.02.2014
comment
Я создал преобразователь данных, который успешно возвращает существующие объекты тегов, но Doctrine по-прежнему пытается выполнить INSERT вместо UPDATE, поэтому просто генерирует исключение. Как я могу это исправить? - person Gergő; 26.03.2014
comment
@Gergő: ты точно делаешь что-то не так, но я не знаю что. Если вы попытаетесь сохранить предварительно выбранный объект базы данных, это приведет к ОБНОВЛЕНИЮ, это логика доктрины. - person DonCallisto; 26.03.2014
comment
Я задал новый вопрос об исключении с более подробной информацией. - person Gergő; 26.03.2014
comment
@DonCallisto - Что касается второго решения, как вы получаете доступ к репо внутри объекта? - person BentCoder; 28.06.2014
comment
Привет @Gergő, ты решил эту проблему? У меня та же проблема, и я не знаю, как ее исправить, пожалуйста, если вы можете посмотреть здесь - person Joseph; 16.12.2015
comment
Привет @DonCallisto, у меня такая же проблема, и я не знаю, как ее исправить, пожалуйста, если вы можете посмотреть здесь - person Joseph; 17.12.2015
comment
Для меня, если вы попытаетесь сохранить предварительно выбранный объект базы данных, это приведет к ОБНОВЛЕНИЮ, является ли логика доктрины золотым правилом. Моя логика сильно отличается от приведенного выше вопроса и ответа, но важно убедиться, что вы сначала попытались получить объект db перед сохранением - тогда этих раздражающих Doctrine больше не будет. - person MusikAnimal; 27.11.2017