Заполнение свойств из Embedded другого документа

// Декабрь 18th, 2012 // Doctrine 2

На днях столкнулись с коллегой с одним занимательным багом. По какой-то причине не заполняся Embedded документ в Doctrine ODM. При всём при этом, когда мы его дампили, то он исправно показывался, а вот до самой MongoDB так и не доходил.

Проблема

Суть баги можно выразить вот таким псевдокодом.

/*  @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 документ, а вот при сохранении — ничего не сохраняется.

$dm->persist($b);
$dm->flush();

Причина проблемы

После долгих ночных изысканий выяснилось, что проблема кроется в том, что доктрина при сохранении документов проходится по их дереву, от родителя к потомкам (Embedded). Когда она добирается до нашего Embedded документа, то получает ссылку на Embedded из объекта a. А ведь он то в базе сохранён и пересохранять его не надо. Поэтому нет запроса. В PHP объекты передаются по ссылке, вот и получается, что у нас в объекте b в поле someEmbeddedOneField стоит ссылка на Embedeed объекта а.

Возможные решения

Костыль

Это было моё первое решение, но оно мне не очень понравилось, поэтому я начал искать дальше.
$this->dm->getUnitOfWork()->clear('Document\SomeEmbeddedDocument');
С ним всё работает, однако он сбрасывает статусы для ВСЕХ экземпляров этого класса в течении HTTP-запроса. Т.е. потенциальное поле для багов. Поэтому и стал копать дальше.

Решение получше

Делаем новый объект, и передаём туда значения.
$с = new Document\SomeEmbeddedDocument();
$c->fromArray($a->getSomeEmbeddedOneField->toArray(); $b->setSomeEmbeddedOneField($c); 

Решение ещё лучше

Просто клонируем объект, полученный из другого документа.

/*  @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. Это можно сделать так:

/**
* Проверяет, есть ли в БД уже данный экземпляр Embedded Document
* Если есть, то клонирует его. Это предотвращает проблему с записями embedded в документ, при том,
* что этот embedded только что взят из другого документа, и есть в БД и не менялся.
* @static
* @param $document
*/
public static function getEmbeddedForSetter($document)
{
  if(self::$dm->contains($document))
    return clone $document;
  return $document;
}

А в мутатор добавить вызов этой функции.

public function setSomeEmbeddedField($document)
{
$this->someEmbeddedField = getEmbeddedForSetter($document);
}

Да, функцию getEmbeddedForSetter лучше убрать в ваш сервисный слой.

Ссылки

EntityState (это про ORM на самом деле, но прочитать стоит)

Share

Спасибо!


Если вам помогла статья, или вы хотите поддержать мои исследования и блог - вот лучший способ сделать это:


2 Responses to “Заполнение свойств из Embedded другого документа”

  1. Александр:

    Спасибо огромное, очень помогли. Долго не мог понять, почему не сохраняются Embedded документы. Теперь все стало намного понятнее.

Комментировать