上一文中我们使用@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进一步处理吧。