Заполнение свойств из Embedded другого документа
На днях столкнулись с коллегой с одним занимательным багом. По какой-то причине не заполняся Embedded документ в Doctrine ODM. При всём при этом, когда мы его дампили, то он исправно показывался, а вот до самой MongoDB так и не доходил.
Проблема
Суть баги можно выразить вот таким псевдокодом.
1 2 3 4 5 6 7 |
/* @var Doctrine\ODM\MongoDB\DocumentManager */ $a = $dm->getRepository('Document\SomeDoc')->findOneBy(array('id' = > '50a5468f0d42e2344800003c'); $b = $dm->getRepository('Document\AnotherDoc')->findOneBy(array('id' = > '50a5468f0d42e2344800003c'); $b->setSomeEmbeddedOneField($a->getSomeEmbeddedOneField()); |
После этого при дампе объекта $b в нём в свойстве someEmbeddedOneField лежит наш Embedded документ, а вот при сохранении – ничего не сохраняется.
1 2 |
$dm->persist($b); $dm->flush(); |
Причина проблемы
После долгих ночных изысканий выяснилось, что проблема кроется в том, что доктрина при сохранении документов проходится по их дереву, от родителя к потомкам (Embedded). Когда она добирается до нашего Embedded документа, то получает ссылку на Embedded из объекта a. А ведь он то в базе сохранён и пересохранять его не надо. Поэтому нет запроса. В PHP объекты передаются по ссылке, вот и получается, что у нас в объекте b в поле someEmbeddedOneField стоит ссылка на Embedeed объекта а.
Возможные решения
Костыль
1 |
$this->dm->getUnitOfWork()->clear('Document\SomeEmbeddedDocument'); |
Решение получше
1 2 |
$с = new Document\SomeEmbeddedDocument(); $c->fromArray($a-><wbr />getSomeEmbeddedOneField->toArray(); $b->setSomeEmbeddedOneField($<wbr />c); |
Решение ещё лучше
Просто клонируем объект, полученный из другого документа.
1 2 3 4 5 6 7 |
/* @var Doctrine\ODM\MongoDB\DocumentManager */ $a = $dm->getRepository('Document\SomeDoc')->findOneBy(array('id' = > '50a5468f0d42e2344800003c'); $b = $dm->getRepository('Document\AnotherDoc')->findOneBy(array('id' = > '50a5468f0d42e2344800003c'); $b->setSomeEmbeddedOneField(clone $a->getSomeEmbeddedOneField()); |
А теперь, правильное решение
У предыдушего варианта есть один недостаток. Он клонирует все объекты не разбирая, новые они (клонирование в этом случчае не нужно) или уже есть в БД (клонирование нужно). Поэтому надо узнавать, а сохранён ли объект в БД, т.е. является ли он managed в терминологии Doctrine. Это можно сделать так:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * Проверяет, есть ли в БД уже данный экземпляр Embedded Document * Если есть, то клонирует его. Это предотвращает проблему с записями embedded в документ, при том, * что этот embedded только что взят из другого документа, и есть в БД и не менялся. * @static * @param $document */ public static function getEmbeddedForSetter($document) { if(self::$dm->contains($document)) return clone $document; return $document; } |
А в мутатор добавить вызов этой функции.
1 2 3 4 |
public function setSomeEmbeddedField($document) { $this->someEmbeddedField = getEmbeddedForSetter($document); } |
Да, функцию getEmbeddedForSetter лучше убрать в ваш сервисный слой.
Ссылки
EntityState (это про ORM на самом деле, но прочитать стоит)
Спасибо огромное, очень помогли. Долго не мог понять, почему не сохраняются Embedded документы. Теперь все стало намного понятнее.
Рад помочь, сам с этими граблями долго просидел)