Избегание рекурсии с сущностями Doctrine и JMSserializer

Я создаю REST API, используя Symfony2, Doctrine, FOSRestBundle и JMSSerializer.

Проблема, с которой я сталкиваюсь, заключается в том, что при сериализации моих объектов сериализатор извлекает любые связанные объекты. Например, для задачи, которая является частью истории, которая является частью доски, поэтому при сериализации задачи я получаю вывод, который включает в себя историю, включающую доску, которая затем включает все остальные истории на доске.

Есть ли простой способ ограничить это и вместо этого просто включить иностранные идентификаторы?


person WayneC    schedule 07.08.2012    source источник
comment
Я думаю, что здесь был размещен тот же вопрос: stackoverflow.com/questions/6706485/ . Вы найдете некоторые ответы, используя JMSSerializer или нет.   -  person Julien Fastré    schedule 13.08.2012


Ответы (5)


Проверьте файл Serializer/Handler/DoctrineProxyHandler.php на JMSSerializerBundle. Теперь, если вы прокомментируете эту строку:

public function serialize(VisitorInterface $visitor, $data, $type, &$handled)
    {
        if (($data instanceof Proxy || $data instanceof ORMProxy) && (!$data->__isInitialized__ || get_class($data) === $type)) {
            $handled = true;

            if (!$data->__isInitialized__) {
                //$data->__load();
            }

Это остановит ленивую загрузку ваших объектов. Если это то, что вы ищете, просто продолжайте и создайте свой собственный обработчик где вы не лениво загружаете.

Если это не так, я рекомендую вам настроить свои сущности перед отправкой их в JMSSerializerBundle по своему вкусу. Например, в любых связанных сущностях мне нужен идентификатор, а в других мне нужно имя пользовательского столбца, такое как код, имя или что-то еще.

Я просто создаю копию своего объекта сущности, а затем начинаю получать поля, необходимые для отношений. Затем я сериализую эту копию. JMSSerializerBundle не будет лениво загружаться, потому что я уже указал нужные поля.

person Klaus S.    schedule 15.08.2012
comment
Похоже, это может сработать для меня, у меня не было возможности поработать над этим на этой неделе, но посмотрю на этих выходных. - person WayneC; 16.08.2012
comment
Уже понял это несколько месяцев назад. Спасибо, что украл это. stackoverflow .com/questions/11575345/ - person Christian Huber; 17.10.2012

Используйте политику исключения JMS.

Пример использования аннотаций к объекту категории, где вы не хотите включать дочерние объекты и объекты, связанные с продуктом:

use ...
    JMS\SerializerBundle\Annotation\ExclusionPolicy,
    JMS\SerializerBundle\Annotation\Exclude,
    ...;

/**
 * ...
 * @ExclusionPolicy("none")
 */
class Category
{
   /**
    * ...
    * @Exclude
    */
   private $children;

   /**
    * ...
    * @Exclude
    */
   private $products;

}

Дополнительные сведения см. в документах по JMSSerializer. .

ИЗМЕНИТЬ:

Например, вы можете использовать частичное ключевое слово, чтобы выбрать только те данные, которые вам нужны. Хотя я не мог, на всю жизнь, отключить загрузку полных связанных объектов (на два уровня ниже), если я передаю объект объекта в сериализатор (даже при отключении загрузки в DoctrineProxyHandler), но если я использую массив, чем он не использует ленивую загрузку доктрины через прокси (как и ожидалось от c).

Пример с использованием ваших примеров объектов:

$dql = "SELECT t, s, partial b.{id}, partial ss.{id}
        FROM Acme\AppBundle\Entity\Task t
        JOIN t.story s
        JOIN s.board b
        JOIN b.stories ss"

$q = $this->_em-createQuery($dql);

$result = $q->getArrayResult();

Таким образом, вы получите что-то вроде:

[
{
    id: 33,
    title: "My Task",
    story: [
    {
        id: 554,
        board: [
        {
            id: 14,
            stories: [
            {
                id: 554
            },
            {
                id: 3424
            },
            {
                id: 3487
            }
            ]
        }
        ]
    }
    ]

}
]

P.S. Я действительно заинтригован этой "проблемой". В любом случае я постараюсь придумать решение, как сериализовать объект объекта без использования результата массива.

person Marko Jovanović    schedule 14.08.2012
comment
Дружественное обновление: начиная с версии 0.11 Ядро JMS Seralizer было извлечено из пакета, поэтому вместо JMS\SerializerBundle\Annotation\ExclusionPolicy будет JMS\Serializer\Annotation\ExclusionPolicy, а JMS\SerializerBundle\Annotation\Exclude будет JMS\Serializer\Annotation\Exclude. - person Kirill Fuchs; 26.04.2013

Просто обновление в последней версии JMSSerializer, место, на которое вы должны обратить внимание, это

JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber

вместо

Сериализатор\обработчик\DoctrineProxyHandler

Чтобы переопределить поведение ленивой загрузки по умолчанию, нужно определить своего собственного подписчика событий.

В вашем приложении/config.yuml добавьте это:

parameters:
    ...
    jms_serializer.doctrine_proxy_subscriber.class: Your\Bundle\Event\DoctrineProxySubscriber

вы можете скопировать класс из JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber в Your\Bundle\Event\DoctrineProxySubscriber и закомментировать $object->__load(); линия

public function onPreSerialize(PreSerializeEvent $event)
{
    $object = $event->getObject();
    $type = $event->getType();

    // If the set type name is not an actual class, but a faked type for which a custom handler exists, we do not
    // modify it with this subscriber. Also, we forgo autoloading here as an instance of this type is already created,
    // so it must be loaded if its a real class.
    $virtualType = ! class_exists($type['name'], false);

    if ($object instanceof PersistentCollection) {
        if ( ! $virtualType) {
            $event->setType('ArrayCollection');
        }

        return;
    }

    if ( ! $object instanceof Proxy && ! $object instanceof ORMProxy) {
        return;
    }

     //$object->__load(); Just comment this out

    if ( ! $virtualType) {
        $event->setType(get_parent_class($object));
    }
}

Обновление: в итоге я написал собственную упрощенную версию инструмента сериализации: https://github.com/dlin-me/array-converter-bundle

person David Lin    schedule 29.05.2013
comment
Мне нравится это решение, однако оно не работает для меня. Я создал свой собственный DoctrineProxySubscriber и проверил, используется ли он во время сериализации, но никогда не достигает закомментированной строки и всегда лениво загружает связанные объекты. Я не хочу, чтобы сериализатор делал запросы к БД, но, с другой стороны, я хочу, чтобы он сериализовал уже извлеченные данные. Буду признателен за любые идеи. - person Radzikowski; 22.05.2014
comment
Я прокомментировал строку $object-›__load, и все пошло почти так, как мне было нужно... теперь она возвращает id_thing : { id_thing : 1}, вставленный в объект... есть ли способ вернуть только id_thing : 1? - person KnF; 12.09.2014
comment
Ах, вот еще что... Я не использую Symfony2... только JMSSerializer и Doctrine... Как мне создать пользовательского подписчика? - person KnF; 12.09.2014
comment
@КнФ zf2? если это так, добавьте 'jms_serializer.doctrine_proxy_subscriber' => 'ApplicationRest\EventDispatcher\Subscriber\DoctrineProxySubscriber' под service_manager['invokables'] в ваш module.config.php - person Marcel Djaman; 23.04.2015
comment
@MarcelDjaman Нет, никакой фреймворк не используется. Просто доктрина и JMSSerializer с Composer.. - person KnF; 29.04.2015
comment
Понял.. $serializer = JMS\Serializer\SerializerBuilder::create()->configureListeners(function(EventDispatcher $dispatcher) {$dispatcher->addSubscriber(new \MyBundle\ProxySubscriber($this->container));})->build(); - person KnF; 29.04.2015

Вот функция для выбора идентификаторов связанных объектов «один к одному» или «один ко многим» общим способом без использования объединений.

function selectWithAssociations($doctrine, $className) {

    $em = $doctrine->getManager();
    $meta = $em->getClassMetadata($className);

    //explicitly get IDs of associated entities
    $assocClauses = array();
    foreach ($meta->getAssociationMappings() as $assocName => $assoc) {
        if (isset($assoc['joinTable'])) {
            //todo: doesn't handle many to many associations
        } else {
            $assocClauses[] = ", IDENTITY(e.$assocName) AS $assocName";
        }
    }

    //run custom DQL query
    $q = $em->createQuery('SELECT e AS _d' . implode('', $assocClauses) . ' FROM ' . $className . ' e');
    $result = $q->getArrayResult();

    return $result;
}
person Tamlyn    schedule 09.12.2013

Вот класс, который предотвращает ленивую загрузку одной или многих ассоциаций, которые можно использовать в качестве JMS Serializer ExclusionStrategy.

use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\Proxy;
use JMS\Serializer\Context;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\SerializationContext;

/**
 * Class OnlyLoadedAssociationsExclusionStrategy
 *
 * http://stackoverflow.com/questions/11851197/avoiding-recursion-with-doctrine-entities-and-jmsserializer
 */
class OnlyLoadedAssociationsExclusionStrategy implements ExclusionStrategyInterface
{
    public function shouldSkipClass(ClassMetadata $metadata, Context $context)
    {
    }

    public function shouldSkipProperty(PropertyMetadata $property, Context $context)
    {
        if ($context instanceof SerializationContext){
            $vistingSet=$context->getVisitingSet();

            //iterate over object to get last object
            foreach ($vistingSet as $v){
                $currentObject=$v;
            }

            $propertyValue=$property->getValue($currentObject);

            if ($propertyValue instanceof Proxy){
                // skip not loaded one association
                if (!$propertyValue->__isInitialized__){
                    return true;
                }
            }

            if ($propertyValue instanceof PersistentCollection){
                // skip not loaded many association
                if (!$propertyValue->isInitialized()){
                    return true;
                }
            }
        }
        return false;
    }
}

Пример использования:

$serializationContext->addExclusionStrategy(
     new OnlyLoadedAssociationsExclusionStrategy()
);
person Pavel Sokolov    schedule 24.08.2015
comment
Также опубликовано здесь: stackoverflow.com/questions/11575345/ - person Pavel Sokolov; 24.08.2015
comment
Вам следует избегать копирования и вставки ответов на несколько вопросов. Когда вы это сделаете, флаг автоматически поднимается для внимания модератора. Вместо этого просто один из вопросов как дубликат другого. - person CubeJockey; 24.08.2015