---
name: spring-boot-patterns-rules
description: "Spring Boot 与 JPA/MyBatis 优化规范。适用于所有 Java 后端开发与数据库交互优化。"
globs: ["src/main/java/**/*.java", "**/controller/*.java", "**/service/*.java", "**/repository/*.java"]
alwaysApply: false
updated: 2026-05-22
---

# Spring Boot 与 JPA/MyBatis 优化规范

> [!IMPORTANT]
> 数据库的交互效率与多线程安全直接决定了 Java 后端服务的吞吐上限与系统稳定性。AI 助手在编写 Spring Boot 代码时，必须拦截隐式 N+1 查询风暴，防御高并发下的脏数据覆盖，并绝对确保单例 Bean 的线程安全性。

## 1. 适用场景

当您定义 Entity 实体、编写 Repository/Mapper 数据库接口、开发 Service 业务逻辑，或使用 `@Transactional` 控制事务与处理并发数量扣减时生效。

---

## 2. 操作规则

### 2.1 彻底拦截 N+1 查询 (Zero N+1 Query)
- **JPA/Hibernate 场景**：
  - 凡是在关联关系中定义了 `@ManyToOne` 或 `@OneToOne`，默认必须使用 `FetchType.LAZY`（懒加载），严禁裸用默认的 `FetchType.EAGER`。
  - 在需要批量展示关联列表数据时，必须显式在 Repository 中使用 **`@EntityGraph`** 或带有 **`JOIN FETCH`** 的 JPQL 联表查询，绝对禁止在 `for` 循环内部遍历 Entity 触发隐式懒加载 SQL 查询。
- **MyBatis 场景**：
  - 严禁在 `<resultMap>` 中配置带有 `select` 属性的分步查询嵌套，这会造成严重的 N+1。必须通过联表 SQL 并配合 `<association>` 或 `<collection>` 的嵌套结果映射一次性查出。

### 2.2 高并发状态原子扣减 (Atomic Concurrency Locking)
- **并发扣减与防超卖**：
  - 当处理商品库存、账户余额等并发极高的数字扣减时，绝对禁止将实体读入 JVM 内存计算后再进行 `save` / `update` 覆盖。
  - 必须使用**乐观锁**（在 Entity 中配置 `@Version` 字段）自动截获并发冲突；或者在 Repository 接口中使用 **`@Lock(LockModeType.PESSIMISTIC_WRITE)`** (或在 MyBatis 中显式编写 `SELECT ... FOR UPDATE`) 引入底层排他锁，保障强一致性扣减。

### 2.3 声明式事务 (@Transactional) 防御失效
- **切面失效防御**：
  - `@Transactional` 注解必须**仅**标注在 `public` 修饰的方法上。标注在 `private` 或 `protected` 方法上事务会自动失效。
  - 严禁在同一个 Service 类的内部非事务方法直接自调用（Self-invocation）带有 `@Transactional` 的事务方法（因为这会绕过 Spring AOP 动态代理）。
  - 若需要自调用，必须通过注入自身代理（如使用 `@Autowired` 延迟注入 `self`）或将事务方法剥离到独立的 Helper/Service 组件中。

### 2.4 单例模式 Bean 线程安全 (Singleton Thread-Safety)
- **成员变量无状态化**：
  - Spring 管理的 `@Service`、`@RestController`、`@Component` 默认均为 **Singleton（单例）**。
  - 绝对禁止在这些类中声明与请求级数据挂钩的、带状态的共享类成员变量（如保存当前登录用户的 `private User currentUser`），这在多线程并发请求下会导致严重的用户数据串写。所有请求级状态必须通过方法参数传递，或使用线程隔离的 `ThreadLocal`（如 `RequestContextHolder`）进行安全存取。

---

## 3. 禁止事项

- 绝对禁止在 `for` 或 `while` 循环体内部调用 `JpaRepository.findById()` 或 MyBatis Mapper 接口，必须收集 ID 列表后使用 `findAllById(List<ID> ids)` 或 `<foreach>` 批量查出，在 JVM 内存中进行 Map 转换匹配。
- 严禁在 `@Transactional` 事务方法内部执行耗时极长的外部 HTTP API 调用或复杂计算，这会长时间霸占数据库连接池连接（Connection），导致连接池枯竭崩溃。长耗时外部调用必须剥离在事务外部执行。

---

## 4. 验证方式

- 运行测试用例时开启 `spring.jpa.properties.hibernate.generate_statistics=true` 统计 SQL 执行条数。
- 对并发接口执行 JUnit 或 JMeter 并发压力测试，验证数据一致性。

---

## 5. 代码对比示例

### ❌ 错误示例（隐式 N+1 导致数据库瘫痪、并发扣减覆盖导致超卖、自调用事务失效）

```java
@Service
public class OrderService {
    @Autowired
    private BookRepository bookRepository;

    // 违反规则 2.3：自调用会导致 `@Transactional` 事务切面失效，发生异常时无法回滚
    public void createOrder(Long bookId, int qty) {
        this.processPurchase(bookId, qty); 
    }

    @Transactional
    public void processPurchase(Long bookId, int qty) {
        Book book = bookRepository.findById(bookId)
            .orElseThrow(() -> new ResourceNotFoundException("书本未找到"));
        
        // 违反规则 2.2：高并发下，多线程同时读取到 stock=10，计算后并发保存，导致超卖
        int newStock = book.getStock() - qty;
        book.setStock(newStock);
        bookRepository.save(book);
    }

    // 违反规则 2.1：在循环中遍历懒加载关联属性，产生 N+1 次 SQL 联查
    public List<BookResponse> listBooks() {
        List<Book> books = bookRepository.findAll();
        List<BookResponse> responses = new ArrayList<>();
        for (Book book : books) {
            responses.add(new BookResponse(
                book.getTitle(),
                book.getAuthor().getName() // 触发隐式外键懒加载 SQL！
            ));
        }
        return responses;
    }
}
```

###  正确方向（EntityGraph 联表预加载、行锁防超卖、规避自调用）

```java
@Service
public class OrderService {
    @Autowired
    private BookRepository bookRepository;
    
    @Autowired
    @Lazy
    private OrderService self; // 遵循规则 2.3：注入自身代理，规避自调用事务失效

    public void createOrder(Long bookId, int qty) {
        // 通过代理调用，确保 @Transactional 事务切面正常生效
        self.processPurchase(bookId, qty);
    }

    @Transactional
    public void processPurchase(Long bookId, int qty) {
        // 遵循规则 2.2：使用行锁（SELECT ... FOR UPDATE）防超卖，保障高并发原子性
        Book book = bookRepository.findAndLockById(bookId)
            .orElseThrow(() -> new ResourceNotFoundException("书本未找到"));
        
        if (book.getStock() < qty) {
            throw new InsufficientStockException("库存不足，扣减失败。");
        }
        
        // 数据库层面原子扣减
        bookRepository.decreaseStock(bookId, qty);
    }

    // 遵循规则 2.1：利用 JPA EntityGraph 强制联表预加载，1 条 SQL 搞定，消灭 N+1
    public List<BookResponse> listBooksOptimized() {
        List<Book> books = bookRepository.findAllWithAuthor();
        return books.stream()
            .map(b -> new BookResponse(b.getTitle(), b.getAuthor().getName()))
            .collect(Collectors.toList());
    }
}

// Repository 接口声明
interface BookRepository extends JpaRepository<Book, Long> {
    
    // 遵循规则 2.1：通过 EntityGraph 预加载关联的 author 字段
    @EntityGraph(attributePaths = {"author"})
    @Query("SELECT b FROM Book b")
    List<Book> findAllWithAuthor();

    // 遵循规则 2.2：使用排他写锁
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT b FROM Book b WHERE b.id = :id")
    Optional<Book> findAndLockById(@Param("id") Long id);

    @Modifying
    @Query("UPDATE Book b SET b.stock = b.stock - :qty WHERE b.id = :id AND b.stock >= :qty")
    int decreaseStock(@Param("id") Long id, @Param("qty") int qty);
}
```
