使用 Hibernate/JPA 等 O/R Mapping 框架時,延遲加載雖避免了無用數據加載,但易導致著名的 N+1 查詢問題:遍歷集合時每個元素觸發一次 SQL,嚴重拖垮性能。預先抓取又可能過度加載。本文展示基於 Spring AOP + 策略模式 實現業務場景感知的動態抓取策略:在 Repository 層外部攔截查詢執行前,根據調用鏈 (業務方法) 動態注入最優抓取策略 (JOIN FETCH / EntityGraph / 子查詢),實現零侵入、可擴展、緩存友好的數據訪問優化。
// 關係:Employee *--1 Department, Employee 1--* Dependent
@Entity class Employee {
@Id Long id;
@ManyToOne(fetch = LAZY) Department department; // 默認延遲
@OneToMany(mappedBy = "employee", fetch = LAZY) List dependents;
}
@Entity class Department { @Id Long id; String name; }
@Entity class Dependent { @Id Long id; @ManyToOne Employee employee; String name; }
三個業務操作需不同數據:
若 Repository 寫死 findById 邏輯,要么 N+1,要么過度加載。
┌─────────────────────────────────────────────────────────────┐
│ Spring AOP 切面層 │
│ @Aspect │
│ class FetchingStrategyAspect { │
│ // 識別業務流程切點 │
│ @Pointcut("execution(* EmployeeService.getEmployeeWithDepartment*(..))") │
│ void departmentFlow() {} │
│ @Pointcut("execution(* EmployeeService.getEmployeeWithDependents*(..))") │
│ void dependentsFlow() {} │
│ │
│ // 在 Repository 方法執行前注入策略 │
│ @Before("departmentFlow() && target(repo)") │
│ void injectDepartmentStrategy(EmployeeRepository repo) { │
│ repo.setFetchingStrategy(new DepartmentFetchStrategy()); │
│ } │
│ @Before("dependentsFlow() && target(repo)") │
│ void injectDependentsStrategy(EmployeeRepository repo) { │
│ repo.setFetchingStrategy(new DependentsFetchStrategy()); │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Repository 層 (無狀態) │
│ interface EmployeeRepository { │
│ Employee findById(Long id); │
│ void setFetchingStrategy(FetchingStrategy strategy); │
│ } │
│ │
│ class EmployeeRepositoryImpl implements EmployeeRepository {│
│ private FetchingStrategy fetchingStrategy = new DefaultFetchStrategy(); │
│ │
│ public Employee findById(Long id) { │
│ // 委託策略構建查詢 │
│ String hql = fetchingStrategy.buildQuery("findById"); │
│ return hibernateTemplate.find(hql, id).get(0); │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 策略實現 (可插拔) │
│ interface FetchingStrategy { │
│ String buildQuery(String operation); │
│ } │
│ │
│ class DefaultFetchStrategy implements FetchingStrategy { │
│ public String buildQuery(String op) { │
│ return "from Employee e where e.id = ?"; │
│ } │
│ } │
│ │
│ class DepartmentFetchStrategy implements FetchingStrategy {│
│ public String buildQuery(String op) { │
│ return "from Employee e JOIN FETCH e.department where e.id = ?"; │
│ } │
│ } │
│ │
│ class DependentsFetchStrategy implements FetchingStrategy {│
│ public String buildQuery(String op) { │
│ return "from Employee e JOIN FETCH e.dependents where e.id = ?"; │
│ } │
│ } │
│ │
│ class DeptAndDepFetchStrategy implements FetchingStrategy {│
│ public String buildQuery(String op) { │
│ return "from Employee e " + │
│ "JOIN FETCH e.department " + │
│ "JOIN FETCH e.dependents " + │
│ "where e.id = ?"; │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
public interface FetchingStrategy {
String buildQuery(String operationName);
}
@Component("defaultFetchStrategy")
public class DefaultFetchStrategy implements FetchingStrategy {
@Override public String buildQuery(String operationName) {
return "from Employee e where e.id = ?";
}
}
@Component("departmentFetchStrategy")
public class DepartmentFetchStrategy implements FetchingStrategy {
@Override public String buildQuery(String operationName) {
return "from Employee e JOIN FETCH e.department where e.id = ?";
}
}
@Component("dependentsFetchStrategy")
public class DependentsFetchStrategy implements FetchingStrategy {
@Override public String buildQuery(String operationName) {
// 對集合使用 JOIN FETCH 可能導致笛卡爾積,大集合建議 SUBSELECT 或分批
return "from Employee e JOIN FETCH e.dependents where e.id = ?";
}
}
@Component("deptAndDepFetchStrategy")
public class DeptAndDepFetchStrategy implements FetchingStrategy {
@Override public String buildQuery(String operationName) {
return "from Employee e " +
"JOIN FETCH e.department " +
"JOIN FETCH e.dependents " +
"where e.id = ?";
}
}
public interface EmployeeRepository {
Employee findById(Long id);
void setFetchingStrategy(FetchingStrategy strategy);
}
@Repository
public class EmployeeRepositoryImpl implements EmployeeRepository {
@Autowired @Qualifier("defaultFetchStrategy")
private FetchingStrategy fetchingStrategy; // 默認注入
@PersistenceContext EntityManager em;
@Override
public Employee findById(Long id) {
String hql = fetchingStrategy.buildQuery("findById");
TypedQuery query = em.createQuery(hql, Employee.class);
query.setParameter(1, id);
return query.getSingleResult();
}
@Override
public void setFetchingStrategy(FetchingStrategy strategy) {
this.fetchingStrategy = strategy;
}
}
@Aspect
@Component
@Slf4j
public class FetchingStrategyAspect {
// 業務流程切點:服務層方法
@Pointcut("execution(* com.example.service.EmployeeService.getEmployeeWithDepartment*(..))")
public void departmentFlow() {}
@Pointcut("execution(* com.example.service.EmployeeService.getEmployeeWithDependents*(..))")
public void dependentsFlow() {}
@Pointcut("execution(* com.example.service.EmployeeService.getEmployeeWithDepartmentAndDependents*(..))")
public void deptAndDepFlow() {}
// Repository 目標對象切點
@Pointcut("target(com.example.repository.EmployeeRepository)")
public void repositoryTarget() {}
// 組合:在業務流程中調用 Repository 方法前注入策略
@Before("departmentFlow() && repositoryTarget()")
public void injectDepartmentStrategy(JoinPoint jp) {
EmployeeRepository repo = (EmployeeRepository) jp.getTarget();
repo.setFetchingStrategy(new DepartmentFetchStrategy());
log.debug("注入 DepartmentFetchStrategy 到 {}", repo.getClass().getSimpleName());
}
@Before("dependentsFlow() && repositoryTarget()")
public void injectDependentsStrategy(JoinPoint jp) {
EmployeeRepository repo = (EmployeeRepository) jp.getTarget();
repo.setFetchingStrategy(new DependentsFetchStrategy());
}
@Before("deptAndDepFlow() && repositoryTarget()")
public void injectDeptAndDepStrategy(JoinPoint jp) {
EmployeeRepository repo = (EmployeeRepository) jp.getTarget();
repo.setFetchingStrategy(new DeptAndDepFetchStrategy());
}
}
@Service
@Transactional(readOnly = true)
public class EmployeeService {
@Autowired EmployeeRepository employeeRepository;
public Employee getEmployeeBasic(Long id) {
return employeeRepository.findById(id); // 使用默認策略
}
public Employee getEmployeeWithDepartment(Long id) {
return employeeRepository.findById(id); // AOP 注入 Department 策略
}
public Employee getEmployeeWithDependents(Long id) {
return employeeRepository.findById(id); // AOP 注入 Dependents 策略
}
public Employee getEmployeeWithDepartmentAndDependents(Long id) {
return employeeRepository.findById(id); // AOP 注入聯合策略
}
}
| 優勢 | 說明 |
|---|---|
| 零侵入業務邏輯 | Service/Repository 無 if-else,新增場景只需加切點+策略類 |
| 單一職責 | 抓取策略集中在策略類,業務邏輯專注流程 |
| 緩存友好 | 策略可返回 EntityGraph 或 HQL,Hibernate 二級緩存正常工作 (不像 fetch join 繞過緩存) |
| 事務邊界清晰 | Service @Transactional 統一管理,Repository 無狀態可安全注入策略 |
| 可測試性 | 策略類可獨立單測,Repository 可 Mock 策略驗證查詢構建 |
@Component
public class EntityGraphFetchStrategy implements FetchingStrategy {
@PersistenceContext EntityManager em;
@Override public String buildQuery(String operationName) {
// 不返回 HQL,而是通過 EntityManager 構建圖
throw new UnsupportedOperationException("使用 applyGraph 方法");
}
public void applyGraph(TypedQuery> query, String graphName) {
EntityGraph> graph = em.getEntityGraph(graphName);
query.setHint("javax.persistence.fetchgraph", graph);
}
}
// 使用:@NamedEntityGraph(name="Employee.withDepartment", attributeNodes = @NamedAttributeNode("department"))
// 切面中調用 strategy.applyGraph(query, "Employee.withDepartment");
setFetchingStrategy 修改實例字段 → **併發環境下線程不安全**!解決:ThreadLocal 存儲策略,或每請求創建 Repository (原型作用域),或使用 EntityGraph Hint 方式無狀態傳遞@After 切面清理 ThreadLocal 或恢復默認基於 Spring AOP 的動態抓取策略模式,將「什麼時候用什麼查詢」從 Repository 遷移到切面層,實現了業務場景與數據訪問策略的解耦。配合 ThreadLocal 解決線程安全、EntityGraph 實現類型安全、@BatchSize 緩解集合加載,構建出高性能、可維護、緩存友好的數據訪問層。此模式適用於 Hibernate/JPA/MyBatis 等所有支持動態查詢構建的 ORM 框架。