Пару слов про Doctrine 2 Identity Map

// Март 27th, 2013 // Doctrine 2, NoSQL

cache-iconНу что мои траварищи, эту заметку я пишу после тяжелого трудового дня в поисках одного коварного бага. Было тяжело, но интересно. Я познал, так сказать, прелести внутреннестей и внутренности прелестей Doctrine 2 ODM.Дело было так. Есть страничка, как полагается контроллер, экшен, вид. В виде вызывается несколько блоков. Понадобилось мне получить в контроллере ещё кое какие данные, а именно MongoId одного объекта по его ID из MySQL. В контроллере делаем запрос:

$qb = $this->_dm->getRepository('App\DefaultBundle\Document\SomeDoc')->createQueryBuilder();
$result = $qb->select('id')->field('sqlId')->equals($sqlId)->getQuery()

Вроде бы всё просто. Получаем MongoId по SqlId. Однако после этого почему-то падал один из блоков. Не буду вдаваться в подробности, расскажу лишь, что у нас у документа SomeDoc есть параметр slug.

/** @ODM\String @ODM\Index */
protected $slug;

 

Вот с такими аксессорами и мутаторами.

  public function getSlug()
        {
            if(!$this->slug){
                $this->setSlug();
            }
            return $this->slug;
        }

  public function setSlug($slug = '')
  {
      if (empty($slug)) {
          $slug = $this->name;
      }
      $this->slug = strtolower(\Service::translit($slug));
      return $this;
  }

Идея то была хорошаая. Можно вызовом $someDoc->getSlug() получать slug, а если его нет — то автоматически генерить и возвращать. Но это всё в теории… А на практике….

Засада

idMapperSketch

В основном контроллере я получал объект SomeDoc, потом в виде блока делал вызов getSlug() а ещё дальше в коде выполнялось сохранение объекта. А засада состоит в том, что код работает примерно так:

  1. Получаем документ по sqlId. Сохраняем в IdentityMap. Это вроде кэша доктрины для одинаковых сущностей в контексте запроса. При этом, у этого объекта заполнено только поле id. Мы ведь не просили другие :-)
  2. В виде вызываем getSlug(), хм, ну надо же слага нет, заполняем его пустой строкой.
  3. Сохраняем.
  4. PROFIT! EPIC FAIL!

В разных местах юзается один и тот же объект, а т.к. он уже был загружен ранее — то второй вызов не грузит его. Проверить это можно сохранив первый объектв например в Zend_Registry, и сдампив его второй раз потом.

Решение

Ну понятно, что надо модифицировать вызов getSlug(), чтобы он нам такую вот заподляночку не делал. Я долго копался в Doctrine и сделал так:

public function getSlug()
{
/* Если загрузить объект без поля slug, а потом где-то вызовется аксессор getSlug() например в виде
* а потом объект где-то сохранится, то получим пустой slug без этой строчки
* при этом необязательно его напарямую пеердавать, это может быть по прямой ссылке
*/
/** @var $dm \Doctrine\ODM\MongoDB\DocumentManager */
$dm = \AppKernel::getKernel()->getContainer()->get('doctrine_mongodb.odm.default_document_manager');

/*
* Делать обновление данных из БД только для обычных документов (не Embedded)
*/
/** @var $metadata \Doctrine\ODM\MongoDB\Mapping\ClassMetadata */
$metadata = $dm->getClassMetadata(__NAMESPACE__.'\\'.$this->getClassName());
$isManaged = ($dm->getUnitOfWork()->getDocumentState($this) === \Doctrine\ODM\MongoDB\UnitOfWork::STATE_MANAGED);
if(!$metadata->isEmbeddedDocument && $isManaged)
$dm->refresh($this);

if(!$this->slug){
$this->setSlug();
}

return $this->slug;
}

Иными словами мы при вызове getSlug() просто догружаем данные из БД в случае, когда документ в состоянии MANAGED (получен из БД, а не инстанцирован в коде) и если он не Embedded. После обновления геттеров и сеттеров не забудьте перегенерить прокси-классы.

Ссылки

Entities and the Identity Map

Share

Спасибо!


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


3 Responses to “Пару слов про Doctrine 2 Identity Map”

  1. Используй события, Люк, и да прибудет с тобою сила!

    Костыль начался еще с «$this->slug = strtolower(\Service::translit($slug));» в getSlug(), и явно проявил себя при необходимости заюзать EM в сущности.

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