问题描述
因为设计树形结构的实体中用到了多对一,一对多的映射关系,在加载这个实体对象的时候,因为JPA的懒加载特效会导致触发N+1的问题,通常1的这方是通过1条SQL查找得到的1个对象或1个集合,由于关联的存在 ,又需要将这个对象(或集合)关联的集合取出,1这方的集合数量是N,则要发出N条SQL,于是本来的1条联表查询SQL可解决的问题变成了N+1条SQL。
例如以下场景,后台管理系统菜单往往都是树结构的,一般会存在多个菜单和子菜单,如下:
1.实体类
@Data
@Entity
@Table(name = "menu")
public class Menu {
@Id
private Integer id;
private String menuName;
private Integer parentId;
@OneToMany
@JoinColumn(name="parentId")
private List<Menu> childList;
}
2.数据访问层
public interface MenuRepository extends JpaRepository<Menu,Integer> {
List<Menu> findAllByParentIdIsNull();
}
插入基础数据,以下插入了2个根菜单和3个子菜单
@Autowired
MenuRepository menuRepository;
{
Menu menu = new Menu();
menu.setId(1);
menu.setMenuName("系统管理");
Menu menu2 = new Menu();
menu2.setId(2);
menu2.setMenuName("用户管理");
menu2.setParentId(1);
Menu menu3 = new Menu();
menu3.setId(3);
menu3.setMenuName("角色管理");
menu3.setParentId(1);
Menu menu4 = new Menu();
menu4.setId(4);
menu4.setMenuName("报表统计");
Menu menu5 = new Menu();
menu5.setId(5);
menu5.setMenuName("按月统计");
menu5.setParentId(4);
menuRepository.save(menu);
menuRepository.save(menu2);
menuRepository.save(menu3);
menuRepository.save(menu4);
menuRepository.save(menu5);
}
3.测试触发N+1
List<Menu> menuList = menuRepository.findAllByParentIdIsNull();
System.out.println("一级菜单数量="+menuList.size());
for (Menu menu : menuList) {
System.out.println("菜单名称="+menu.getMenuName()+"的子菜单数量="+menu.getChildList().size());
}
可以看到执行的sql一共打印了3条sql,第1条sql查询出所有的根菜单,第2和第3条则是根据根菜单的Id去查询对应的子菜单信息。
4.解决N+1的问题
在实体类加@NamedEntityGraph注解,并且通过@NamedAttributeNode注解关联上需要一起加载的模型类
@NamedEntityGraph(name = "menu.findAll", attributeNodes = {
@NamedAttributeNode(value = "childList")
})
public class Menu {}
在数据访问层通过@EntityGraph的value指定需要使用@NamedEntityGraph注解里配置的name名称
@EntityGraph(value = "menu.findAll", type = EntityGraph.EntityGraphType.FETCH)
List<Menu> findAllByParentIdIsNull();
然后再执行测试代码,可以看到下面只打印了一条sql,代表着N+1的问题消失了。
5.jackson序列化导致的N+1问题
标签4解决的N+1问题只是在遍历获取的元素的时候,当没有遍历直接返回数据给页面时候又会导致这个问题。
问题复现:
@RestController
public class TestController {
@Autowired
private MenuRepository menuRepository;
@GetMapping("/test")
public List<Menu> menuList(){
List<Menu> menuList = menuRepository.findAllByParentIdIsNull();
return menuList;
}
}
解决N+1问题
添加jackson-datatype-hibernate5包
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate5</artifactId>
<version>2.13.2</version>
</dependency>
重写 SpringMvc的 MappingJackson2HttpMessageConverter,将Hibernate5Module这个Module 注册到ObjectMapper。
@Configuration
public class WebMvcConfig {
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = converter.getObjectMapper();
Hibernate5Module hibernate5Module = new Hibernate5Module();
mapper.registerModule(hibernate5Module);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
return converter;
}
}
再次访问http://localhost:8013/test即可发现控制台只打印1条sql了。