BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Implementando Estratégia de Busca em uma aplicação J2EE utilizando AOP

Implementando Estratégia de Busca em uma aplicação J2EE utilizando AOP

Favoritos

Uma aplicação típica J2EE que utiliza uma ferramenta de Mapeamento O/R tem a tarefa de retornar os dados solicitados utilizando um conjunto mínimo de consultas SQL. Mas isto não é sempre uma tarefa fácil. Por padrão as ferramentas de Mapeamento O/R carregam os dados sob demanda a não ser que sejam configuradas para fazer de outra maneira. Este comportamento conhecido como Lazy Loading (ou Carregamento Preguiçoso) assegura que as dependências sejam carregadas somente se forem especificamente requisitadas e consequentemente se torna possível evitar a criação desnecessária de objetos. Lazy loading é útil quando os componentes dependentes não são utilizados em casos de uso de negócio que estamos tentando atender e resolve o problema relacionado ao carregamento desnecessário de componentes.

Tipicamente, nosso caso de uso de negócio sabe qual informação é solicitada. Por conta do lazy loading, a performance do BD reduz quando um grande número de consultas Select são executadas, pois toda informação necessária para o negócio não é recuperada de uma só vez. Isto pode se tornar um gargalo para a aplicação que precisa suportar um grande número de requisições (um problema de escalabilidade).

Vamos analisar um exemplo, um caso de uso de negócio que requer que as informações de uma Pessoa e seu Endereço sejam retornadas. Como o componente Endereço é configurado para carregamento preguiçoso, mais consultas SQL serão executadas para retornar o dado solicitado, por exemplo, primeira a Pessoa e depois seu Endereço. Isto aumenta a comunicação entre o banco de dados e a aplicação. Isto poderia ter sido evitado se fosse carregado ambos componentes, Pessoa e o Endereço em uma única consulta porque nós sabemos que nosso caso de uso requer ambos componentes.

Se nós começarmos a escrever o caso de uso especifico para APIs de Busca no DAO/Repositório e uma camada de serviços de baixo nível, nós precisamos escrever APIs diferentes para retornar e popular o mesmo objeto de domínio com diferentes conjuntos de dados. Isto irá inchar a Camada do repositório ou a Camada de serviços de baixo nível e a manutenção irá se tornar um pesadelo.

Outro problema com lazy loading é que a conexão ao banco de dados tem que ser mantida até que todos os dados necessários sejam retornados ou então a aplicação irá lançar uma exceção relacionada a lazy loading.

Nota: A solução do problema acima não existe se em nossa consulta estivermos retornando os dados de um segundo nível de cache.  No caso da ferramenta de mapeamento O/R Hibernate se estamos utilizando eager fetching (retorno ansioso) para informações de um segundo nível de cachê, ela irá retornar a informação do banco de dados ao invés de retorná-la do cachê mesmo que a informação já esteja disponível no cache de segundo nível. Hibernate não possui uma solução para o problema descrito acima. Isto indica por sua vez, que nós nunca deveríamos utilizar eager fetching em nossas consultas para objetos em cache de segundo nível.  

Uma ferramenta de mapeamento O/R que nos permita ajustar as queries para os objetos que configuramos para cache, irá fazer uma leitura do cache se o objeto já estiver lá, de outro modo o retorno será ansioso. Isto irá resolver o problema de conexão de transação/BD citado acima, uma vez que as informações que estão em cache elas serão também retornadas durante a execução da consulta ao invés de permitir a leitura sob demanda. (ex: lazy loading).

Vamos tomar um código de exemplo para analisar os problemas encontrados ao utilizar lazy loading e a solução de contorno. Vamos considerar um domínio com três entidades Employee (Funcionário), Department (Departamento) e Dependent (Dependente).

Os relacionamentos entre estas entidades são:

  • Funcionário possui zero ou mais dependentes.
  • Departamento possui zero ou mais funcionários.
  • Funcionário pertence a zero ou um departamento.

Nós tomamos três casos de uso para executar:

 

  1. Retornar os detalhes do funcionário.
  2. Retornar o funcionário com os detalhes do dependente.
  3. Retornar o funcionário com os detalhes do departamento.

Os casos de uso acima demandam informações diferentes a serem retornadas e renderizadas. Utilizando lazy loading nós temos as seguintes desvantagens:

 

  • Se nós usarmos a funcionalidade de lazy loading para os dependentes na entidade employee  e nas entidades department no caso de uso 2 e 3, então mais consultas SQL serão disparadas para retornar os dados necessários.
  • A conexão do banco de dados precisa ser mantida durante o ciclo de múltiplas consultas senão uma exceção de lazy loading será lançada, resultando em um uso inadequado dos recursos.

Por outro lado, utilizar retorno ansioso (Eager-fetching) tem as seguintes disvantagens.

 

  • Retorno ansioso das entidades dependentes e departamento para um funcionário irá resultar em um retorno desnecessário de dados.
  • Consultas não podem ser ajustadas para um caso de uso especifico.

Resolvendo os problemas acima, utilizando APIs específicas no Repositório / DAO ou em uma camada de serviço de baixo nível irá resultar em:

 

  • Código Inchado – de ambas as classes Serviço e Repositório/DAO.
  • Pesadelo na Manutenção – para qualquer caso de uso novo, em ambas as camadas de serviço e Repositório/DAO, novas APIs precisam ser adicionadas.
  • Duplicação de Código – Se alguma lógica de negócio precisa ser aplicada em uma entidade retornada da camada de serviço de baixo nível.  De maneira similar o retorno da consulta da camada DAO/Repositório precisa ser checada para disponibilidade de dados antes de retornar as informações...

Para resolver o enigma acima, a camada Repositório/DAO precisa estar ciente da consulta que precisa ser executada para retornar a entidade baseada no caso de uso. Para fazer isto, o padrão de retorno default na classe Repositório/DAO é sobrescrito por um padrão de busca diferente baseado no caso de uso específico conforme definido na classe Aspecto. Todas as classes do padrão de busca implementam a mesma interface.

 

a

 

 



A classe repositório utiliza o padrão de busca acima para retornar a consulta que precisa ser executada conforme mostra o código de exemplo abaixo:

public Employee findEmployeeById(int employeeId) {
	List employee = hibernateTemplate.find(fetchingStrategy.queryEmployeeById(),
		new   Integer(employeeId));
  if(employee.size() == 0)
  return null;
  return (Employee)employee.get(0);
}

A estratégia de busca do funcionário (employee) na classe repositório precisa ser alterada com base no caso de uso requerido. A decisão a respeito de mudar a estratégia de busca na camada de repositório é mantida fora das camadas de repositório e de serviço no aspecto de classe, por isso para adicionar qualquer caso de uso de negócio novo é preciso fazer modificações apenas no aspecto e mais uma implementação de estratégia de busca para o uso do repositório. Aqui nós utilizamos Programação Orientada a Aspecto (AOP) para decidir qual estratégia de busca precisamos utilizar na base do caso de uso de negócio. 

Então, O que é Programação Orientada a Aspecto?

Programação Orientada a Aspectos (AOP) permite uma implementação modularizada dos  conceitos crosscutting que proliferam com a prática: logging, tracing, profiling dinâmico, tratamento de erros, acordos de nível de serviço (SLA), policy enforcement, pooling, caching, controle de concorrência, segurança, gerenciamento de transação, regras de negócio, e assim por diante. Implementações tradicionais destes conceitos requer que você faça uma fusão da implementação com o conceito núcleo de um módulo. Com AOP, você pode implementar cada um destes conceitos em um módulo separado chamado de aspecto. O resultado de tal implementação modular é um design simplificado, melhora no entendimento, melhora na qualidade, redução do time-to-market, e uma resposta esperada para as mudanças de requisito do sistema.

O leitor pode pegar como referência o livro AspectJ in Action de Ramnivas Laddad para um aprofundamento detalhado dos conceitos e programação AspectJ e o livro AspectJ Development Tools para ferramentas aspectj.

Aspecto desempenha um papel muito importante na implementação de uma estratégia de busca. Estratégia de busca é um conceito transversal de nível de negócio e ele muda de acordo com o caso de uso de negócio. Aspect ajuda a decidir qual estratégia de busca precisa ser utilizada em um caso de uso de negócio específico. Aqui, o gerenciamento da decisão da estratégia de busca é mantido do lado de fora do serviço de baixo nível ou da camada de repositório. Como qualquer caso de uso de negócio requer uma estratégia de busca diferente e pode ser aplicada sem a modificação do serviço de baixo nível ou da API repositório.

FetchingStrategyAspect.aj

/**
     Identify the getEmployeeWithDepartmentDetails flow where you need to change the fetching 
     strategy at repository level 
*/
pointcut empWithDepartmentDetail(): call(* EmployeeRepository.findEmployeeById(int))
&& cflow(execution(* EmployeeDetailsService.getEmployeeWithDepartmentDetails(int)));

/**
    When you are at the specified poincut before continuing further update the fetchingStrategy in   
    EmployeeRepositoryImpl to EmployeeWithDepartmentFetchingStrategy
*/
before(EmployeeRepositoryImpl r): empWithDepartmentDetail() && target(r) { 
r.fetchingStrategy = new EmployeeWithDepartmentFetchingStrategy(); 
}

/** 
   Identify the getEmployeeWithDependentDetails flow where you need to change the fetching 
   staratergy at repository level 
*/
pointcut empWithDependentDetail(): call(* EmployeeRepository.findEmployeeById(int))
&& cflow(execution(* EmployeeDetailsService.getEmployeeWithDependentDetails(int)));

/** 
   When you are at the specified poincut before continuing further update the fetchingStrategy in 
    EmployeeRepositoryImpl to EmployeeWithDependentFetchingStrategy 
*/
before(EmployeeRepositoryImpl r): empWithDependentDetail() && target(r) { 
r.fetchingStrategy = new EmployeeWithDependentFetchingStrategy(); 
}

Deste modo, as decisões a respeito de uma consulta em particular que precise ser executada pelo repositório é mantida do lado de fora das camadas de repositório e serviço, os casos de uso novos não requerem uma modificação nas camadas de Repositório ou nos Serviços de baixo nível. A lógica de decisão da consulta que precisa ser executada é um conceito crosscutting que é retido em um Aspecto. Aspecto decide qual estratégia de busca precisa ser injetada no repositório antes da camada de serviço executar a API no repositório baseado no caso de uso do negócio. Por isto podemos utilizar o mesmo serviço e APIs da camada de repositório para satisfazer diferentes requisitos do caso de uso de negócio.

Vamos apresentar este conceito com um exemplo de caso de uso de negócio para busca de um Departamento e os detalhes dos Dependentes de um funcionário. Aqui nós precisamos fazer alterações na nossa camada de serviço do negócio adicionando o caso de uso getEmployeeWithDepartmentAndDependentsDetails(int employeeId). Implemente a nova classe de estratégia de Busca EmployeeWithDepartmentAndDependentFetchingStaratergy que implementa EmployeeFetchingStrategy e sobrescreva a API queryEmployeeById que retorna uma consulta otimizada que auxilia na busca da informação necessária em uma só rajada.

A decisão de injetar a estratégia de busca acima para o caso de uso requerido é implementado em aspect conforme apresentado abaixo.

pointcut empWithDependentAndDepartmentDetail(): call(* EmployeeRepository.findEmployeeById(int))
&& cflow(execution(* EmployeeDetailsService.getEmployeeWithDepartmentAndDependentsDetails(int)));

before(EmployeeRepositoryImpl r): empWithDependentAndDepartmentDetail() && target(r) { 
     r.fetchingStrategy = new EmployeeWithDepartmentAndDependentFetchingStaratergy(); 
}

Como podemos ver nós não modificamos as camadas de repositório e serviço de baixo nível para aplicar o novo caso de uso citado acima. Nós utilizamos aspect e uma implementação de FetchingStrategy para atender ao novo caso de uso de negócio.

Agora vamos discutir o problema relacionado a otimização da consulta para os objetos que são configurados para um cache de segundo nível. Em nosso código de exemplo vamos modificar a entidade departamento para ser configurada em cache de segundo nível. Se nós tentarmos efetuar uma busca ansiosa na entidade departamento então para cada busca do mesmo departamento o banco de dados é acionado para a informação do departamento mesmo que ela esteja disponível em nosso cache de segundo nível. Se nós não buscarmos a entidade departamento em nossa consulta então nossa camada de Negócio (camada do caso de uso) irá participar da transação pois a entidade departamento não esta em cache, pois ela será retornada por lazy loading.

De modo que a demarcação da transação é movida para a camada de negócio a partir de uma camada inferior mesmo que saibamos qual a informação necessária para o caso de uso, mas somente se uma ferramenta de mapeamento O/R não fornecer um mecanismo para resolver o problema acima, por exemplo, retorno ansioso de dados em cache.

Esta abordagem funciona bem para todos os itens de informação que não precisem de cache, mas para os itens de informação de cache dependemos de uma ferramenta de mapeamento O/R que resolva o problema relacionado a dados em cache.

Veja o código fonte em anexo para um exemplo funcional completo que ilustra a estratégia de busca. O arquivo zip contém o exemplo funcional que ilustra todos os cenários discutidos acima. Você pode utilizar qualquer IDE ou rodá-lo a partir de um prompt de comando utilizando um compilador aspectj para executar e testar o código fonte em anexo. Antes de executar tenha certeza que você tenha editado o arquivo jdbc.properties e criado as tabelas necessárias para a aplicação de demonstração.

Você pode utilizar o IDE Eclipse e o plugin AJDT seguindo os seguintes passos:

  1. Descompacte o código fonte baixado e Importe o projeto para dentro do eclipse.
  2. Configure o banco de dados de acordo com a configuração do arquivo jdbc.properties sob Resources/dbscript.
  3. Execute o script resources\dbscript\tables.sql no banco de dados configurado acima que criará as tabelas necessárias para a aplicação de demonstração.
  4. Execute Main.java como uma aplicação AspectJ/Java para criar dados default e testar a implementação da estratégia de busca mencionado acima.

Conclusão

Este artigo apresentou como uma estratégia de busca para otimizar o processo de retorno de dados a partir de um sistema back end de acordo com um caso de uso de maneira modular sem inchar seus serviços de baixo nível ou as camadas de repositório.

Bio: Manjunath R Naganna trabalha atualmente na Alcatel Lucent como Engenheiro de Software Sênior com especialização em design e implementação de aplicações corporativas utilizando Java/J2EE. Ele é mais interessado no framework Spring, Domain Driven Design, Event Driven Architecture e Aspect Oriented Programming. Manjunath gostaria de agradecer a Hemraj Rao Surlu pela edição e formatação do conteúdo. Ele gostaria também de agradecer seus mentores Ramana e Saurabh Sharma por revisar o conteúdo e fornecer um feedback importante.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT