Spring Data JPA Skill
Comprehensive guide to data access with Spring Data JPA including repository patterns, custom queries, and performance optimization.
Overview
This skill covers everything needed for production-ready database access with Spring Data JPA.
Parameters
| Name | Type | Required | Default | Validation |
|------|------|----------|---------|------------|
| database | enum | ✗ | postgresql | postgresql | mysql | h2 |
| migration_tool | enum | ✗ | flyway | flyway | liquibase | none |
| fetch_strategy | enum | ✗ | lazy | lazy | eager |
Topics Covered
Core (Must Know)
- Entities:
@Entity,@Table,@Id, relationships - Repositories:
JpaRepository, derived queries - Transactions:
@Transactional, propagation
Intermediate
- Custom Queries:
@Querywith JPQL and native SQL - Projections: Interface-based, class-based
- Auditing:
@CreatedDate,@LastModifiedDate
Advanced
- Specifications: Dynamic query building
- Entity Graphs: Fetch optimization
- N+1 Prevention: JOIN FETCH strategies
Code Examples
Entity with Relationships
@Entity
@Table(name = "orders")
@EntityListeners(AuditingEntityListener.class)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String orderNumber;
@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<>();
@CreatedDate
private LocalDateTime createdAt;
@Version
private Long version;
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
}
}
Repository with Custom Queries
public interface OrderRepository extends JpaRepository<Order, Long>,
JpaSpecificationExecutor<Order> {
Optional<Order> findByOrderNumber(String orderNumber);
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.customer.id = :customerId")
List<Order> findByCustomerWithItems(@Param("customerId") Long customerId);
@EntityGraph(attributePaths = {"items", "customer"})
Optional<Order> findWithDetailsById(Long id);
@Modifying
@Query("UPDATE Order o SET o.status = :status WHERE o.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);
}
Specification Pattern
public class OrderSpecifications {
public static Specification<Order> hasStatus(OrderStatus status) {
return (root, query, cb) ->
status == null ? null : cb.equal(root.get("status"), status);
}
public static Specification<Order> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
}
// Usage
Specification<Order> spec = Specification
.where(OrderSpecifications.hasStatus(status))
.and(OrderSpecifications.createdAfter(since));
Page<Order> orders = orderRepository.findAll(spec, pageable);
Troubleshooting
Failure Modes
| Issue | Diagnosis | Fix |
|-------|-----------|-----|
| N+1 queries | Multiple SELECTs | Use @EntityGraph or JOIN FETCH |
| LazyInitializationException | Access outside session | Use @Transactional or DTO |
| Data not saved | Missing @Transactional | Add to service method |
Debug Checklist
□ Enable SQL logging: spring.jpa.show-sql=true
□ Check for N+1 with hibernate.generate_statistics
□ Verify @Transactional on write operations
□ Review fetch types (LAZY vs EAGER)
Unit Test Template
@DataJpaTest
@Testcontainers
class OrderRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@Autowired
private OrderRepository orderRepository;
@Test
void shouldFindByOrderNumber() {
Order order = new Order();
order.setOrderNumber("ORD-001");
orderRepository.save(order);
Optional<Order> found = orderRepository.findByOrderNumber("ORD-001");
assertThat(found).isPresent();
}
}
Usage
Skill("spring-data-jpa")
Version History
| Version | Date | Changes | |---------|------|---------| | 2.0.0 | 2024-12-30 | Specifications, auditing, performance patterns | | 1.0.0 | 2024-01-01 | Initial release |