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

Я использую встроенную форму 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 ?

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

Первый

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

 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 здесь

СОВЕТ ДЛЯ ВТОРОГО РЕШЕНИЯ

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

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

Решение объясняется здесь частично: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#why-do-i-get-exceptions-about-unique- ограничительный отказы-во-эм-флеш

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

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

В основном, что я добавил к этому, нужно установить indexedBy = "name" и fetch = "EXTRA_LAZY" на стороне владельца отношения, которое в вашем случае является объектом Article (вам может потребоваться прокрутить блок кода по горизонтали, чтобы увидеть дополнение ) :

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

Вы можете прочитать о опции fetch = "EXTRA_LAZY" здесь: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/extra-lazy-associations.html

Вы можете прочитать информацию о параметре indexBy = «name», выполнив поиск в Google для «Doctrine + indexed association» без кавычек и посмотрите результат, который является частью документов Doctrine (не может размещать другую ссылку, потому что моя репутация недостаточно высока) ,

Затем я изменил свои версии вашего метода 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 coalesce требует PHP7 +.

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

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

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

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