Spring Data JPA é uma biblioteca que facilita o acesso a dados em aplicações Java que usam o framework Spring. Ela permite que os desenvolvedores escrevam consultas usando uma linguagem de domínio específico, baseada em nomes de métodos ou anotações. Além disso, ela oferece suporte a diversas funcionalidades, como paginação, ordenação, cache, auditoria e projeções. Neste artigo, vamos explicar como o Spring Data JPA funciona e quais são as suas vantagens e desvantagens.
Spring Data JPA x JPA x Hibernate
Antes de continuarmos, vamos entender um pouquinho sobre esses carinhas:
- JPA
JPA ou Java Persistence API é uma especificação que define uma API para o mapeamento de objeto-relacionais (ORM) que simplificam e muito a implementação da camada de acesso de dados de uma aplicação Java.
- Hibernate
Já o Hibernate é um das implementações mais populares dessa especificação.
- Spring Data JPA
Bom, já sabemos o que é o Spring Data JPA, mas qual o seu propósito? O Spring Data JPA adiciona um novo layer à API de persistência do Java, ou seja, permite você usar todo o poder do JPA em sua aplicação e no topo disso adiciona suas próprias funcionalidades como a implementação no-code do padrão repository, paginação e ordenação de resultados semi-automática, geração automática de queries sql e redução de escrita de código repetitivo.
Implementação no-code do padrão repository
O repository pattern é um dos padrões relacionado à persistência de dados mais utilizado. Ele abstrai a lógica de acesso aos dados, fornecendo uma interface comum para manipulação de dados, o que permite isolar o domínio da aplicação dos detalhes de implementação da camada de persistência da aplicação, facilitando assim a manutenção e o teste de código.
A implementação desse padrão não é tão complicada, porém, em uma aplicação onde há muitas entidades a serem mapeadas, escrever as operações padrões de um CRUD para cada uma dessas entidades tendem a produzir código repetitivo.
Abaixo, temos um exemplo de um repository que provê todos os métodos necessários para:
- atualizar, persistir e remover uma ou múltiplas pessoas;
- encontrar uma ou mais pessoas pelas suas chaves primárias;
- listar, contar ou remover todas as pessoas;
- checar se uma pessoa com uma determinada chave primária existe:
package com.ronilsonalves.springjpa.data;
import com.ronilsonalves.springjpa.model.Person;
import org.springframework.data.repository.CrudRepository;
public interface PersonRepository extends CrudRepository<Person, Long> {
// Isso é tudo que precisamos!
}Já em uma classe de serviço, podemos injetar esse repository como dependência e acessar os métodos padrões de um CRUD provido pelo Spring Data JPA sem sequer termos escrito suas assinaturas em nosso repository:
package com.ronilsonalves.springjpa.service;
import com.ronilsonalves.jparestapi.data.PersonRepository;
import com.ronilsonalves.jparestapi.model.Person;
public class PersonServiceImpl implements PersonService {
private final PersonRepository personRepository;
/**
* The @Autowired annotation is optional and is used to inject the PersonRepository dependency.
* @param personRepository our Person repository interface
*/
public PersonServiceImpl(PersonRepository personRepository) {
this.personRepository = personRepository;
}
/**
* This method saves a Person object in the database. Observe that in our Person repository we don't have a
* method signature or a custom query to save a person.
* @param person the Person object (or could be a DTO) to be saved
* @return the Person object
*/
public Person savePerson(Person person) {
return personRepository.save(person);
}
/**
* This method returns a Person object by its id. Again, observe that in our Person repository we don't have a
* method signature or a custom query to find a person by its id.
* @param personId the id of the person to be returned
* @return the Person object
* @throws RuntimeException if the person is not found
*/
public Person getPerson(Long personId) throws RuntimeException {
return personRepository.findById(personId).orElseThrow(
() -> new RuntimeException("Person not found")
);
}
/* Omited for brevity */
}Paginação e ordenação
O Spring Data JPA fornece uma interface que permite a implementação de recursos de paginação e ordenação de maneira simplificada, além disso você pode usar em conjunto com o CrudRepository, como no exemplo acima a nossa PersonRepository extende de. Vamos aplicar um exemplo de paginação e ordenação a esse repository:
package com.ronilsonalves.springjpa.data;
import com.ronilsonalves.jparestapi.model.Person;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
@Repository
public interface PersonRepository extends CrudRepository<Person, Long>, PagingAndSortingRepository<Person, Long> {
// We don't need to declare the signature of method to find all people and apply pagination, Spring Data JPA do it for us and order the results (by PK as default).
// Query generation and ordination
/**
* This method returns a Page object containing Person objects qyerying by last name, ordered by first name and
* applying it pagination.
* @param name the last name of the people to be returned
* @param pageable the Pageable object
* @return a Page object containing Person objects
*/
Page<Person> findAllByLastNameOrderByFirstName(String name, Pageable pageable);
}Para exemplificar a paginação e ordenação extendendo da interface PagingAndSortingRepository do Spring Data JPA, além das importações, foi preciso escrever APENAS uma linha de código.
Em nosso classe de serviço, podemos chamar o método findAll() – de novo, sem que sua assinatura tenha sido definida em nosso repository – passando um objeto Pageable como parâmetro.
Já a ordenação, apenas precisamos chamar o método que gera uma JPQL query (já falaremos sobre essa funcionalidade do Spring Data JPA) e que retorna uma página com o resultado:
package com.ronilsonalves.springjpa.data;
import com.ronilsonalves.jparestapi.data.PersonRepository;
import com.ronilsonalves.jparestapi.model.Person;
public class PersonServiceImpl implements PersonService {
/* Omited for brevity */
/**
* This method returns a Page object containing Person objects. Observe that in our Person repository we don't have
* a method signature or a custom query to find all people applying it pagination.
* @param page the page number
* @param size the page size
* @return a Page object containing Person objects
*/
public Page<Person> personPageable(int page, int size) {
return personRepository.findAll(PageRequest.of(page, size));
}
/**
* This method returns a Page object containing Person objects.
* @param lastName the last name of the people to be returned
* @param page the page number
* @param size the page size
* @return a Page object containing Person objects
*/
public Page<Person> personPageableByLastName(String lastName, int page, int size) {
return personRepository.findAllByLastNameOrderByFirstName(lastName, PageRequest.of(page, size));
}
/* Omited for brevity */
}Geração de queries
O Spring Data JPA permite a geração de queries ao banco de dados baseadas nos nomes dos métodos definidos no repository. Em queries não muito complexas você pode fazer o uso dessa ferramenta mágica definindo um método que se inicia por find...by, o Spring então 'traduz' o nome do método e cria uma query JPQL para ele. Nos exemplos de código acima, vimos exemplos dessa implementação em nosso repository:
package com.ronilsonalves.springjpa.data;
/* Omited for brevity */
@Repository
public interface PersonRepository extends CrudRepository<Person, Long>, PagingAndSortingRepository<Person, Long> {
/* Omited for brevity */
Page<Person> findAllByLastNameOrderByFirstName(String name, Pageable pageable);
}Mas a magia desse recurso não para por aí, com ele você também pode criar queries manuais customizadas usando uma JPQL query e @Query annotation.
Custom queries
As queries customizadas são uma mão na roda e você pode usa-lá em queries mais complexas ou em consultas personalizadas, para isso basta anotar a assinatura do método no repository com a anotação @Query do próprio Spring Data JPA. Por exemplo, suponha que Person possua os seguintes atributos nome, sobrenome, data_de_nascimento e email. Se quisermos buscar todas as pessoas que tem entre 18 e 25 anos, que possuam endereços de emails terminados em "@gmail.com", podemos usar uma query nativa em nossa custom query, como vemos a seguir:
package com.ronilsonalves.springjpa.data;
// Omited for brevety
public interface PersonRepository extends PagingAndSortingRepository<Person, Long>, CrudRepository<Person, Long> {
// Omited for brevety
// Custom query generation
/**
* This method returns a List object containing Person objects querying their age range and ending mail using
* a native query.
* @param minAge the minimum age
* @param maxAge the maximum age
* @param endingMail the ending mail
* @return a List object containing Person objects
*/
@Query(value = "SELECT * FROM person " +
"WHERE TIMESTAMPDIFF(YEAR, date_of_birth, CURRENT_DATE) BETWEEN :minAge AND :maxAge " +
"AND LOWER(email) LIKE LOWER(CONCAT('%', :endingMail))", nativeQuery = true)
List<Person> retrievePersonsByAgeAndEMailNative(@Param("minAge") int minAge, @Param("maxAge") int maxAge,
@Param("endingMail") String endingMail);
}Com as customs queries além de poder usar queries JPQL, você pode usar queries nativas como no exemplo acima.
Redução de escrita de código repetitivo
Como vimos nos exemplos acima, não foi necessário escrever métodos para criação, leitura, atualização e exclusão de entradas em nosso banco de dados, facilitando assim a manipulação de dados na camada de persistência e como exemplifiquei anteriormente, em um sistema com muitas entidades a serem mapeadas, não seria necessário escrever diversas vezes o mesmo método para salvar/atualizar, ler e excluir informações de um objeto, isso graças a implementação no-code do padrão repository.
Desvantagens
Nem tudo são flores, até aqui vimos diversas vantagens em se utilizar o Spring Data JPA em nossas aplicações, mas o uso dede levar em consideração os possíveis efeitos negativos:
- Forte acoplamento entre as entidades e a API
O Spring Data JPA pode levar a um forte acoplamento entre as entidades e a API dificultando a manutenção do código se não for usado uma boa abordagem de separação de responsabilidades.
- Questões de desempenho
O Spring Data JPA pode apresentar dificuldades de desempenho quando lidando com consultas complexas ou com grande fluxo de dados.
- Limitações na personalização de consultas
Apesar de possibilitar a criação de consultas automáticas usando o nome do métodos – e dar liberdade para criação de queries customizadas com a JQPL, essa customização pode sofrer limitação o que pode se tornar um problema na criação de consultas complexas usando o Spring Data JPA.
Conclusão
Vimos as principais características do Spring Data JPA e diversos exemplos de sua aplicação no desenvolvimento de aplicações, pudemos observar sua praticidade por facilitar o gerenciamento da camada de persistência de dados. O ecossistema Spring, como um todo traz essa comodidade no desenvolvimento mas também trás algumas disvantagens que devem ser consideradas na adoção dos recursos do framework.
