Spring Data JPA: Advanced Queries and Best Practices
Master Spring Data JPA with advanced queries, custom repositories, specifications, projections, and performance optimization techniques.
Moshiour Rahman
Advertisement
Introduction to Spring Data JPA
Spring Data JPA simplifies database operations by providing a repository abstraction over JPA. You get CRUD operations automatically and can define custom queries easily.
Entity Mapping
Basic Entity
@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@Enumerated(EnumType.STRING)
private ProductStatus status;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
@Version
private Long version;
}
Relationships
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@ManyToMany
@JoinTable(
name = "order_tags",
joinColumns = @JoinColumn(name = "order_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();
// Helper methods
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
}
}
Repository Methods
Query Methods
public interface ProductRepository extends JpaRepository<Product, Long> {
// Simple queries
List<Product> findByName(String name);
Optional<Product> findByNameIgnoreCase(String name);
List<Product> findByStatus(ProductStatus status);
// Comparison
List<Product> findByPriceGreaterThan(BigDecimal price);
List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
// String matching
List<Product> findByNameContaining(String keyword);
List<Product> findByNameStartingWith(String prefix);
List<Product> findByDescriptionContainingIgnoreCase(String keyword);
// Null checks
List<Product> findByDescriptionIsNull();
List<Product> findByDescriptionIsNotNull();
// Collection membership
List<Product> findByStatusIn(Collection<ProductStatus> statuses);
List<Product> findByIdIn(List<Long> ids);
// Boolean
List<Product> findByActiveTrue();
// Ordering
List<Product> findByStatusOrderByPriceDesc(ProductStatus status);
List<Product> findTop10ByStatusOrderByCreatedAtDesc(ProductStatus status);
// Limiting
Product findFirstByOrderByPriceDesc();
List<Product> findTop5ByStatusOrderByPriceAsc(ProductStatus status);
// Counting
long countByStatus(ProductStatus status);
boolean existsByName(String name);
// Delete
void deleteByStatus(ProductStatus status);
long deleteByCreatedAtBefore(LocalDateTime date);
}
JPQL Queries
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o WHERE o.customer.id = :customerId")
List<Order> findByCustomerId(@Param("customerId") Long customerId);
@Query("SELECT o FROM Order o WHERE o.totalAmount > :amount AND o.status = :status")
List<Order> findExpensiveOrdersByStatus(
@Param("amount") BigDecimal amount,
@Param("status") OrderStatus status
);
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.customer WHERE o.id IN :ids")
List<Order> findAllByIdWithCustomers(@Param("ids") List<Long> ids);
@Query("SELECT DISTINCT o FROM Order o " +
"JOIN FETCH o.items i " +
"WHERE o.customer.id = :customerId")
List<Order> findAllByCustomerWithItems(@Param("customerId") Long customerId);
// Aggregate queries
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.customer.id = :customerId")
BigDecimal getTotalSpentByCustomer(@Param("customerId") Long customerId);
@Query("SELECT COUNT(o) FROM Order o WHERE o.createdAt >= :date")
long countOrdersSince(@Param("date") LocalDateTime date);
@Query("SELECT o.status, COUNT(o) FROM Order o GROUP BY o.status")
List<Object[]> countByStatus();
}
Native Queries
@Query(value = "SELECT * FROM orders WHERE YEAR(created_at) = :year",
nativeQuery = true)
List<Order> findByYear(@Param("year") int year);
@Query(value = "SELECT * FROM products WHERE MATCH(name, description) AGAINST(:term)",
nativeQuery = true)
List<Product> fullTextSearch(@Param("term") String term);
@Modifying
@Query(value = "UPDATE products SET view_count = view_count + 1 WHERE id = :id",
nativeQuery = true)
void incrementViewCount(@Param("id") Long id);
Projections
Interface Projection
public interface ProductSummary {
Long getId();
String getName();
BigDecimal getPrice();
}
public interface ProductRepository extends JpaRepository<Product, Long> {
List<ProductSummary> findByStatus(ProductStatus status);
@Query("SELECT p.id as id, p.name as name, p.price as price " +
"FROM Product p WHERE p.category.id = :categoryId")
List<ProductSummary> findSummaryByCategory(@Param("categoryId") Long categoryId);
}
Class Projection (DTO)
@Data
@AllArgsConstructor
public class ProductDTO {
private Long id;
private String name;
private BigDecimal price;
private String categoryName;
}
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT new com.example.dto.ProductDTO(p.id, p.name, p.price, c.name) " +
"FROM Product p JOIN p.category c")
List<ProductDTO> findAllProductDTOs();
}
Dynamic Projections
public interface ProductRepository extends JpaRepository<Product, Long> {
<T> List<T> findByStatus(ProductStatus status, Class<T> type);
<T> Optional<T> findById(Long id, Class<T> type);
}
// Usage
List<ProductSummary> summaries = productRepository.findByStatus(ACTIVE, ProductSummary.class);
List<Product> full = productRepository.findByStatus(ACTIVE, Product.class);
Specifications (Dynamic Queries)
public class ProductSpecifications {
public static Specification<Product> hasName(String name) {
return (root, query, cb) -> {
if (name == null || name.isEmpty()) {
return cb.conjunction();
}
return cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
};
}
public static Specification<Product> hasStatus(ProductStatus status) {
return (root, query, cb) -> {
if (status == null) {
return cb.conjunction();
}
return cb.equal(root.get("status"), status);
};
}
public static Specification<Product> priceBetween(BigDecimal min, BigDecimal max) {
return (root, query, cb) -> {
if (min == null && max == null) {
return cb.conjunction();
}
if (min == null) {
return cb.lessThanOrEqualTo(root.get("price"), max);
}
if (max == null) {
return cb.greaterThanOrEqualTo(root.get("price"), min);
}
return cb.between(root.get("price"), min, max);
};
}
public static Specification<Product> inCategory(Long categoryId) {
return (root, query, cb) -> {
if (categoryId == null) {
return cb.conjunction();
}
return cb.equal(root.get("category").get("id"), categoryId);
};
}
}
public interface ProductRepository extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> {
}
// Usage
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public Page<Product> searchProducts(ProductSearchCriteria criteria, Pageable pageable) {
Specification<Product> spec = Specification
.where(ProductSpecifications.hasName(criteria.getName()))
.and(ProductSpecifications.hasStatus(criteria.getStatus()))
.and(ProductSpecifications.priceBetween(criteria.getMinPrice(), criteria.getMaxPrice()))
.and(ProductSpecifications.inCategory(criteria.getCategoryId()));
return productRepository.findAll(spec, pageable);
}
}
Pagination and Sorting
// Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findByStatus(ProductStatus status, Pageable pageable);
@Query("SELECT p FROM Product p WHERE p.category.id = :categoryId")
Slice<Product> findByCategorySliced(@Param("categoryId") Long categoryId, Pageable pageable);
}
// Service
@Service
public class ProductService {
public Page<Product> getProducts(int page, int size, String sortBy, String direction) {
Sort sort = direction.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return productRepository.findAll(pageable);
}
public Page<Product> getProductsMultiSort(int page, int size) {
Sort sort = Sort.by(
Sort.Order.desc("featured"),
Sort.Order.asc("name")
);
return productRepository.findAll(PageRequest.of(page, size, sort));
}
}
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);
}
}
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
@Entity
public class Product extends BaseEntity {
// fields...
}
Performance Optimization
N+1 Problem Solution
// Problem: N+1 queries
List<Order> orders = orderRepository.findAll();
orders.forEach(o -> System.out.println(o.getCustomer().getName())); // N additional queries!
// Solution 1: JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();
// Solution 2: Entity Graph
@EntityGraph(attributePaths = {"customer", "items"})
List<Order> findByStatus(OrderStatus status);
// Solution 3: Batch fetching (in entity)
@OneToMany(mappedBy = "order")
@BatchSize(size = 20)
private List<OrderItem> items;
Batch Operations
@Modifying
@Query("UPDATE Product p SET p.price = p.price * :multiplier WHERE p.category.id = :categoryId")
int updatePricesByCategory(@Param("categoryId") Long categoryId,
@Param("multiplier") BigDecimal multiplier);
@Modifying
@Query("DELETE FROM Product p WHERE p.status = :status AND p.createdAt < :date")
int deleteOldInactiveProducts(@Param("status") ProductStatus status,
@Param("date") LocalDateTime date);
Read-Only Transactions
@Service
@Transactional(readOnly = true)
public class ProductQueryService {
public List<Product> getProducts() {
return productRepository.findAll();
}
@Transactional // Override for write operations
public Product save(Product product) {
return productRepository.save(product);
}
}
Summary
| Feature | Use Case |
|---|---|
| Query methods | Simple queries |
| @Query JPQL | Complex queries |
| Native queries | Database-specific |
| Specifications | Dynamic queries |
| Projections | Partial data |
| EntityGraph | Eager loading |
Spring Data JPA simplifies database operations while providing powerful customization options.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Spring Security Database Authentication: Custom UserDetailsService Guide
Implement database authentication in Spring Security. Learn UserDetailsService, password encoding, account locking, and multi-tenant authentication.
Spring BootSpring Boot 3 Virtual Threads: Complete Guide to Java 21 Concurrency
Master virtual threads in Spring Boot 3. Learn configuration, performance benchmarks, when to use them, common pitfalls, and production-ready patterns for high-throughput applications.
Spring BootSpring Security Method-Level Authorization: Complete @PreAuthorize Guide
Master method-level security in Spring Boot. Learn @PreAuthorize, @PostAuthorize, SpEL expressions, custom permissions, and domain object security.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.