Voltar para o Blog
Spring Data JPA: Advanced Techniques and Performance Optimization
13 min de leitura
Spring Data JPADatabasePerformanceHibernate
Vá além do básico com Spring Data JPA. Aprenda técnicas avançadas para consultas complexas, otimização de performance, e como evitar problemas comuns como N+1 queries.
Projections: Otimizando Queries
Interface-Based Projections
// Interface projection - busca apenas campos necessários
public interface UserSummary {
String getUsername();
String getEmail();
LocalDateTime getCreatedAt();
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Retorna apenas username e email
List<UserSummary> findAllProjectedBy();
// Com filtro
List<UserSummary> findByActiveTrue();
}
// Uso
List<UserSummary> users = userRepository.findAllProjectedBy();
users.forEach(user ->
System.out.println(user.getUsername() + ": " + user.getEmail())
);Class-Based Projections (DTOs)
// DTO
public record UserDTO(String username, String email, LocalDateTime createdAt) {}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT new com.example.dto.UserDTO(u.username, u.email, u.createdAt) " +
"FROM User u WHERE u.active = true")
List<UserDTO> findActiveUsers();
// Com paginação
@Query("SELECT new com.example.dto.UserDTO(u.username, u.email, u.createdAt) " +
"FROM User u")
Page<UserDTO> findAllUsers(Pageable pageable);
}Dynamic Projections
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Projection dinâmica através de generics
<T> List<T> findByActive(boolean active, Class<T> type);
}
// Uso
List<UserSummary> summaries = userRepository.findByActive(true, UserSummary.class);
List<UserDTO> dtos = userRepository.findByActive(true, UserDTO.class);
List<User> entities = userRepository.findByActive(true, User.class);Specifications para Queries Dinâmicas
Criando Specifications
public class UserSpecifications {
public static Specification<User> hasUsername(String username) {
return (root, query, cb) ->
username == null ? null : cb.equal(root.get("username"), username);
}
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null : cb.like(root.get("email"), "%" + email + "%");
}
public static Specification<User> isActive() {
return (root, query, cb) -> cb.isTrue(root.get("active"));
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
public static Specification<User> hasRole(String role) {
return (root, query, cb) -> {
Join<User, Role> roles = root.join("roles");
return cb.equal(roles.get("name"), role);
};
}
}
// Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
// Uso - combinar specifications dinamicamente
Specification<User> spec = Specification.where(null);
if (username != null) {
spec = spec.and(UserSpecifications.hasUsername(username));
}
if (email != null) {
spec = spec.and(UserSpecifications.hasEmail(email));
}
if (activeOnly) {
spec = spec.and(UserSpecifications.isActive());
}
List<User> users = userRepository.findAll(spec);Resolvendo N+1 Queries
O Problema N+1
// ❌ Causa N+1 queries
@Entity
public class User {
@OneToMany(mappedBy = "user")
private List<Order> orders;
}
// 1 query para users + N queries para orders
List<User> users = userRepository.findAll();
users.forEach(user ->
System.out.println(user.getOrders().size()) // Query para cada user!
);Solução 1: Entity Graph
@Entity
@NamedEntityGraph(
name = "User.orders",
attributeNodes = @NamedAttributeNode("orders")
)
public class User {
@OneToMany(mappedBy = "user")
private List<Order> orders;
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(value = "User.orders", type = EntityGraph.EntityGraphType.FETCH)
List<User> findAll();
@EntityGraph(attributePaths = {"orders", "roles"})
Optional<User> findById(Long id);
}Solução 2: JOIN FETCH
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT DISTINCT u FROM User u " +
"LEFT JOIN FETCH u.orders " +
"WHERE u.active = true")
List<User> findActiveUsersWithOrders();
// Com múltiplos joins
@Query("SELECT DISTINCT u FROM User u " +
"LEFT JOIN FETCH u.orders o " +
"LEFT JOIN FETCH o.items " +
"LEFT JOIN FETCH u.roles " +
"WHERE u.id = :id")
Optional<User> findByIdWithDetails(@Param("id") Long id);
}Solução 3: Batch Fetching
@Entity
public class User {
@OneToMany(mappedBy = "user")
@BatchSize(size = 10) // Carrega em batches de 10
private List<Order> orders;
}
// application.yml
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 10Custom Repository Implementation
Criando Métodos Customizados
// Interface customizada
public interface UserRepositoryCustom {
List<User> findUsersWithComplexLogic(UserSearchCriteria criteria);
void bulkUpdateUsers(List<Long> userIds, UserUpdateDTO update);
}
// Implementação
@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager em;
@Override
public List<User> findUsersWithComplexLogic(UserSearchCriteria criteria) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
List<Predicate> predicates = new ArrayList<>();
if (criteria.getUsername() != null) {
predicates.add(cb.like(root.get("username"),
"%" + criteria.getUsername() + "%"));
}
if (criteria.getMinAge() != null) {
predicates.add(cb.greaterThanOrEqualTo(
root.get("age"), criteria.getMinAge()));
}
query.where(predicates.toArray(new Predicate[0]));
query.orderBy(cb.desc(root.get("createdAt")));
return em.createQuery(query)
.setMaxResults(100)
.getResultList();
}
@Override
@Transactional
public void bulkUpdateUsers(List<Long> userIds, UserUpdateDTO update) {
em.createQuery("UPDATE User u SET u.status = :status " +
"WHERE u.id IN :ids")
.setParameter("status", update.getStatus())
.setParameter("ids", userIds)
.executeUpdate();
}
}
// Repository principal herda da customizada
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
UserRepositoryCustom {
}Otimização de Performance
1. Read-Only Transactions
@Service
public class UserService {
// Read-only desabilita dirty checking
@Transactional(readOnly = true)
public List<User> findActiveUsers() {
return userRepository.findByActiveTrue();
}
}2. Query Hints
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
@Query("SELECT u FROM User u WHERE u.username = :username")
Optional<User> findByUsernameWithCache(@Param("username") String username);
@QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true"))
List<User> findByActive(boolean active);
}3. Second-Level Cache
// application.yml
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
region:
factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
javax:
cache:
provider: org.ehcache.jsr107.EhcacheCachingProvider
// Entity
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
// ...
}
@Entity
public class User {
@OneToMany(mappedBy = "user")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private List<Order> orders;
}4. Pagination Otimizada
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Pagination com count query otimizada
@Query(
value = "SELECT u FROM User u WHERE u.active = true",
countQuery = "SELECT COUNT(u) FROM User u WHERE u.active = true"
)
Page<User> findActiveUsers(Pageable pageable);
// Slice em vez de Page (sem count)
Slice<User> findByActive(boolean active, Pageable pageable);
}
// Uso
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Slice<User> users = userRepository.findByActive(true, pageable);
// Slice é mais rápido - não executa COUNT query
if (users.hasNext()) {
// Há mais dados
}Auditoria Automática
Configuração de Auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}
// Base entity
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(nullable = false, updatable = false)
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
// Uso
@Entity
public class User extends AuditableEntity {
// Campos de auditoria são preenchidos automaticamente
}Soft Delete
Implementação de Soft Delete
@Entity
@SQLDelete(sql = "UPDATE user SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private boolean deleted = false;
private LocalDateTime deletedAt;
}
// Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Queries normais ignoram deleted = true automaticamente
List<User> findByUsername(String username);
// Query específica para incluir deletados
@Query("SELECT u FROM User u WHERE u.username = :username")
List<User> findByUsernameIncludingDeleted(@Param("username") String username);
}Melhores Práticas
- Use projections para buscar apenas dados necessários
- Sempre teste queries com explain plan
- Configure batch size apropriadamente
- Use @Transactional(readOnly=true) para leitura
- Evite lazy loading fora de transações
- Implemente paginação para grandes datasets
- Monitor SQL queries em desenvolvimento
- Use cache para dados que mudam raramente
- Prefira Slice a Page quando count não é necessário
- Otimize índices no database
Conclusão
Spring Data JPA é poderoso, mas requer conhecimento profundo para ser usado eficientemente. Dominar estas técnicas avançadas permite construir aplicações que são tanto produtivas quanto performáticas.
Sempre performe e monitore queries. Uma aplicação lenta geralmente tem problemas no layer de persistência.