因为在设计一个树形结构的实体中用到了多对一, 一对多的映射关系, 在加载其关联对象的时候, 为了性能考虑, 很自然的想到了懒加载.
也由此遇到了 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 策略为懒加载.
- package name.ealen.entity;
- import com.fasterxml.jackson.annotation.JsonIgnore;
- import org.hibernate.annotations.GenericGenerator;
- import javax.persistence.*;
- import java.util.List;
- /**
- * Created by EalenXie on 2018/10/16 16:49.
- * 典型的 多层级 区域关系
- */
- @Entity
- @Table(name = "jpa_area")
- public class Area {
- /**
- * Id 使用 UUID 生成策略
- */
- @Id
- @GeneratedValue(generator = "UUID")
- @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
- private String id;
- /**
- * 区域名
- */
- private String name;
- /**
- * 一个区域信息下面很多子区域 (多级) 比如 : 广东省 (子) 区域 : 广州市 (孙)子区域 : 天河区
- */
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "parent_id")
- @JsonIgnore
- private Area parent;
- @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
- private List children;
- public String getId() {
- return id;
- }
- public void setId(String id) {
- this.id = id;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public Area getParent() {
- return parent;
- }
- public void setParent(Area parent) {
- this.parent = parent;
- }
- public List getChildren() {
- return children;
- }
- public void setChildren(List children) {
- this.children = children;
- }
- }
2 . 为 Area 写一个简单的 dao 进行数据库访问: AreaRepository
- package name.ealen.dao;
- import name.ealen.entity.Area;
- import org.springframework.data.jpa.repository.JpaRepository;
- /**
- * Created by EalenXie on 2018/10/16 16:56.
- */
- public interface AreaRepository extends JpaRepository {
- }
3. 现在来进行一波关键性的测试 : 首先我们插入数据测试 :
- @Autowired
- private AreaRepository areaRepository;
- /**
- * 新增区域测试
- */
- @Test
- public void addArea() {
- // 广东省 (顶级区域)
- Area guangdong = new Area();
- guangdong.setName("广东省");
- areaRepository.save(guangdong);
- // 广东省 下面的 广州市(二级区域)
- Area guangzhou = new Area();
- guangzhou.setName("广州市");
- guangzhou.setParent(guangdong);
- areaRepository.save(guangzhou);
- // 广州市 下面的 天河区(三级区域)
- Area tianhe = new Area();
- tianhe.setName("天河区");
- tianhe.setParent(guangzhou);
- areaRepository.save(tianhe);
- // 广东省 下面的 湛江市(二级区域)
- Area zhanjiang = new Area();
- zhanjiang.setName("湛江市");
- zhanjiang.setParent(guangdong);
- areaRepository.save(zhanjiang);
- // 湛江市 下面的 霞山区(三级区域)
- Area xiashan = new Area();
- xiashan.setName("霞山区");
- xiashan.setParent(zhanjiang);
- areaRepository.save(xiashan);
- }
4 . 进行查询, 并触发懒加载 :
- /**
- * 触发懒加载查询 典型的 N+1 现象
- */
- @Test
- @Transactional
- public void findAllArea() {
- List areas = areaRepository.findAll();
- System.out.println(JSONArray.toJSONString(areas.get(0)));
- }
此时, 我们可以在控制台中看到, 触发了懒加载, 导致了 N+1 的问题.
上面我们首先发出 1 条 SQL 查出了所有的 Area 对象, 然后为了取第一个中的关联对象发了 5 条 SQL.
解决的方法如下 :
1 . 首先在实体上面注解 @NamedEntityGraph, 指明 name 供查询方法使用, attributeNodes 指明被标注为懒加载的属性节点
如下 : Category 实体
- package name.ealen.entity;
- import com.fasterxml.jackson.annotation.JsonIgnore;
- import org.hibernate.annotations.GenericGenerator;
- import javax.persistence.*;
- import java.util.Set;
- /**
- * Created by EalenXie on 2018/10/16 16:13.
- * 典型的 多层级 分类
- *
- * :@NamedEntityGraph : 注解在实体上 , 解决典型的 N+1 问题
- * name 表示实体图名, 与 repository 中的注解 @EntityGraph 的 value 属性相对应,
- * attributeNodes 表示被标注要懒加载的属性节点 比如此例中 : 要懒加载的子分类集合 children
- */
- @Entity
- @Table(name = "jpa_category")
- @NamedEntityGraph(name = "Category.Graph", attributeNodes = {@NamedAttributeNode("children")})
- public class Category {
- /**
- * Id 使用 UUID 生成策略
- */
- @Id
- @GeneratedValue(generator = "UUID")
- @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
- private String id;
- /**
- * 分类名
- */
- private String name;
- /**
- * 一个商品分类下面可能有多个商品子分类 (多级) 比如 分类 : 家用电器 (子) 分类 : 电脑 (孙)子分类 : 笔记本电脑
- */
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "parent_id")
- @JsonIgnore
- private Category parent; // 父分类
- @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
- private Set children; // 子分类集合
- public String getId() {
- return id;
- }
- public void setId(String id) {
- this.id = id;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public Category getParent() {
- return parent;
- }
- public void setParent(Category parent) {
- this.parent = parent;
- }
- public Set getChildren() {
- return children;
- }
- public void setChildren(Set children) {
- this.children = children;
- }
- }
2 . 在访问的 dao 的查询方法上面注解 @EntityGraph,value 属性值为 @NamedEntityGraph 的 name 属性值, 如 CategoryRepository :
- package name.ealen.dao;
- import name.ealen.entity.Category;
- import org.springframework.data.jpa.repository.EntityGraph;
- import org.springframework.data.jpa.repository.JpaRepository;
- import java.util.List;
- /**
- * Created by EalenXie on 2018/10/16 16:19.
- */
- public interface CategoryRepository extends JpaRepository {
- /**
- * 解决 懒加载 JPA 典型的 N + 1 问题
- */
- @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH)
- List findAll();
- }
3 . 进行测试 : 新增一些分类
- @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);
- }
进行查询 , 并触发懒加载 :
- /**
- * 查找分类测试 已经解决了经典的 N+1 问题
- */
- @Test
- @Transactional
- public void findCategory() {
- List categories = categoryRepository.findAll();
- for (Category category : categories) {
- System.out.println(JSONArray.toJSONString(category));
- }
- }
此时可以看到控制台里面只发了一条联表查询就得到了关联对象.