BT

Spring Data: A solução mais geral para persistência?

Postado por Tobias Trelle , traduzido por Marcelo Cenerino em 03 Abr 2013 |

O SpringData é um projeto da SpringSource com proposta de unificar e facilitar o acesso a diferentes tecnologias de armazenamento de dados, como bancos de dados relacionais e os NoSQL.

Independentemente da solução de armazenamento, as classes de "repositório" (também conhecidas como Data Access Objects ou DAOs) normalmente disponibilizam operações CRUD (Create-Read-Update-Delete) para um determinado objeto de domínio, além de métodos de pesquisa e funcionalidades de ordenação e paginação. O SpringData disponibiliza interfaces genéricas para esses aspectos (CrudRepository e PagingAndSortingRepository), além de implementações específicas para cada banco de dados. Uma visão geral da arquitetura do Spring Data é mostrada na figura abaixo.

É provável que o leitor tenha utilizado algum dos templates do Spring (como o JdbcTemplate) para escrever suas próprias implementações de repositórios. Embora os templates sejam poderosos, é possível fazer melhor: com os repositórios do Spring Data é preciso escrever apenas uma interface com métodos de pesquisa definidos de acordo com um conjunto de convenções (as quais podem variar dependendo da tecnologia de armazenamento utilizada). O Spring Data fornece uma implementação apropriada dessa interface em tempo de execução. Veja um exemplo:

public interface UserRepository extends MongoRepository<User, String> { 
        @Query("{ fullName: ?0 }")
        List<User> findByTheUsersFullName(String fullName);

        List<User> findByFullNameLike(String fullName, Sort sort);
}
...

Autowired UserRepository repo;

 

Neste artigo iremos comparar três subprojetos do Spring Data: JPA, MongoDB e Neo4j. O JPA é parte da plataforma Java EE e define uma API unificada para acessar bancos de dados relacionais e realizar mapeamento objeto-relacional. O MongoDB é um banco de dados open source, escalável, de alta performance e orientado a documentos. O Neo4j é um banco de dados orientado a grafos, com suporte completo a transações.

Todos esses projetos do Spring Data suportam aspectos como: uso de templates; mapeamento de objetos para a estrutura de armazenamento do banco de dados; e suporte a repositórios.

Outros projetos do Spring Data, como o Spring Data Redis e o Spring Data Riak, dão suporte apenas a templates, pois esses sistemas de armazenamento mantêm dados não-estruturados, que não podem ser mapeados ou pesquisados.

Vamos agora examinar os templates, o mapeamento de objetos e o suporte a repositórios.

Templates

O principal propósito de um template (modelo) do Spring Data, e de todos os outros templates do Spring, é cuidar da alocação de recursos e tradução de exceções.

No caso a seguir, o recurso é um datastore acessado remotamente através de uma conexão TCP/IP. O exemplo mostra como configurar o template MongoDB:

<!-- Connection to MongoDB server -->
<mongo:db-factory host="localhost" port="27017" dbname="test" /> 

<!-- MongoDB Template -->
<bean id="mongoTemplate"
class="org.springframework.data.mongodb.core.MongoTemplate">
  <constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/> 
</bean>

Primeiramente, definimos uma fábrica de conexões, que é então referenciada pelo MongoTemplate. No caso do MongoDB, o projeto Spring Data depende do driver de baixo nível para Java.

Em geral, uma API de baixo nível como a do driver do MongoDB tem sua própria estratégia de tratamento e lançamento de exceções. O Spring costuma lidar com exceções através de exceções não-verificadas, que o desenvolvedor pode capturar e tratar, sem no entanto ser obrigado a fazê-lo. Para cobrir essa lacuna, a implementação de um template captura as exceções de baixo nível e relança uma exceção não-verificada correspondente do Spring, a qual é subclasse da exceção DataAccessException.

Um template fornece operações específicas para persistência, como salvar, atualizar e excluir um registro, e também para executar consultas ou jobs map/reduce. Entretanto, esses métodos funcionam apenas com o respectivo banco de dados subjacente.

O Spring Data não fornece um template para JPA, uma vez que a implementação JPA é por si só uma camada de abstração no topo da API JDBC. Um EntityManager do JPA é a classe correspondente a um template. A tradução de exceções é realizada pela implementação do repositório.

Mapeamentos

O JPA introduziu um padrão para mapeamento objeto-relacional (em outras palavras, mapeamento de grafos de objetos para tabelas em banco de dados). O Hibernate é provavelmente a implementação mais popular da especificação JPA.

Com o Spring Data, o suporte se estende também aos bancos de dados NoSQL que possuem estruturas similares a objetos. Mas essas estruturas de dados podem ser muito diferentes entre si, fato que tornaria difícil construir uma API comum de mapeamento de objetos para estruturas de armazenamento específicas dos banco de dados. Cada banco de dados tem seu próprio conjunto de anotações para fornecer os metadados necessários ao mapeamento. O exemplo a seguir mostra como um objeto de domínio simples pode ser mapeado para diferentes bancos de dados:

JPA

MongoDB

Neo4j

@Entity
@Table(name="TUSR")
public class User {

  @Id
  private String id;

  @Column(name="fn")
  private String name;

  private Date lastLogin;

...
}
@Document(
collection="usr")
public class User {

  @Id
  private String id;

  @Field("fn")
  private String name;

  private Date lastLogin;

 ...
}
@NodeEntity
public class User {

  @GraphId
  Long id;


  private String name;

  private Date lastLogin;

...
}
 

Se o leitor possuir familiaridade com entidades JPA, terá reconhecido as anotações padrão da API. O Spring Data faz reuso dessas anotações; nenhuma outra é introduzida. O mapeamento é feito pela implementação de JPA que estiver sendo utilizada. Tanto o MongoDB quanto o Neo4j requerem um conjunto similar de anotações. A primeira anotação é colocada no nível da classe, mapeando-a para uma coleção (no MongoDB uma coleção é similar a uma tabela em um banco de dados relacional) ou para um nó (nós e arestas são os principais tipos de dados em um banco de dados orientado a grafos como o Neo4j).

Cada entidade JPA tem que possuir um identificador único. O mesmo é válido para os documentos no MongoDB e os nós no Neo4j.

O MongoDB utiliza uma anotação @Id (não é a mesma anotação do JPA; esta é localizada no pacote org.springframework.data.annotation), enquanto o Neo4j utiliza a anotação @GraphId. Os valores desses atributos são preenchidos depois de persistir o objeto de domínio. Para outros atributos persistentes, a anotação @Field deve ser utilizada caso o nome do atributo no documento MongoDB não seja equivalente ao atributo Java.

Referências a outros objetos também são suportadas. Os papéis (Roles) da nossa classe usuário (User) podem ser persistidos da seguinte forma:

JPA

MongoDB

Neo4j




@OneToMany
private List roles;




private List roles;
@RelatedTo(
type = "has",
direction = 
Direction.OUTGOING)
private List roles;

Com o JPA, a anotação @OneToMany é utilizada para representar um relacionamento. O lado "n" é armazenado em outra tabela e normalmente é recuperado com uma junção (join). O MongoDB não suporta junções entre coleções. Por padrão, os objetos referenciados são armazenados dentro do mesmo documento. Também é possível ter referências a outros documentos, o que resultará em uma junção no lado do cliente. No Neo4j, os relacionamentos são chamados de arestas (edges) e são um dos tipos de dados básicos.

Em resumo, o MongoDB e o Neo4j utilizam um mapeamento de objetos similar ao bastante conhecido mapeamento objeto-relacional do JPA. Entretanto, por causa de suas diferentes estruturas de dados, os mapeamentos não são exatamente a mesma coisa. Mas o conceito por trás de todos os mapeamentos é o mesmo: mapear objetos Java para a estrutura de dados do banco de dados correspondente.

Suporte a repositórios

Quem já persistiu dados em uma aplicação de negócio, provavelmente já escreveu algum tipo de DAO. Normalmente são implementadas operações de CRUD para um único registro e vários métodos de pesquisa para cada uma das classes persistentes. Os métodos de pesquisa possuem parâmetros que são incluídos na consulta antes de ela ser executada no banco de dados.

Com o JPA, pelo menos as operações de CRUD estão disponíveis na interface EntityManager. Mas escrever métodos de pesquisa personalizados ainda é tedioso: criar uma named query, determinar cada parâmetro e executar a consulta. Por exemplo:

@Entity
@NamedQuery( name="myQuery", query = "SELECT u FROM User u where u.name = :name" )
public class User { 
...
} 

@Repository 
public class ClassicUserRepository { 

   @PersistenceContext EntityManager em; 

   public List<User> findByName(String Name) { 
      TypedQuery<User> q = getEntityManger().createNamedQuery("myQuery", User.class); 

      q.setParameter("name", fullName);

      return q.getResultList();
   } 
   ...

Esse código pode ser um pouco reduzido se for utilizada a interface fluente de uma TypedQuery:

@Repository
public class ClassicUserRepository { 

   @PersistenceContext EntityManager em; 

   public List<User> findByName(String name) {
      return getEntityManger().createNamedQuery("myQuery", User.class)
         .setParameter("name", fullName)
         .getResultList(); 
   } 
   ...

Mas ainda assim está sendo implementado um método que invoca métodos setters e executa um comando para cada tipo de pesquisa. Com o Spring Data JPA, a mesma pesquisa é reduzida para o seguinte trecho de código:

package repositories; 

public interface UserRepository extends JpaRepository<User, String> {

   List<User> findByName(String name); 
}

 

Com o Spring Data JPA, as consultas JPQL não precisam mais ser declaradas com a anotação @NamedQuery na classe correspondente à entidade JPA. Em vez disso, a consulta é escrita em uma anotação no método do repositório:

@Transactional(timeout = 2, propagation = Propagation.REQUIRED)
@Query("SELECT u FROM User u WHERE u.name = 'User 3'")
List<User> findByGivenQuery();

Tudo o escrito anteriormente também é válido para Spring Data MongoDB e Spring Data Neo4j. O exemplo a seguir efetua uma pesquisa em um banco de dados Neo4j com a linguagem de consulta Cipher:

public interface UserRepository extends GraphRepository<User> {

  User findByLogin(String login); 

  @Query("START root=node:User(login = 'root') MATCH root-[:knows]->friends RETURN friends")
  List<User> findFriendsOfRoot(); 
}

As convenções de nomeação dos métodos de pesquisa, é claro, diferem de um banco de dados para outro. Por exemplo, o MongoDB suporta consultas geoespaciais e, sendo assim, pode-se escrever consultas como:

public interface LocationRepository extends MongoRepository<Location, String> {

        List<Location> findByPositionWithin(Circle c);

        List<Location> findByPositionWithin(Box b);

        List<Location> findByPositionNear(Point p, Distance d);
}

Há também suporte genérico para paginação e ordenação para todos os bancos de dados. Para isso, é necessário fornecer parâmetros especiais aos métodos de pesquisa.

As principais vantagens do suporte a repositórios são que: o desenvolvedor escreve muito menos código repetitivo, e as consultas podem ser definidas junto aos métodos de pesquisa e suas documentações. Além disso, as consultas JPQL são compiladas assim que o contexto do Spring é criado e não na primeira vez que a consulta é utilizada, o que facilita a detecção de erros de sintaxe.

Resumo

Este artigo forneceu uma visão geral de uma série de tecnologias complexas, tentando descobrir similaridades e diferenças entre elas. Para uma visão mais detalhada dos projetos Spring Data, visite as páginas dos projetos Spring Data JPA, Spring Data MongoDB e Spring Data Neo4j.

Respondendo a pergunta feita no título: não, não existe nenhuma API genérica para todos os tipos de bancos de dados, pois as diferenças estão nos fundamentos, na estrutura básica. Entretanto, o projeto Spring Data fornece um modelo de programação comum para acesso a dados, ilustrada abaixo:

O suporte a repositórios é uma abstração da camada física e também facilita bastante a escrita de métodos de pesquisa. Com o mapeamento de objetos, os objetos de domínio podem ser transformados nos tipos de dados do banco escolhido. E os templates fornecem acesso de baixo nível às capacidades específicas do banco de dados.


Sobre o autor

Tobias Trelle é consultor de TI sênior na Codecentric AG, com mais de 15 anos de experiência. Seus principais interesses são arquiteturas de software, EAI e computação em nuvem. Tobias escreve regularmente em blogs, além de dar treinamentos e palestras em conferências. (Twitter, Blog, Linked In, G+)

Olá visitante

Você precisa cadastrar-se no InfoQ Brasil ou para enviar comentários. Há muitas vantagens em se cadastrar.

Obtenha o máximo da experiência do InfoQ Brasil.

Dê sua opinião

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão
Comentários da comunidade

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão

Dê sua opinião

Conteúdo educacional

Feedback geral
Bugs
Publicidade
Editorial
InfoQ Brasil e todo o seu conteúdo: todos os direitos reservados. © 2006-2014 C4Media Inc.
Política de privacidade
BT