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: 10

Custom 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.