解决Spring Data JPA Hibernate的N+1问题的最佳方法

news/2024/9/18 12:47:25/文章来源:https://www.cnblogs.com/xkzhangsanx/p/18412961

  最佳方法:定制@NamedEntityGraph、定制查询和定制VO,可以做到按照需要最佳查询,需要注意的地方:定制VO的字段一定要等于或小于实际查询的字段,才不会复制的时候触发N+1查询。

1 问题复现

1.1 项目结构

 1.2 entity

package com.xkzhangsan.jpa.entity;import lombok.Getter;
import lombok.Setter;import javax.persistence.*;@Entity
@Getter
@Setter
@Table(name = "user")
public class User {@Id@GeneratedValue(strategy = GenerationType.AUTO)private Long id;private String name;@OneToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)@JoinColumn(name = "user_detail_id")private UserDetail userDetail;
}
package com.xkzhangsan.jpa.entity;import lombok.Getter;
import lombok.Setter;import javax.persistence.*;@Entity
@Getter
@Setter
@Table(name = "user_detail")
public class UserDetail {@Id@GeneratedValue(strategy = GenerationType.AUTO)private Long id;private String address;
}

1.3 repository

package com.xkzhangsan.jpa.repository;import com.xkzhangsan.jpa.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;public interface UserRepository extends JpaRepository<User, Integer> {
}

1.4 service

package com.xkzhangsan.jpa.service;import com.xkzhangsan.jpa.entity.User;
import com.xkzhangsan.jpa.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class UserService {@Autowiredprivate UserRepository userRepository;public List<User> findAll(){return userRepository.findAll();}}

1.5 controller

package com.xkzhangsan.jpa.controller;import com.xkzhangsan.jpa.entity.User;
import com.xkzhangsan.jpa.service.UserService;
import com.xkzhangsan.jpa.vo.UserDetailVO;
import com.xkzhangsan.jpa.vo.UserVO;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;
import java.util.stream.Collectors;@RestController
public class UserController {@Autowiredprivate UserService userService;@RequestMapping(value = "/")public List<UserVO> getPersons() {ModelMapper modelMapper = new ModelMapper();List<User> userList = userService.findAll();if (!CollectionUtils.isEmpty(userList)) {return userList.stream().map(user -> {UserVO userVO = modelMapper.map(user, UserVO.class);UserDetailVO userDetailVO = modelMapper.map(user.getUserDetail(), UserDetailVO.class);userVO.setUserDetailVO(userDetailVO);return userVO;}).collect(Collectors.toList());}return null;}
}

1.6 测试

 

 

查询1次,实际查询了4次,这里的N指的是集合的数量3,所以 N+1,就是3+1=4

1.7 问题原因

User关联对象懒加载导致

 2 最佳解决方法

  定制@NamedEntityGraph、定制查询和定制VO,可以做到按照需要最佳查询。

2.1 定制@NamedEntityGraph

package com.xkzhangsan.jpa.entity;import lombok.Getter;
import lombok.Setter;import javax.persistence.*;@Entity
@Getter
@Setter
@Table(name = "user")
@NamedEntityGraph(name = "user.userDetail", attributeNodes = {@NamedAttributeNode(value = "userDetail")
})
public class User {@Id@GeneratedValue(strategy = GenerationType.AUTO)private Long id;private String name;@OneToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)@JoinColumn(name = "user_detail_id")private UserDetail userDetail;
}

从代码种可以看出增加注解,使用user.userDetail会联表查询UserDetail

@NamedEntityGraph(name = "user.userDetail", attributeNodes = {@NamedAttributeNode(value = "userDetail")
})

2.1.2 定制查询

package com.xkzhangsan.jpa.repository;import com.xkzhangsan.jpa.entity.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;import java.util.List;public interface UserRepository extends JpaRepository<User, Integer> {@Override@EntityGraph(value = "user.userDetail", type = EntityGraph.EntityGraphType.FETCH)List<User> findAll();
}

从代码种可以看出,重写了findAll,使用了user.userDetail

2.3 定制VO(可选)

根据需要返回的字段定义一个VO,如果没有需要则不需要定制,比如当前实例种就不需要。

测试结果如下,只查询了一次,但联表查询了,left outer join user_detail

select user0_.id as id1_0_0_, userdetail1_.id as id1_1_1_, user0_.name as name2_0_0_, user0_.user_detail_id as user_det3_0_0_, userdetail1_.address as address2_1_1_ from user user0_ left outer join user_detail userdetail1_ on user0_.user_detail_id=userdetail1_.id

2.3.1 需要定制VO的实例

比如,只想返回用户id和name,可以定制一个UserSimpleVO,如下:

package com.xkzhangsan.jpa.vo;import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class UserSimpleVO {private Long id;private String name;
}

因为不包括UserDetailVO userDetailVO,所以复制属性的时候不会触发更多的查询。

需要注意的地方:定制VO的字段一定要等于或小于实际查询的字段,才不会复制的时候触发N+1查询。

还有一种方式不用定制VO,通过设置ModelMapper跳过不需要的字段,但这样有2个问题

(1)需要设置ModelMapper,比较麻烦

(2)既然不需要一些字段,定制VO是最有效的方法,这样符合迪米特法则,比如不能展示的敏感字段,如果查询或处理过程中没有处理好,可能导致误返回,最好定制VO,在VO删除这个字段。

 

3 使用EAGER加载方式

既然是因为LAZY导致的,改成EAGER是否可以解决问题?经过验证是不可行的,仍然会查询多次,比如:

package com.xkzhangsan.jpa.entity;import lombok.Getter;
import lombok.Setter;import javax.persistence.*;@Entity
@Getter
@Setter
@Table(name = "user")
public class User {@Id@GeneratedValue(strategy = GenerationType.AUTO)private Long id;private String name;@OneToOne(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)@JoinColumn(name = "user_detail_id")private UserDetail userDetail;
}

 发现仍然会执行多个sql

Hibernate: select user0_.id as id1_0_, user0_.name as name2_0_, user0_.user_detail_id as user_det3_0_ from user user0_
Hibernate: select userdetail0_.id as id1_1_0_, userdetail0_.address as address2_1_0_ from user_detail userdetail0_ where userdetail0_.id=?
Hibernate: select userdetail0_.id as id1_1_0_, userdetail0_.address as address2_1_0_ from user_detail userdetail0_ where userdetail0_.id=?
Hibernate: select userdetail0_.id as id1_1_0_, userdetail0_.address as address2_1_0_ from user_detail userdetail0_ where userdetail0_.id=?

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/796839.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全网最适合入门的面向对象编程教程:48 Python函数方法与接口-位置参数、默认参数、可变参数和关键字参数

在 Python 中,函数可以接受多种不同类型的参数,包括位置参数、默认参数、可变参数和关键字参数等,理解这些参数类型对于编写灵活且可维护的代码非常重要。全网最适合入门的面向对象编程教程:48 Python 函数方法与接口-位置参数、默认参数、可变参数和关键字参数摘要: 在 P…

软工作业-个人介绍

一、自我介绍 我是广东工业大学大三的学生,我的爱好是打游戏 二、快速阅读软件工程教材之后的问题 软件工程中的需求分析如何做到全面而准确? 软件设计的原则有哪些,如何在实际项目中灵活运用? 软件测试的方法有哪些,如何确保测试的有效性? 项目管理在软件工程中的重要性…

浙江理工大学24软件技术基础第一次作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/zjlg/rjjc/这个作业的目标 介绍自己并发表对课程学习的期望姓名-学号 李想-2021330301036浙江理工大学24软件技术基础第一次作业 一、自我介绍 (一)、基本信息 大家好!我是22级自动化2班的李想,来自广东。我的爱好是动…

海贼女帝3D打印模型免费分享

链接:https://pan.quark.cn/s/fbe6cb7fd476

碧蓝航线-山城-猫娘3D打印模型免费分享

下载链接:https://pan.quark.cn/s/4c64df8eb54e

地牢里发生了什么3D模型免费分享

下载链接:https://pan.quark.cn/s/0c01c6fd4e90

斩赤红之瞳-艾斯德斯3D打印模型免费分享

链接:https://pan.quark.cn/s/c26ae6a17b69

18号3D打印模型分享

下载地址:https://pan.quark.cn/s/5c735ceee560

Redis 入门 - 收官

《Redis入门》系列完成,涵盖Redis基本信息、安装、可视化工具、基础数据类型、C#/.NETCore客户端库及封装。分享学习经验,帮助初学者少走弯路,形成系统性概念。后续将分享《Redis进阶》。《Redis 入门》系列文章总算完成了,希望这个系列文章可以想入门或刚入门的同学提供帮…

计算机桌面有去不掉的窗口边框 ,电脑屏幕有残留刷新去不掉怎么办

先允许任务管理器 ,结束explorer进程树 还可以参考 https://blog.csdn.net/weixin_42614269/article/details/118206476再重新运行explorer任务 桌面恢复正常。

对HTML基础详细拓展

1.什么是HMTL? HTML 是用来描述网页的一种语言。 HTML 指的是超文本标记语言: HyperText Markup Language HTML 不是一种编程语言,而是一种标记语言 标记语言是一套标记标签 (markup tag) HTML 使用标记标签来描述网页 HTML 文档包含了HTML 标签及文本内容 HTML文档也叫做 we…

第八篇:权限管理体系

Linux12位权限管理体系权限管理概述 Linux通过rwx3种权限控制系统与保护系统,组成9位权限 Linux权限体系中还有3位特殊权限,组合起来就是12位权限体系rwx三种权限权限 含义r read 是否可读w write 是否可写x execute 是否可执行(一般是命令、脚本)用户三种关系 Linux下面…