上一文中我們使用@ManyToOne、@OneToMany進行自關聯查詢,遇到的“N+1”問題需要通過@NamedEntityGraph來解決。
Entity:
/** * 典型的 多層級 分類 * <p> * :@NamedEntityGraph :注解在實體上 , 解決典型的N+1問題 * name表示實體圖名, 與 repository中的注解 @EntityGraph的value屬性相對應, * attributeNodes 表示被標注要懶加載的屬性節點 比如此例中 : 要懶加載的子分類集合children */ @Entity @Table @Data @NamedEntityGraph(name = "Category.Graph", attributeNodes = {@NamedAttributeNode("children")}) public class Category { @Id @GeneratedValue private Long id; // 分類名 private String name; // 一個商品分類下面可能有多個商品子分類(多級) 比如 分類 : 家用電器 (子)分類 : 電腦 (孫)子分類 : 筆記本電腦 @ManyToOne(fetch = FetchType.LAZY) @JsonIgnore private Category parent; //父分類 // @JsonInclude(JsonInclude.Include.NON_EMPTY) // 不要使用值為null或內容為空的屬性。 @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) private List<Category> children; //子分類集合,用Set代替List時,請在類上添加注解@EqualsAndHashCode(exclude = "children") }
Repository:
public interface CategoryRepository extends JpaRepository<Category, Long> { /** * 解決 懶加載 JPA 典型的 N + 1 問題 */ @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH) List<Category> findAll(); @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH) // 無效, 始終存在N+1問題 Category findByName(String name); @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH) // 無效, 始終存在N+1問題 Optional<Category> findById(Long id); }
Controller:
@RestController public class Output { @Autowired private CategoryRepository categoryRepository; @GetMapping("category") public Category getCategory() { List<Category> categories = categoryRepository.findAll(); // return categories.get(0); // 一條SQL // return categories.stream().filter(category -> category.getName().equals("家用電器")).findFirst().orElse(null); // 一條SQL return categoryRepository.findByName("家用電器"); // 無論findByName(String name)方法加不加@EntityGraph,都不會觸發N+1查詢,對比下文更神奇 } @GetMapping("category/name/{name}") public Category getCategoryByName(@PathVariable String name) { // 雖然findByName(String name)添加了@EntityGraph,但是沒有起作用, 依然存在N+1問題 return categoryRepository.findByName(name); } @GetMapping("category/id/{id}") public Category getCategoryById(@PathVariable Long id) { // 雖然findById(Long id)添加了@EntityGraph,但是沒有起作用, 依然存在N+1問題 return categoryRepository.findById(id).orElse(null); } @GetMapping("categories") public List<Category> getCategories() { return categoryRepository.findAll(); } }
插入數據:

@Autowired private CategoryRepository categoryRepository; @Test public void addCategory() { //一個 家用電器分類(頂級分類) Category appliance = new Category(); appliance.setName("家用電器"); categoryRepository.save(appliance); //家用電器 下面的 電腦分類(二級分類) Category computer = new Category(); computer.setName("電腦"); computer.setParent(appliance); categoryRepository.save(computer); //電腦 下面的 筆記本電腦分類(三級分類) Category notebook = new Category(); notebook.setName("筆記本電腦"); notebook.setParent(computer); categoryRepository.save(notebook); //家用電器 下面的 手機分類(二級分類) Category mobile = new Category(); mobile.setName("手機"); mobile.setParent(appliance); categoryRepository.save(mobile); //手機 下面的 智能機 / 老人機(三級分類) Category smartPhone = new Category(); smartPhone.setName("智能機"); smartPhone.setParent(mobile); categoryRepository.save(smartPhone); Category oldPhone = new Category(); oldPhone.setName("老人機"); oldPhone.setParent(mobile); categoryRepository.save(oldPhone); }
數據庫條目:
返回結果:

// 20200611150503 // http://localhost:8080/category { "id": 6, "name": "家用電器", "children": [ { "id": 7, "name": "電腦", "children": [ { "id": 8, "name": "筆記本電腦", "children": [ ] } ] }, { "id": 9, "name": "手機", "children": [ { "id": 10, "name": "智能機", "children": [ ] }, { "id": 11, "name": "老人機", "children": [ ] } ] } ] }
神奇的現象出現了
- List<Category> findAll()方法,無論對List進行索引取值序列化,還是直接JSON序列化List,都不會觸發“N+1”問題;
- Category findByName(String name)方法,通過"category/id/{id}"訪問,總是有N+1問題,通過"category"訪問則沒有"N+1”問題(兩條SQL);
- Optional<Category> findById(Long id)方法,總是有N+1問題;
原因判斷:
- 不熟悉@EntityGraph或者是@OneToMany的細節表現;
- 由於Jackson序列化導致的問題;
- 其他問題
解決辦法:
- 總是使用List<Category> findAll()方法,然后通過stream進行結果流處理
參考鏈接:
代碼樣例:
6月11日更新:
- 原因大致找到了,只要先運行帶有@EntityGraph的List<Category> findAll()方法,那么后續的Option<Category> findById(Long id)等方法無論帶不帶@EntityGraph,都不會觸發N+1查詢,看來是一級緩存的功勞;
- 為什么@EntityGraph只對findAll起效的原因還在找,暫時就先用這個方法把結果都查詢出來,然后利用Stream API進一步處理吧。