Заполнение свойств из 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 на самом деле, но прочитать стоит)
