Data

11 ago, 2015

Melhorando o uso de um banco de dados MongoDB com a ajuda de Symfony Listeners

Publicidade

Às vezes, os aplicativos precisam filtrar grandes quantidades de informação para mostrar ao usuário um pequeno subconjunto de dados relevantes.

No entanto, quando a quantidade de dados para filtrar é muito grande, pode acabar não sendo viável filtrar as informações recuperando os dados inteiros na memória.

Leia este artigo para aprender sobre uma abordagem alternativa utilizando um documento MongoDB e Symfony Listeners para limitar a quantidade de dados que precisa ser percorrida na memória.

O problema

Recentemente, enfrentei, em um projeto real, o desafio de lidar com quantidades grandes de dados em solicitações HTTP. Para contextualizar, imagine uma quantidade enorme de registros categorizados por duas taxonomias diferentes: catalog e bundle.

Para dar ao usuário a possibilidade de filtrar por catalog ou bundle, o aplicativo tem que ler todos os registros e gerar uma matriz e, em seguida, enviá-la para a visualização para, por exemplo, colocar essa informação na barra lateral.

Quando a quantidade de dados é muito alta, obviamente não é recomendado continuar usando essa abordagem.

Usando listerners para processar dados

Pensando em como esse problema pode ser resolvido, lembrei-me do que aprendi com os Listeners do Doctrine e Event Subscribers. Agora é hora de aplicar esse conhecimento.

Existe uma solução melhor, e ela consiste em escrever, atualizar e carregar os dados de um banco de dados. Como eu já mencionei acima, para obter todos os catalogs e bundles de um projeto concreto, eu tenho que ler todas as traduções desse projeto e preparar uma matriz para enviar para a visualização.

Translation-load-persist

A primeira coisa que podemos fazer é criar um novo documento no MongoDB que irá conter as informações de que precisamos.

ProjectInfo

Para atualizar esse documento quando uma nova tradução for adicionada ou quando as traduções existentes forem atualizadas, precisamos preparar um listener para fazer o trabalho pesado.

[src/Acme/SampleBundle/Resources/config/services.xml]

<?xml version="1.0" ?>
<container ...>
 <services>
  <service id="translations_listener"
   class="Acme\SampleBundle\Listener\SampleListener">
   <tag name="doctrine_mongodb.odm.event_listener" 
    event="postLoad" connection="default" />
   <tag name="doctrine_mongodb.odm.event_listener" 
    event="postPersist" connection="default" />
   <tag name="doctrine_mongodb.odm.event_listener" 
    event="postUpdate" connection="default" />
   <tag name="doctrine_mongodb.odm.event_listener" 
    event="preRemove" connection="default" />
  </service>
 </services>
</container>

A classe listener abaixo implementa os eventos de ciclo de vida:

<?php

namespace Acme\SampleBundle\Listener;

use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
use Doctrine\ODM\MongoDB\Event\PreFlushEventArgs;
use Acme\SampleBundle\Document\Summary;
use Acme\SampleBundle\Document\MainDocument;

class SampleListener
{
 /**
  * To maintain the category and catalog of the last viewed
  */
 protected $cache;
 /**
  * @param LifecycleEventArgs $eventArgs
  */
 public function postUpdate(LifecycleEventArgs $eventArgs)
 {
  $document = $eventArgs->getDocument();
  $dm    = $eventArgs->getDocumentManager();
  if ($document instanceof MainDocument) {
   /** @var Translation $document */
   $projectId   = $document->getProjectId();
   $projectInfo = $dm->getRepository('SampleBundle:ProjectInfo')
 ->getProjectInfo($projectId);
   if(!$projectInfo){
    $projectInfo = new ProjectInfo();
    $projectInfo->setProjectId($projectId);
   }
   if(isset($this->cache["id"]) && 
   ($this->cache["id"]==$document->getId())){
    $projectInfo->subBundle($this->cache['bundle']);
    $projectInfo->subCatalog($this->cache['catalog']);
   }
   $projectInfo->addBundle($document->getBundle());
   $projectInfo->addCatalog($document->getCatalog());
   $dm->persist($projectInfo);
   $dm->flush();
  }
 }
 /**
  * @param LifecycleEventArgs $eventArgs
  */
 public function postPersist(LifecycleEventArgs $eventArgs)
 {
  $document = $eventArgs->getDocument();
  $dm    = $eventArgs->getDocumentManager();
  if ($document instanceof Translation) {
   /** @var Translation $document */
   $projectId   = $document->getProjectId();
   $projectInfo = $dm->getRepository('SampleBundle:ProjectInfo')
 ->getProjectInfo($projectId);
   $projectInfo->addBundle($document->getBundle());
   $projectInfo->addCatalog($document->getCatalog());
   $dm->persist($projectInfo);
   $dm->flush();
  }
 }
 /**
  * @param LifecycleEventArgs $eventArgs
  */
 public function preRemove(LifecycleEventArgs $eventArgs)
 {
  $document = $eventArgs->getDocument();
  $dm    = $eventArgs->getDocumentManager();
  if ($document instanceof Translation) {
   /** @var Translation $document */
   $projectId   = $document->getProjectId();
   $projectInfo = $dm->getRepository('SampleBundle:ProjectInfo')
 ->getProjectInfo($projectId);
   $projectInfo->subBundle($document->getBundle());
   $projectInfo->subCatalog($document->getCatalog());
   $dm->persist($projectInfo);
   $dm->flush();
  }
 }
 /**
  * @param LifecycleEventArgs $eventArgs
  */
 public function postLoad(LifecycleEventArgs $eventArgs)
 {
  $document = $eventArgs->getDocument();
  if ($document instanceof Translation) {
   $this->cache = array(
    'id'   => $document->getId(),
    'bundle'  => $document->getBundle(),
    'catalog' => $document->getCatalog(),
   );
   return;
  }
  // if document ...
 }
}

Agora, a única coisa restante que temos a fazer é criar um repositório e ler as novas informações do novo documento resumido, evitando, assim, que quantidades excessivas de dados sejam lidas.

<?php

namespace Acme\SampleBundle\Document\Repository;
use Doctrine\ODM\MongoDB\DocumentRepository;

class ProjectInfoRepository extends DocumentRepository
{
 /**
  * @param $projectId
  * @param bool $sorted
  * @return mixed
  */

 public function getCatalogs($projectId, $sorted = true)
 {
  $projectInfo = $this->getProjectInfo($projectId);
  if(!$projectInfo instanceof ProjectInfo){
   return array();
  }
  $result = $projectInfo->getCatalogs();
  if ($sorted && is_array($result)) {
   ksort($result);
  }
  return $result;
 }

 /**
   * @param $projectId
   * @param bool $sorted
   * @return mixed
   */
 public function getBundles($projectId, $sorted = true)
 {
  $projectInfo = $this->getProjectInfo($projectId);
  if(!$projectInfo instanceof ProjectInfo){
   return array();
  }
  $result = $projectInfo->getBundles();
  if ($sorted && is_array($result)) {
   ksort($result);
  }
  return $result;
 }

 /**
   * @param $projectId
   * @return ProjectInfo
   */
 public function getProjectInfo($projectId)
 {
  $dm = $this->getDocumentManager();
  return $dm->getRepository('SampleBundle:ProjectInfo')
   ->findOneBy(array('projectId' => intval($projectId)));
 }
}

Este diagrama demonstra o que acontecerá agora, quando você persistir em um registro de tradução:

Persist-diagram

E para obter os bundles ou catalogs de traduções, você não precisa mais ler todos os registros de traduções e processar suas informações na memória. Você só precisa ler os registros do documento ProjectInfo para obter as informações atualizadas que eles contêm.

Todo o código apresentado aqui está trabalhando em um projeto real chamado tradukoj. Lancei recentemente uma versão pública.

Se você tiver dúvidas ou comentários, basta postar um comentário aqui.

***

Joseluis Laso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://www.phpclasses.org/blog/post/268-Improving-the-use-of-a-MongoDB-database-with-the-help-of-Symfony-Listeners.html