| name | jpa-patterns |
| description | Use when working with JPA/Hibernate entities, repositories, queries, relationships, performance tuning, and database schema design in Spring Boot applications. |
JPA Patterns Skill
You are an expert in JPA/Hibernate patterns for Spring Boot applications. You optimize queries, design proper entity relationships, and prevent common persistence pitfalls.
Entity Design
Base Entity
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
public Long getId() { return id; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
Relationship Mapping
@Entity
@Table(name = "departments")
public class Department extends BaseEntity {
@Column(nullable = false, unique = true)
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
public Department() {}
public Department(String name) {
this.name = name;
}
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<Employee> getEmployees() { return Collections.unmodifiableList(employees); }
}
@Entity
@Table(name = "employees")
public class Employee extends BaseEntity {
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id", nullable = false)
private Department department;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private EmployeeStatus status = EmployeeStatus.ACTIVE;
public Employee() {}
public Employee(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee other)) return false;
return Objects.equals(email, other.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Department getDepartment() { return department; }
public void setDepartment(Department department) { this.department = department; }
public EmployeeStatus getStatus() { return status; }
public void setStatus(EmployeeStatus status) { this.status = status; }
}
N+1 Problem Solutions
Fetch Join
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") OrderStatus status);
@Query("SELECT e FROM Employee e JOIN FETCH e.department WHERE e.status = :status")
List<Employee> findByStatusWithDepartment(@Param("status") EmployeeStatus status);
}
Entity Graph
@Entity
@NamedEntityGraph(
name = "Order.withItemsAndCustomer",
attributeNodes = {
@NamedAttributeNode("items"),
@NamedAttributeNode("customer")
}
)
public class Order extends BaseEntity {
}
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(value = "Order.withItemsAndCustomer")
@Query("SELECT o FROM Order o WHERE o.id = :id")
Optional<Order> findByIdWithDetails(@Param("id") Long id);
@EntityGraph(attributePaths = {"items", "customer"})
Optional<Order> findWithDetailsById(Long id);
}
Projections for Read-Only Queries
public interface EmployeeSummary {
Long getId();
String getName();
String getEmail();
String getDepartmentName();
}
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
@Query("""
SELECT e.id AS id, e.name AS name, e.email AS email, d.name AS departmentName
FROM Employee e JOIN e.department d
WHERE e.status = :status
""")
List<EmployeeSummary> findSummariesByStatus(@Param("status") EmployeeStatus status);
@Query("""
SELECT new pl.piomin.services.dto.EmployeeReport(
e.id, e.name, e.email, d.name, e.status)
FROM Employee e JOIN e.department d
WHERE d.id = :departmentId
""")
List<EmployeeReport> findReportByDepartment(@Param("departmentId") Long departmentId);
}
Specification Pattern for Dynamic Queries
public final class EmployeeSpecifications {
private EmployeeSpecifications() {}
public static Specification<Employee> hasStatus(EmployeeStatus status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<Employee> inDepartment(Long departmentId) {
return (root, query, cb) -> cb.equal(root.get("department").get("id"), departmentId);
}
public static Specification<Employee> nameLike(String name) {
return (root, query, cb) ->
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<Employee> hiredAfter(LocalDate date) {
return (root, query, cb) -> cb.greaterThanOrEqualTo(root.get("hireDate"), date);
}
}
@Transactional(readOnly = true)
public Page<Employee> search(EmployeeSearchRequest request, Pageable pageable) {
Specification<Employee> spec = Specification.where(null);
if (request.status() != null) {
spec = spec.and(EmployeeSpecifications.hasStatus(request.status()));
}
if (request.departmentId() != null) {
spec = spec.and(EmployeeSpecifications.inDepartment(request.departmentId()));
}
if (request.name() != null) {
spec = spec.and(EmployeeSpecifications.nameLike(request.name()));
}
return employeeRepository.findAll(spec, pageable);
}
Batch Processing
@Service
public class DataMigrationService {
private static final int BATCH_SIZE = 50;
private final EntityManager entityManager;
public DataMigrationService(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional
public int migrateRecords(List<LegacyRecord> records) {
int count = 0;
for (var record : records) {
var entity = mapToEntity(record);
entityManager.persist(entity);
count++;
if (count % BATCH_SIZE == 0) {
entityManager.flush();
entityManager.clear();
}
}
entityManager.flush();
entityManager.clear();
return count;
}
}
Common Pitfalls
| Pitfall | Solution |
|---|
| N+1 selects | JOIN FETCH or EntityGraph |
| LazyInitializationException | Fetch within transaction or use DTO projection |
| Large result sets | Use pagination with Pageable |
| Missing indexes | Add @Index on frequently queried columns |
| Cascade delete issues | Use orphanRemoval = true carefully |
| Eager fetching on collections | Always use FetchType.LAZY on @OneToMany |
| Mutable ID in equals/hashCode | Use natural/business key |