jpa-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseJPA Patterns Skill
JPA 最佳实践技能
Best practices and common pitfalls for JPA/Hibernate in Spring applications.
Spring应用中JPA/Hibernate的最佳实践与常见陷阱。
When to Use
适用场景
- User mentions "N+1 problem" / "too many queries"
- LazyInitializationException errors
- Questions about fetch strategies (EAGER vs LAZY)
- Transaction management issues
- Entity relationship design
- Query optimization
- 用户提及「N+1问题」/「查询过多」
- 出现LazyInitializationException错误
- 询问抓取策略(EAGER 与 LAZY的区别)
- 事务管理相关问题
- 实体关系设计相关问题
- 查询优化相关问题
Quick Reference: Common Problems
快速参考:常见问题
| Problem | Symptom | Solution |
|---|---|---|
| N+1 queries | Many SELECT statements | JOIN FETCH, @EntityGraph |
| LazyInitializationException | Error outside transaction | Open Session in View, DTO projection, JOIN FETCH |
| Slow queries | Performance issues | Pagination, projections, indexes |
| Dirty checking overhead | Slow updates | Read-only transactions, DTOs |
| Lost updates | Concurrent modifications | Optimistic locking (@Version) |
| 问题 | 现象 | 解决方案 |
|---|---|---|
| N+1查询 | 产生大量SELECT语句 | JOIN FETCH、@EntityGraph |
| LazyInitializationException | 在事务外访问懒加载字段时报错 | Open Session in View、DTO投影、JOIN FETCH |
| 慢查询 | 性能问题 | 分页、投影、索引 |
| 脏检查开销大 | 更新操作慢 | 只读事务、DTO |
| 更新丢失 | 并发修改时数据覆盖 | 乐观锁(@Version) |
N+1 Problem
N+1查询问题
The #1 JPA performance killer
排名第一的JPA性能杀手
The Problem
问题描述
java
// ❌ BAD: N+1 queries
@Entity
public class Author {
@Id private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
}
// This innocent code...
List<Author> authors = authorRepository.findAll(); // 1 query
for (Author author : authors) {
System.out.println(author.getBooks().size()); // N queries!
}
// Result: 1 + N queries (if 100 authors = 101 queries)java
// ❌ 错误示例:产生N+1查询
@Entity
public class Author {
@Id private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
}
// 这段看起来没问题的代码...
List<Author> authors = authorRepository.findAll(); // 1次查询
for (Author author : authors) {
System.out.println(author.getBooks().size()); // N次查询!
}
// 结果:1 + N次查询(如果有100个作者就会产生101次查询)Solution 1: JOIN FETCH (JPQL)
解决方案1:JOIN FETCH(JPQL)
java
// ✅ GOOD: Single query with JOIN FETCH
public interface AuthorRepository extends JpaRepository<Author, Long> {
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
}
// Usage - single query
List<Author> authors = authorRepository.findAllWithBooks();java
// ✅ 正确示例:使用JOIN FETCH实现单查询
public interface AuthorRepository extends JpaRepository<Author, Long> {
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
}
// 使用方式 - 仅1次查询
List<Author> authors = authorRepository.findAllWithBooks();Solution 2: @EntityGraph
解决方案2:@EntityGraph
java
// ✅ GOOD: EntityGraph for declarative fetching
public interface AuthorRepository extends JpaRepository<Author, Long> {
@EntityGraph(attributePaths = {"books"})
List<Author> findAll();
// Or with named graph
@EntityGraph(value = "Author.withBooks")
List<Author> findAllWithBooks();
}
// Define named graph on entity
@Entity
@NamedEntityGraph(
name = "Author.withBooks",
attributeNodes = @NamedAttributeNode("books")
)
public class Author {
// ...
}java
// ✅ 正确示例:使用EntityGraph实现声明式抓取
public interface AuthorRepository extends JpaRepository<Author, Long> {
@EntityGraph(attributePaths = {"books"})
List<Author> findAll();
// 或者配合命名图使用
@EntityGraph(value = "Author.withBooks")
List<Author> findAllWithBooks();
}
// 在实体上定义命名图
@Entity
@NamedEntityGraph(
name = "Author.withBooks",
attributeNodes = @NamedAttributeNode("books")
)
public class Author {
// ...
}Solution 3: Batch Fetching
解决方案3:批量抓取
java
// ✅ GOOD: Batch fetching (Hibernate-specific)
@Entity
public class Author {
@OneToMany(mappedBy = "author")
@BatchSize(size = 25) // Fetch 25 at a time
private List<Book> books;
}
// Or globally in application.properties
spring.jpa.properties.hibernate.default_batch_fetch_size=25java
// ✅ 正确示例:批量抓取(Hibernate特有特性)
@Entity
public class Author {
@OneToMany(mappedBy = "author")
@BatchSize(size = 25) // 每次批量抓取25个关联对象
private List<Book> books;
}
// 或者在application.properties中全局配置
spring.jpa.properties.hibernate.default_batch_fetch_size=25Detecting N+1
检测N+1问题
yaml
undefinedyaml
undefinedEnable SQL logging to detect N+1
开启SQL日志检测N+1问题
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
---spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
---Lazy Loading
懒加载
FetchType Basics
FetchType基础
java
@Entity
public class Order {
// LAZY: Load only when accessed (default for collections)
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
// EAGER: Always load immediately (default for @ManyToOne, @OneToOne)
@ManyToOne(fetch = FetchType.EAGER) // ⚠️ Usually bad
private Customer customer;
}java
@Entity
public class Order {
// LAZY:仅在访问时加载(集合的默认策略)
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
// EAGER:总是立即加载(@ManyToOne、@OneToOne的默认策略)
@ManyToOne(fetch = FetchType.EAGER) // ⚠️ 通常不推荐使用
private Customer customer;
}Best Practice: Default to LAZY
最佳实践:默认使用LAZY
java
// ✅ GOOD: Always use LAZY, fetch when needed
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY) // Override EAGER default
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}java
// ✅ 正确示例:始终使用LAZY,需要时再抓取
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY) // 覆盖默认的EAGER策略
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}LazyInitializationException
LazyInitializationException问题
java
// ❌ BAD: Accessing lazy field outside transaction
@Service
public class OrderService {
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
}
// In controller (no transaction)
Order order = orderService.getOrder(1L);
order.getItems().size(); // 💥 LazyInitializationException!java
// ❌ 错误示例:在事务外访问懒加载字段
@Service
public class OrderService {
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
}
// 在controller中调用(无事务上下文)
Order order = orderService.getOrder(1L);
order.getItems().size(); // 💥 抛出LazyInitializationException!Solutions for LazyInitializationException
LazyInitializationException解决方案
Solution 1: JOIN FETCH in query
java
// ✅ Fetch needed associations in query
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);Solution 2: @Transactional on service method
java
// ✅ Keep transaction open while accessing
@Service
public class OrderService {
@Transactional(readOnly = true)
public OrderDTO getOrderWithItems(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
// Access within transaction
int itemCount = order.getItems().size();
return new OrderDTO(order, itemCount);
}
}Solution 3: DTO Projection (recommended)
java
// ✅ BEST: Return only what you need
public interface OrderSummary {
Long getId();
String getStatus();
int getItemCount();
}
@Query("SELECT o.id as id, o.status as status, SIZE(o.items) as itemCount " +
"FROM Order o WHERE o.id = :id")
Optional<OrderSummary> findOrderSummary(@Param("id") Long id);Solution 4: Open Session in View (not recommended)
yaml
undefined解决方案1:查询中使用JOIN FETCH
java
// ✅ 在查询中提前抓取需要的关联对象
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);解决方案2:在service方法上加@Transactional
java
// ✅ 访问懒加载字段时保持事务开启
@Service
public class OrderService {
@Transactional(readOnly = true)
public OrderDTO getOrderWithItems(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
// 在事务内访问懒加载字段
int itemCount = order.getItems().size();
return new OrderDTO(order, itemCount);
}
}解决方案3:DTO投影(推荐)
java
// ✅ 最优方案:仅返回需要的字段
public interface OrderSummary {
Long getId();
String getStatus();
int getItemCount();
}
@Query("SELECT o.id as id, o.status as status, SIZE(o.items) as itemCount " +
"FROM Order o WHERE o.id = :id")
Optional<OrderSummary> findOrderSummary(@Param("id") Long id);解决方案4:Open Session in View(不推荐)
yaml
undefinedKeeps session open during view rendering
在视图渲染期间保持会话开启
⚠️ Can mask N+1 problems, use with caution
⚠️ 可能掩盖N+1问题,请谨慎使用
spring:
jpa:
open-in-view: true # Default is true
---spring:
jpa:
open-in-view: true # 默认值为true
---Transactions
事务
Basic Transaction Management
基础事务管理
java
@Service
public class OrderService {
// Read-only: Optimized, no dirty checking
@Transactional(readOnly = true)
public Order findById(Long id) {
return orderRepository.findById(id).orElseThrow();
}
// Write: Full transaction with dirty checking
@Transactional
public Order createOrder(CreateOrderRequest request) {
Order order = new Order();
// ... set properties
return orderRepository.save(order);
}
// Explicit rollback
@Transactional(rollbackFor = Exception.class)
public void processPayment(Long orderId) throws PaymentException {
// Rolls back on any exception, not just RuntimeException
}
}java
@Service
public class OrderService {
// 只读事务:优化性能,无脏检查
@Transactional(readOnly = true)
public Order findById(Long id) {
return orderRepository.findById(id).orElseThrow();
}
// 写事务:完整事务支持,包含脏检查
@Transactional
public Order createOrder(CreateOrderRequest request) {
Order order = new Order();
// ... 设置属性
return orderRepository.save(order);
}
// 显式指定回滚异常
@Transactional(rollbackFor = Exception.class)
public void processPayment(Long orderId) throws PaymentException {
// 任何异常都会回滚,不仅仅是RuntimeException
}
}Transaction Propagation
事务传播机制
java
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
// REQUIRED (default): Uses existing or creates new
paymentService.processPayment(order);
// If paymentService throws, entire order is rolled back
}
}
@Service
public class PaymentService {
// REQUIRES_NEW: Always creates new transaction
// If this fails, order can still be saved
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment(Order order) {
// Independent transaction
}
// MANDATORY: Must run within existing transaction
@Transactional(propagation = Propagation.MANDATORY)
public void updatePaymentStatus(Order order) {
// Throws if no transaction exists
}
}java
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
// REQUIRED(默认):使用现有事务,不存在则新建
paymentService.processPayment(order);
// 如果paymentService抛出异常,整个订单操作都会回滚
}
}
@Service
public class PaymentService {
// REQUIRES_NEW:总是新建独立事务
// 如果该方法失败,订单仍然可以保存
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment(Order order) {
// 独立事务上下文
}
// MANDATORY:必须在现有事务中运行
@Transactional(propagation = Propagation.MANDATORY)
public void updatePaymentStatus(Order order) {
// 如果没有事务则抛出异常
}
}Common Transaction Mistakes
常见事务使用错误
java
// ❌ BAD: Calling @Transactional method from same class
@Service
public class OrderService {
public void processOrder(Long id) {
updateOrder(id); // @Transactional is IGNORED!
}
@Transactional
public void updateOrder(Long id) {
// Transaction not started because called internally
}
}
// ✅ GOOD: Inject self or use separate service
@Service
public class OrderService {
@Autowired
private OrderService self; // Or use separate service
public void processOrder(Long id) {
self.updateOrder(id); // Now transaction works
}
@Transactional
public void updateOrder(Long id) {
// Transaction properly started
}
}java
// ❌ 错误示例:同一个类内部调用@Transactional方法
@Service
public class OrderService {
public void processOrder(Long id) {
updateOrder(id); // @Transactional注解会被忽略!
}
@Transactional
public void updateOrder(Long id) {
// 因为是内部调用,事务不会开启
}
}
// ✅ 正确示例:注入自身代理或者使用单独的服务类
@Service
public class OrderService {
@Autowired
private OrderService self; // 或者使用单独的服务类
public void processOrder(Long id) {
self.updateOrder(id); // 现在事务会正常开启
}
@Transactional
public void updateOrder(Long id) {
// 事务正常开启
}
}Entity Relationships
实体关系
OneToMany / ManyToOne
OneToMany / ManyToOne
java
// ✅ GOOD: Bidirectional with proper mapping
@Entity
public class Author {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books = new ArrayList<>();
// Helper methods for bidirectional sync
public void addBook(Book book) {
books.add(book);
book.setAuthor(this);
}
public void removeBook(Book book) {
books.remove(book);
book.setAuthor(null);
}
}
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
}java
// ✅ 正确示例:双向关联且映射正确
@Entity
public class Author {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books = new ArrayList<>();
// 双向关联同步辅助方法
public void addBook(Book book) {
books.add(book);
book.setAuthor(this);
}
public void removeBook(Book book) {
books.remove(book);
book.setAuthor(null);
}
}
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
}ManyToMany
ManyToMany
java
// ✅ GOOD: ManyToMany with Set (not List) to avoid duplicates
@Entity
public class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
public void addCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void removeCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
}
@Entity
public class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}java
// ✅ 正确示例:ManyToMany使用Set而非List避免重复
@Entity
public class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
public void addCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void removeCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
}
@Entity
public class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}equals() and hashCode() for Entities
实体的equals()和hashCode()方法
java
// ✅ GOOD: Use business key or ID carefully
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NaturalId // Hibernate annotation for business key
@Column(unique = true, nullable = false)
private String isbn;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book book)) return false;
return isbn != null && isbn.equals(book.isbn);
}
@Override
public int hashCode() {
return Objects.hash(isbn); // Use business key, not ID
}
}java
// ✅ 正确示例:谨慎使用业务键或者ID
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NaturalId // Hibernate提供的业务键注解
@Column(unique = true, nullable = false)
private String isbn;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book book)) return false;
return isbn != null && isbn.equals(book.isbn);
}
@Override
public int hashCode() {
return Objects.hash(isbn); // 使用业务键而非ID
}
}Query Optimization
查询优化
Pagination
分页
java
// ✅ GOOD: Always paginate large result sets
public interface OrderRepository extends JpaRepository<Order, Long> {
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
// With sorting
@Query("SELECT o FROM Order o WHERE o.status = :status")
Page<Order> findByStatusSorted(
@Param("status") OrderStatus status,
Pageable pageable
);
}
// Usage
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<Order> orders = orderRepository.findByStatus(OrderStatus.PENDING, pageable);java
// ✅ 正确示例:大数据集始终分页
public interface OrderRepository extends JpaRepository<Order, Long> {
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
// 带排序
@Query("SELECT o FROM Order o WHERE o.status = :status")
Page<Order> findByStatusSorted(
@Param("status") OrderStatus status,
Pageable pageable
);
}
// 使用方式
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<Order> orders = orderRepository.findByStatus(OrderStatus.PENDING, pageable);DTO Projections
DTO投影
java
// ✅ GOOD: Fetch only needed columns
// Interface-based projection
public interface OrderSummary {
Long getId();
String getCustomerName();
BigDecimal getTotal();
}
@Query("SELECT o.id as id, o.customer.name as customerName, o.total as total " +
"FROM Order o WHERE o.status = :status")
List<OrderSummary> findOrderSummaries(@Param("status") OrderStatus status);
// Class-based projection (DTO)
public record OrderDTO(Long id, String customerName, BigDecimal total) {}
@Query("SELECT new com.example.dto.OrderDTO(o.id, o.customer.name, o.total) " +
"FROM Order o WHERE o.status = :status")
List<OrderDTO> findOrderDTOs(@Param("status") OrderStatus status);java
// ✅ 正确示例:仅抓取需要的列
// 基于接口的投影
public interface OrderSummary {
Long getId();
String getCustomerName();
BigDecimal getTotal();
}
@Query("SELECT o.id as id, o.customer.name as customerName, o.total as total " +
"FROM Order o WHERE o.status = :status")
List<OrderSummary> findOrderSummaries(@Param("status") OrderStatus status);
// 基于类的投影(DTO)
public record OrderDTO(Long id, String customerName, BigDecimal total) {}
@Query("SELECT new com.example.dto.OrderDTO(o.id, o.customer.name, o.total) " +
"FROM Order o WHERE o.status = :status")
List<OrderDTO> findOrderDTOs(@Param("status") OrderStatus status);Bulk Operations
批量操作
java
// ✅ GOOD: Bulk update instead of loading entities
public interface OrderRepository extends JpaRepository<Order, Long> {
@Modifying
@Query("UPDATE Order o SET o.status = :status WHERE o.createdAt < :date")
int updateOldOrdersStatus(
@Param("status") OrderStatus status,
@Param("date") LocalDateTime date
);
@Modifying
@Query("DELETE FROM Order o WHERE o.status = :status AND o.createdAt < :date")
int deleteOldOrders(
@Param("status") OrderStatus status,
@Param("date") LocalDateTime date
);
}
// Usage
@Transactional
public void archiveOldOrders() {
LocalDateTime threshold = LocalDateTime.now().minusYears(1);
int updated = orderRepository.updateOldOrdersStatus(
OrderStatus.ARCHIVED,
threshold
);
log.info("Archived {} orders", updated);
}java
// ✅ 正确示例:批量更新而非加载所有实体
public interface OrderRepository extends JpaRepository<Order, Long> {
@Modifying
@Query("UPDATE Order o SET o.status = :status WHERE o.createdAt < :date")
int updateOldOrdersStatus(
@Param("status") OrderStatus status,
@Param("date") LocalDateTime date
);
@Modifying
@Query("DELETE FROM Order o WHERE o.status = :status AND o.createdAt < :date")
int deleteOldOrders(
@Param("status") OrderStatus status,
@Param("date") LocalDateTime date
);
}
// 使用方式
@Transactional
public void archiveOldOrders() {
LocalDateTime threshold = LocalDateTime.now().minusYears(1);
int updated = orderRepository.updateOldOrdersStatus(
OrderStatus.ARCHIVED,
threshold
);
log.info("已归档 {} 个订单", updated);
}Optimistic Locking
乐观锁
Prevent Lost Updates
防止更新丢失
java
// ✅ GOOD: Use @Version for optimistic locking
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
private OrderStatus status;
private BigDecimal total;
}
// When two users update same order:
// User 1: loads order (version=1), modifies, saves → version becomes 2
// User 2: loads order (version=1), modifies, saves → OptimisticLockException!java
// ✅ 正确示例:使用@Version实现乐观锁
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
private OrderStatus status;
private BigDecimal total;
}
// 当两个用户同时更新同一个订单时:
// 用户1:加载订单(version=1),修改后保存 → version变为2
// 用户2:加载订单(version=1),修改后保存 → 抛出OptimisticLockException!Handling OptimisticLockException
处理OptimisticLockException
java
@Service
public class OrderService {
@Transactional
public Order updateOrder(Long id, UpdateOrderRequest request) {
try {
Order order = orderRepository.findById(id).orElseThrow();
order.setStatus(request.getStatus());
return orderRepository.save(order);
} catch (OptimisticLockException e) {
throw new ConcurrentModificationException(
"Order was modified by another user. Please refresh and try again."
);
}
}
// Or with retry
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
@Transactional
public Order updateOrderWithRetry(Long id, UpdateOrderRequest request) {
Order order = orderRepository.findById(id).orElseThrow();
order.setStatus(request.getStatus());
return orderRepository.save(order);
}
}java
@Service
public class OrderService {
@Transactional
public Order updateOrder(Long id, UpdateOrderRequest request) {
try {
Order order = orderRepository.findById(id).orElseThrow();
order.setStatus(request.getStatus());
return orderRepository.save(order);
} catch (OptimisticLockException e) {
throw new ConcurrentModificationException(
"订单已被其他用户修改,请刷新后重试。"
);
}
}
// 或者添加重试机制
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
@Transactional
public Order updateOrderWithRetry(Long id, UpdateOrderRequest request) {
Order order = orderRepository.findById(id).orElseThrow();
order.setStatus(request.getStatus());
return orderRepository.save(order);
}
}Common Mistakes
常见错误
1. Cascade Misuse
1. 级联策略误用
java
// ❌ BAD: CascadeType.ALL on @ManyToOne
@Entity
public class Book {
@ManyToOne(cascade = CascadeType.ALL) // Dangerous!
private Author author;
}
// Deleting a book could delete the author!
// ✅ GOOD: Cascade only from parent to child
@Entity
public class Author {
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books;
}java
// ❌ 错误示例:在@ManyToOne上使用CascadeType.ALL
@Entity
public class Book {
@ManyToOne(cascade = CascadeType.ALL) // 非常危险!
private Author author;
}
// 删除一本书可能会连带删除对应的作者!
// ✅ 正确示例:仅从父级到子级使用级联
@Entity
public class Author {
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books;
}2. Missing Index
2. 缺少索引
java
// ❌ BAD: Frequent queries on non-indexed column
@Query("SELECT o FROM Order o WHERE o.customerEmail = :email")
List<Order> findByCustomerEmail(@Param("email") String email);
// ✅ GOOD: Add index
@Entity
@Table(indexes = @Index(name = "idx_order_customer_email", columnList = "customerEmail"))
public class Order {
private String customerEmail;
}java
// ❌ 错误示例:对频繁查询的列未加索引
@Query("SELECT o FROM Order o WHERE o.customerEmail = :email")
List<Order> findByCustomerEmail(@Param("email") String email);
// ✅ 正确示例:添加索引
@Entity
@Table(indexes = @Index(name = "idx_order_customer_email", columnList = "customerEmail"))
public class Order {
private String customerEmail;
}3. toString() with Lazy Fields
3. toString()包含懒加载字段
java
// ❌ BAD: toString includes lazy collection
@Entity
public class Author {
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
@Override
public String toString() {
return "Author{id=" + id + ", books=" + books + "}"; // Triggers lazy load!
}
}
// ✅ GOOD: Exclude lazy fields from toString
@Override
public String toString() {
return "Author{id=" + id + ", name='" + name + "'}";
}java
// ❌ 错误示例:toString包含懒加载集合
@Entity
public class Author {
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
@Override
public String toString() {
return "Author{id=" + id + ", books=" + books + "}"; // 会触发懒加载!
}
}
// ✅ 正确示例:toString排除懒加载字段
@Override
public String toString() {
return "Author{id=" + id + ", name='" + name + "'}";
}Performance Checklist
性能检查清单
When reviewing JPA code, check:
- No N+1 queries (use JOIN FETCH or @EntityGraph)
- LAZY fetch by default (especially @ManyToOne)
- Pagination for large result sets
- DTO projections for read-only queries
- Bulk operations for batch updates/deletes
- @Version for entities with concurrent access
- Indexes on frequently queried columns
- No lazy fields in toString()
- Read-only transactions where applicable
评审JPA代码时,请检查以下项:
- 无N+1查询(使用JOIN FETCH或@EntityGraph解决)
- 默认使用LAZY抓取策略(尤其是@ManyToOne关联)
- 大数据集使用分页
- 只读查询使用DTO投影
- 批量更新/删除使用批量操作
- 存在并发访问的实体添加@Version乐观锁
- 频繁查询的列添加索引
- toString()中不包含懒加载字段
- 适用场景使用只读事务
Related Skills
相关技能
- - Spring Boot controller/service patterns
spring-boot-patterns - - General code review checklist
java-code-review - - Code quality principles
clean-code
- - Spring Boot controller/service最佳实践
spring-boot-patterns - - 通用代码评审检查清单
java-code-review - - 代码质量原则
clean-code