Elasticsearch and Symfony : hydrating objects thanks to Transformers

Share Button

Lire la version française

When and why hydrating objects thanks to Transformers?

When you use Elasticsearch and the FosElasticaBundle Finders, the responses received from Elasticsearch are automatically transformed into Doctrine objects.

In some cases, you will need to display some information that come from a join : a photo, a category translation, …
Unfortunately, the hydratation is only done on the objects : FosElasticaBundle transforms the ids that match your search into objects via a select * in (:ids).
The consequence is that, for each entry in your results list, Doctrine will make a query for the join object (for example a Category). For 100 results in 100 different categories, 100 simple queries will be done. You understand what can happen with a complex model and several joins.

Fortunately, you can avoid that by overriding the Transformer.

For the needs of this example (hydrating the Category when fetching an Article), we are going to create a Category object link to an article thank to a oneToMany relationship.

<?php

namespace Obtao\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\ElasticaBundle\Configuration\Search;

class Category
{
     /**
     * @ORM\Column(type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;
   
    /**
     * @ORM\Column(type="string")
     */
    protected $name;
}

And we also specify the relationship on the Article side.

<?php

namespace Obtao\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\ElasticaBundle\Configuration\Search;

class Article
{
     // ... (see http://obtao.com/blog/2014/02/indexing-and-simple-search-with-elasticsearch-and-symfony/)

     /**
     *
     * @ORM\ManyToOne(targetEntity="Obtao\BlogBundle\Entity\Category", inversedBy="articles" ,cascade={"persist"})
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
     */
    protected $category;
}

Create the service

The first step is to create the service which will tranform the ids into Doctrine objects.

The code for this Transformer is quite simple : we build a QueryBuilder with a join on Category and a where id in (:ids).

<?php

namespace Obtao\BlogBundle\Transformer;

use FOS\ElasticaBundle\Doctrine\AbstractElasticaToModelTransformer;
use Doctrine\ORM\Query;

class ElasticaToArticleTransformer extends AbstractElasticaToModelTransformer
{
    /**
     * Fetch objects for theses identifier values
     *
     * @param array $identifierValues ids values
     * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays
     * @return array of objects or arrays
     */
    protected function findByIdentifiers(array $identifierValues, $hydrate)
    {
        if (empty($identifierValues)) {
            return array();
        }
        $hydrationMode = $hydrate ? Query::HYDRATE_OBJECT : Query::HYDRATE_ARRAY;
        $qb = $this->registry
            ->getManagerForClass('ObtaoBlogBundle:Article')
            ->getRepository('ObtaoBlogBundle:Article')
            ->createQueryBuilder('a')
            ->select('a,c')
            ->join('a.category','c');
        /* @var $qb \Doctrine\ORM\QueryBuilder */
        $qb->where($qb->expr()->in('a.'.$this->options['identifier'], ':values'))
            ->setParameter('values', $identifierValues);

        return $qb->getQuery()->setHydrationMode($hydrationMode)->execute();
    }
}

This service declaration use the same arguments as the default Elastica Transformer :
- Doctrine
- the model
- some options
- the property accessor

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="obtao.blog.transformers.elastica.article" class="Obtao\BlogBundle\Transformer\ArticleElasticaToModelTransformer">
            <argument type="service" id="doctrine" />
            <argument /> 
            <argument type="collection" /> 
            <call method="setPropertyAccessor">
                <argument type="service" id="fos_elastica.property_accessor" />
            </call>
            <tag name="fos_elastica.elastica_to_model_transformer" type="article" index="obtao_blog" />
        </service>
    </services>
</container>

Your Transfomer is now ready to be used.

Editing the fos_elastica.yml config file

To tell FosElasticaBUndle to use your Transformer, you must edit your configuration :

# app/config/fos_elastica.yml

article : 
    # ... (see http://obtao.com/blog/2014/02/indexing-and-simple-search-with-elasticsearch-and-symfony/)
    persistence:
    driver: orm
    model: Obtao\BlogBundle\Entity\Article
    elastica_to_model_transformer:
        service: obtao.blog.transformers.elastica.article
    finder: ~
    provider: ~
    listener: ~

From now, when you call the Finder on the Article object, you Transformer will be called and the Categories will be fetch and hydrated.
Many useless queries are avoided.

To know more about Elasticsearch in a Symfony project (but not only), read our other posts

The code used in this post can be downloaded from our sandbox project on Github.

Share Button

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Protected by WP Anti Spam