jpa-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

JPA 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

快速参考:常见问题

ProblemSymptomSolution
N+1 queriesMany SELECT statementsJOIN FETCH, @EntityGraph
LazyInitializationExceptionError outside transactionOpen Session in View, DTO projection, JOIN FETCH
Slow queriesPerformance issuesPagination, projections, indexes
Dirty checking overheadSlow updatesRead-only transactions, DTOs
Lost updatesConcurrent modificationsOptimistic 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=25
java
// ✅ 正确示例:批量抓取(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=25

Detecting N+1

检测N+1问题

yaml
undefined
yaml
undefined

Enable 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
undefined

Keeps 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-patterns
    - Spring Boot controller/service patterns
  • java-code-review
    - General code review checklist
  • clean-code
    - Code quality principles
  • spring-boot-patterns
    - Spring Boot controller/service最佳实践
  • java-code-review
    - 通用代码评审检查清单
  • clean-code
    - 代码质量原则