注解 @NamedEntityGraph 解决 JPA 懒加载典型的 N+1 问题

因为在设计一个树形结构的实体中用到了多对一, 一对多的映射关系, 在加载其关联对象的时候, 为了性能考虑, 很自然的想到了懒加载.

也由此遇到了 N+1 的典型问题 : 通常 1 的这方, 通过 1 条 SQL 查找得到 1 个对象, 而 JPA 基于 Hibernate,fetch 策略默认为 select(并非联表查询), 由于关联的存在 , 又需要将这个对象关联的集合取出, 集合数量是 N, 则要发出 N 条 SQL, 于是本来的 1 条联表查询 SQL 可解决的问题变成了 N+1 条 SQL

我采取的解决方法是 : 不修改懒加载策略, JPA 也不写 native SQL, 通过联表查询进行解决.

如果对该例子比较感兴趣或者觉得言语表达比较啰嗦, 可查看完整的 demo 地址 :

场景如下 :

我设计了一个典型的二叉树结构实体叫做 Area, 代表的含义是区域 (省, 市, 区). 省是树的一级根节点, 市是省的子节点, 区是市的子节点. 如 : 广东省, 广州市, 天河区

1 . Area 实体设计采用自关联, 关联的子集 fetch 策略为懒加载.

 
  1. package name.ealen.entity;
  2. import com.fasterxml.jackson.annotation.JsonIgnore;
  3. import org.hibernate.annotations.GenericGenerator;
  4. import javax.persistence.*;
  5. import java.util.List;
  6. /**
  7. * Created by EalenXie on 2018/10/16 16:49.
  8. * 典型的 多层级 区域关系
  9. */
  10. @Entity
  11. @Table(name = "jpa_area")
  12. public class Area {
  13. /**
  14. * Id 使用 UUID 生成策略
  15. */
  16. @Id
  17. @GeneratedValue(generator = "UUID")
  18. @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
  19. private String id;
  20. /**
  21. * 区域名
  22. */
  23. private String name;
  24. /**
  25. * 一个区域信息下面很多子区域 (多级) 比如 : 广东省 (子) 区域 : 广州市 (孙)子区域 : 天河区
  26. */
  27. @ManyToOne(fetch = FetchType.LAZY)
  28. @JoinColumn(name = "parent_id")
  29. @JsonIgnore
  30. private Area parent;
  31. @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
  32. private List<Area> children;
  33. public String getId() {
  34. return id;
  35. }
  36. public void setId(String id) {
  37. this.id = id;
  38. }
  39. public String getName() {
  40. return name;
  41. }
  42. public void setName(String name) {
  43. this.name = name;
  44. }
  45. public Area getParent() {
  46. return parent;
  47. }
  48. public void setParent(Area parent) {
  49. this.parent = parent;
  50. }
  51. public List<Area> getChildren() {
  52. return children;
  53. }
  54. public void setChildren(List<Area> children) {
  55. this.children = children;
  56. }
  57. }

2 . 为 Area 写一个简单的 dao 进行数据库访问: AreaRepository

 
  1. package name.ealen.dao;
  2. import name.ealen.entity.Area;
  3. import org.springframework.data.jpa.repository.JpaRepository;
  4. /**
  5. * Created by EalenXie on 2018/10/16 16:56.
  6. */
  7. public interface AreaRepository extends JpaRepository<Area, String> {
  8. }

3. 现在来进行一波关键性的测试 : 首先我们插入数据测试 :

 
  1. @Autowired
  2. private AreaRepository areaRepository;
  3. /**
  4. * 新增区域测试
  5. */
  6. @Test
  7. public void addArea() {
  8. // 广东省 (顶级区域)
  9. Area guangdong = new Area();
  10. guangdong.setName("广东省");
  11. areaRepository.save(guangdong);
  12. // 广东省 下面的 广州市(二级区域)
  13. Area guangzhou = new Area();
  14. guangzhou.setName("广州市");
  15. guangzhou.setParent(guangdong);
  16. areaRepository.save(guangzhou);
  17. // 广州市 下面的 天河区(三级区域)
  18. Area tianhe = new Area();
  19. tianhe.setName("天河区");
  20. tianhe.setParent(guangzhou);
  21. areaRepository.save(tianhe);
  22. // 广东省 下面的 湛江市(二级区域)
  23. Area zhanjiang = new Area();
  24. zhanjiang.setName("湛江市");
  25. zhanjiang.setParent(guangdong);
  26. areaRepository.save(zhanjiang);
  27. // 湛江市 下面的 霞山区(三级区域)
  28. Area xiashan = new Area();
  29. xiashan.setName("霞山区");
  30. xiashan.setParent(zhanjiang);
  31. areaRepository.save(xiashan);
  32. }

4 . 进行查询, 并触发懒加载 :

 
  1. /**
  2. * 触发懒加载查询 典型的 N+1 现象
  3. */
  4. @Test
  5. @Transactional
  6. public void findAllArea() {
  7. List<Area> areas = areaRepository.findAll();
  8. System.out.println(JSONArray.toJSONString(areas.get(0)));
  9. }

此时, 我们可以在控制台中看到, 触发了懒加载, 导致了 N+1 的问题.

上面我们首先发出 1 条 SQL 查出了所有的 Area 对象, 然后为了取第一个中的关联对象发了 5 条 SQL.

解决的方法如下 :

1 . 首先在实体上面注解 @NamedEntityGraph, 指明 name 供查询方法使用, attributeNodes 指明被标注为懒加载的属性节点

如下 : Category 实体

 
  1. package name.ealen.entity;
  2. import com.fasterxml.jackson.annotation.JsonIgnore;
  3. import org.hibernate.annotations.GenericGenerator;
  4. import javax.persistence.*;
  5. import java.util.Set;
  6. /**
  7. * Created by EalenXie on 2018/10/16 16:13.
  8. * 典型的 多层级 分类
  9. * <p>
  10. * :@NamedEntityGraph : 注解在实体上 , 解决典型的 N+1 问题
  11. * name 表示实体图名, 与 repository 中的注解 @EntityGraph 的 value 属性相对应,
  12. * attributeNodes 表示被标注要懒加载的属性节点 比如此例中 : 要懒加载的子分类集合 children
  13. */
  14. @Entity
  15. @Table(name = "jpa_category")
  16. @NamedEntityGraph(name = "Category.Graph", attributeNodes = {@NamedAttributeNode("children")})
  17. public class Category {
  18. /**
  19. * Id 使用 UUID 生成策略
  20. */
  21. @Id
  22. @GeneratedValue(generator = "UUID")
  23. @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
  24. private String id;
  25. /**
  26. * 分类名
  27. */
  28. private String name;
  29. /**
  30. * 一个商品分类下面可能有多个商品子分类 (多级) 比如 分类 : 家用电器 (子) 分类 : 电脑 (孙)子分类 : 笔记本电脑
  31. */
  32. @ManyToOne(fetch = FetchType.LAZY)
  33. @JoinColumn(name = "parent_id")
  34. @JsonIgnore
  35. private Category parent; // 父分类
  36. @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
  37. private Set<Category> children; // 子分类集合
  38. public String getId() {
  39. return id;
  40. }
  41. public void setId(String id) {
  42. this.id = id;
  43. }
  44. public String getName() {
  45. return name;
  46. }
  47. public void setName(String name) {
  48. this.name = name;
  49. }
  50. public Category getParent() {
  51. return parent;
  52. }
  53. public void setParent(Category parent) {
  54. this.parent = parent;
  55. }
  56. public Set<Category> getChildren() {
  57. return children;
  58. }
  59. public void setChildren(Set<Category> children) {
  60. this.children = children;
  61. }
  62. }

2 . 在访问的 dao 的查询方法上面注解 @EntityGraph,value 属性值为 @NamedEntityGraph 的 name 属性值, 如 CategoryRepository :

 
  1. package name.ealen.dao;
  2. import name.ealen.entity.Category;
  3. import org.springframework.data.jpa.repository.EntityGraph;
  4. import org.springframework.data.jpa.repository.JpaRepository;
  5. import java.util.List;
  6. /**
  7. * Created by EalenXie on 2018/10/16 16:19.
  8. */
  9. public interface CategoryRepository extends JpaRepository<Category, String> {
  10. /**
  11. * 解决 懒加载 JPA 典型的 N + 1 问题
  12. */
  13. @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH)
  14. List<Category> findAll();
  15. }

3 . 进行测试 : 新增一些分类

 
  1. @Autowired
  2. private CategoryRepository categoryRepository;
  3. /**
  4. * 新增分类测试
  5. */
  6. @Test
  7. public void addCategory() {
  8. // 一个 家用电器分类(顶级分类)
  9. Category appliance = new Category();
  10. appliance.setName("家用电器");
  11. categoryRepository.save(appliance);
  12. // 家用电器 下面的 电脑分类(二级分类)
  13. Category computer = new Category();
  14. computer.setName("电脑");
  15. computer.setParent(appliance);
  16. categoryRepository.save(computer);
  17. // 电脑 下面的 笔记本电脑分类(三级分类)
  18. Category notebook = new Category();
  19. notebook.setName("笔记本电脑");
  20. notebook.setParent(computer);
  21. categoryRepository.save(notebook);
  22. // 家用电器 下面的 手机分类(二级分类)
  23. Category mobile = new Category();
  24. mobile.setName("手机");
  25. mobile.setParent(appliance);
  26. categoryRepository.save(mobile);
  27. // 手机 下面的 智能机 / 老人机(三级分类)
  28. Category smartPhone = new Category();
  29. smartPhone.setName("智能机");
  30. smartPhone.setParent(mobile);
  31. categoryRepository.save(smartPhone);
  32. Category oldPhone = new Category();
  33. oldPhone.setName("老人机");
  34. oldPhone.setParent(mobile);
  35. categoryRepository.save(oldPhone);
  36. }

进行查询 , 并触发懒加载 :

 
  1. /**
  2. * 查找分类测试 已经解决了经典的 N+1 问题
  3. */
  4. @Test
  5. @Transactional
  6. public void findCategory() {
  7. List<Category> categories = categoryRepository.findAll();
  8. for (Category category : categories) {
  9. System.out.println(JSONArray.toJSONString(category));
  10. }
  11. }

此时可以看到控制台里面只发了一条联表查询就得到了关联对象.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值