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

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

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

如果对该例子比较感兴趣或者觉得言语表达比较啰嗦,可查看完整的demo地址 : https://github.com/EalenXie/springboot-jpa-N-plus-One

场景如下 :

  我设计了一个典型的二叉树结构实体叫做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<Area> 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<Area> getChildren() {
return children;
} public void setChildren(List<Area> 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<Area, String> {
}

  

  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<Area> 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.
* 典型的 多层级 分类
* <p>
* :@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<Category> 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<Category> getChildren() {
return children;
} public void setChildren(Set<Category> 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<Category, String> {
/**
* 解决 懒加载 JPA 典型的 N + 1 问题
*/
@EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH)
List<Category> 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<Category> categories = categoryRepository.findAll();
for (Category category : categories) {
System.out.println(JSONArray.toJSONString(category));
}
}

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

解决JPA懒加载典型的N+1问题-注解@NamedEntityGraph的更多相关文章

  1. SpringBoot JPA懒加载异常 - com.fasterxml.jackson.databind.JsonMappingException: could not initialize proxy

    问题与分析 某日忽然发现在用postman测试数据时报错如下: com.fasterxml.jackson.databind.JsonMappingException: could not initi ...

  2. Spring Boot JPA 懒加载

    最近在使用spring jpa 的过程中经常遇到懒加载的错误:"` org.hibernate.LazyInitializationException: could not initiali ...

  3. jpa懒加载异常

    1.项目背景概述 事情是这样子的,使用了spring data jpa的项目jeesite jeesite的实体中使用了懒加载模式. 并且一个实体类中还不止一个属性设置了懒加载模式. 项目本身已经存在 ...

  4. 在web.xml中添加配置解决hibernate 懒加载异常

    在web.xml添加如下,注意:在配置在struts2的拦截器之前,只能解决请求时出现的懒加载异常:如果没有请求,还需要lazy属性的添加(比如过滤器) <!-- 配置Spring的用于解决懒加 ...

  5. ionic3包还原使用yarn命令执行步骤(解决ionic3懒加载报找不到 module的错误)

    使用cnpm 还原ionic3.6的依赖包的时候 可以正常还原,但是使用懒加载就会报找不到 module 的错误.最简单的解决办法是删除node_modules 挂个vpn 重新执行npm insta ...

  6. Spring Boot JPA Entity Jackson序列化触发懒加载的解决方案

    Spring Jpa这项技术在Spring 开发中经常用到. 今天在做项目用到了Entity的关联懒加载,但是在返回Json的时候,不管关联数据有没有被加载,都会触发数据序列化,而如果关联关系没有被加 ...

  7. dubbo序列化hibernate.LazyInitializationException could not initialize proxy - no Session懒加载异常的解决

    dubbo序列化,hibernate.LazyInitializationException could not initialize proxy - no Session懒加载异常的解决 转载声明: ...

  8. 懒加载session 无法打开 no session or session was closed 解决办法(完美解决)

           首先说明一下,hibernate的延迟加载特性(lazy).所谓的延迟加载就是当真正需要查询数据时才执行数据加载操作.因为hibernate当中支持实体对象,外键会与实体对象关联起来.如 ...

  9. Rest风格中关于JPA使用懒加载的坑

    公司最近使用的ORM框架是JPA实现产品使用的是hibernate,曾经看过一篇博客上面说的是如果团队里面没有一个精通hibernate的人,那么最好不要使用它,我现在是深刻的体会到了.但是使用什么框 ...

随机推荐

  1. centos中mysql的安装

    一:前沿 过完年了,花了不少钱啊!本来还打算买电脑的了,结果这个事情还是的延期啊!苍天啊!刚刚也看了下,一台苹果也大概是1w左右!买吧!boy!别犹豫了吧!好吧现在来说说我自己的工作吧!现在过完年到公 ...

  2. 「模板」 FHQ_Treap 区间翻转

    「模板」 FHQ_Treap 区间翻转 没有旋转的 Treap 实现区间操作的功能,很好理解,也很好写,只是速度不算太快. 对于要翻转的区间,把整棵 Treap(存有区间 \([1,n]\) 的信息) ...

  3. 阿里云maven仓库地址,速度提升100倍

    参照:https://www.cnblogs.com/xxt19970908/p/6685777.html maven仓库用过的人都知道,国内有多么的悲催.还好有比较好用的镜像可以使用,尽快记录下来. ...

  4. Html5学习2(Html表格、Html列表、Html5新元素、Canvas (坐标、路径、画圆、文本、渐变、图像))

    Html表格 1.表格中的表头:<th></th>.其中表头部分字体加粗,颜色深绿色 <h4>水平标题:</h4> <table border=& ...

  5. Spring理论基础-面向切面编程

    AOP是Aspect-Oriented Programming的缩写,中文翻译是面向切面编程.作为Spring的特征之一,是要好好学习的. 首先面向切面编程这个名称很容易让人想起面向对象编程(OOP) ...

  6. 【洛谷 P1129】 [ZJOI2007]矩阵游戏 (二分图匹配)

    题目链接 看到题目肯定首先会想到搜索. 然鹅数据范围\(n<=200\)这么大(其实也不算太大),肯定是不行的. 如果\((i,j)\)是\(1\),从\(i\)向\(j\)连一条边,表示第\( ...

  7. python3中字典的遍历和合并

    #字典的遍历方式 dic={"a":1,"b":2,"c":3} for k in dic: print (k,dic[k]) for k, ...

  8. 如何彻底关闭退出vmware虚拟机

    如何彻底关闭退出vmware虚拟机 每次使用虚拟机之后退出时,它都会在系统托盘区留下一个虚拟机图标,该如何彻底关闭退出vmware虚拟机呢? 首先我们需要运行一下虚拟机程序 1:我们如果要对虚拟机进行 ...

  9. httpd -v command not found

    使用 find / -name "apachectl"查找文件目录下执行 ./apachectl -v

  10. webpack自动生成项目的html

    1 自动生成多个html页面 设置webpack.config.js中的plugins属性,多次调用plugin插件(new htmlWebpackPlugin()),同时设置对应数量的.js入口文件 ...