JPA 懒加载(循环引用,N+1,使用关联对象,No session问题)(二)
这次具体讲述一下,对于懒加载遇到(循环引用,N+1,使用关联对象,No session问题)的解决方案。
为了方便大家模拟操作,我会完整说一下
不想看过程的,直接看总结。
一 建表
创建School和User
School
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for school -- ---------------------------- DROP TABLE IF EXISTS `school`; CREATE TABLE `school` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of school -- ---------------------------- INSERT INTO `school` VALUES (1, 'h1'); INSERT INTO `school` VALUES (2, 'h2'); INSERT INTO `school` VALUES (3, 't3'); INSERT INTO `school` VALUES (4, 'h4'); SET FOREIGN_KEY_CHECKS = 1;
User
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `schoolId` int NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `FK3o0riaw95im7i0xlbrwujumpa`(`schoolId` ASC) USING BTREE, CONSTRAINT `FK3o0riaw95im7i0xlbrwujumpa` FOREIGN KEY (`schoolId`) REFERENCES `school` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES (1, 'u1', 1); INSERT INTO `user` VALUES (2, 'u2', 1); INSERT INTO `user` VALUES (3, 'u3', 2); INSERT INTO `user` VALUES (4, 'u4', NULL); SET FOREIGN_KEY_CHECKS = 1;
二 POM
简单说一下:
1 jackson-datatype-hibernate5 懒加载数据,转换json时,避免错误。
2 其他不赘述了。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-hibernate5</artifactId> <version>2.14.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies>
三 application.yml
spring: datasource: url: jdbc:mysql://localhost:3306/test1?serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: true
四 配置注入
SpringBoot方式
通过jackson-datatype-hibernate5配置,解决懒加载序列化的问题。
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import java.text.SimpleDateFormat; @Configuration public class WebMvcConfig { @Bean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper mapper = converter.getObjectMapper(); //JPA 懒加载 Hibernate5Module hibernate5Module = new Hibernate5Module(); mapper.registerModule(hibernate5Module); mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); return converter; } }
SpringMVC方式
application.xml
<!-- 消息转换器 --> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="defaultCharset" value="UTF-8" /> </bean> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper"> <bean class="com.kintech.common.MyObjectMapper" /> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>
MyObjectMapper
import java.text.SimpleDateFormat; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; public class MyObjectMapper extends ObjectMapper { private static final long serialVersionUID = -7171816038924552983L; public MyObjectMapper(){ SimpleDateFormat format = new MySimpleDateFormat("yyyy-MM-dd HH:mm:ss"); this.setDateFormat(format); this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); //反序列化时忽略多出的属性 this.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); //序列化时忽略映射空属性bean //json 懒加载对象 Hibernate5Module hibernate5Module = new Hibernate5Module(); hibernate5Module.disable(Hibernate5Module.Feature.USE_TRANSIENT_ANNOTATION); //防止@Transient注解,不序列化Json this.registerModule(hibernate5Module); } }
五 对象
School对象
@BatchSize(size=100) , @Fetch(FetchMode.SUBSELECT) 解决N+1问题。
关联表产生的sql变为:select * from XXX where id in (....)
import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.GenericGenerator; import javax.persistence.*; import java.util.List; @Entity @Table(name = "school", catalog = "test1") public class School implements java.io.Serializable { private Integer id; private String name; private List<User> users; @Id @GenericGenerator(name = "generator", strategy = "identity") @Column(name = "id") public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Column(name = "name") public String getName() { return name; } public void setName(String name) { this.name = name; } @JsonIgnoreProperties(value = { "school" }) @BatchSize(size=100) @OneToMany(fetch = FetchType.LAZY) @JoinColumn(name = "schoolId", referencedColumnName = "id", insertable = false, updatable = false) public List<User> getUsers() { return users; } public void setUsers(List<User> users) { this.users = users; } }
User对象
@JsonIgnoreProperties(value = { "users" }) 防止循环引用,指向School表中的users对象
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.hibernate.annotations.GenericGenerator; import javax.persistence.*; @Entity @Table(name = "user",catalog = "test1") public class User { private Integer id; private String name; private Integer schoolId; private School school; @Id @GenericGenerator(name = "generator", strategy = "identity") @Column(name = "id") public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Column(name = "name") public String getName() { return name; } public void setName(String name) { this.name = name; } @Column(name = "schoolId") public Integer getSchoolId() { return schoolId; } public void setSchoolId(Integer schoolId) { this.schoolId = schoolId; } @JsonIgnoreProperties(value = { "users" }) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="schoolId",referencedColumnName = "id",insertable = false,updatable = false) public School getSchool() { return school; } public void setSchool(School school) { this.school = school; } }
六 Dao
SchoolDao
import com.example.test_project.model.School; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface SchollDao extends JpaRepository<School,Integer> { }
UserDao
import com.example.test_project.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserDao extends JpaRepository<User,Integer> { }
Service中的查询方法需要添加,防止no session问题
@Transactional(readOnly = true)
七 Controller
package com.example.test_project.controller; import com.example.test_project.dao.SchoolDao; import com.example.test_project.dao.UserDao; import com.example.test_project.model.School; import com.example.test_project.model.User; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Optional; @RestController @RequestMapping("api/test") public class TestController { @Autowired SchoolDao schoolDao; @Autowired UserDao userDao; /** * School -- findAll users为null * @param reqVo * @return */ @RequestMapping(value = "t1", method = RequestMethod.POST) @ResponseBody public List<School> t1(@RequestBody String reqVo) { List<School> schollList=schoolDao.findAll(); return schollList; } /** * School -- findAll,通过遍历调用,users有值 * @param reqVo * @return */ @RequestMapping(value = "t2", method = RequestMethod.POST) @ResponseBody public List<School> t2(@RequestBody String reqVo) { List<School> schollList=schoolDao.findAll(); schollList.forEach(x->Optional.ofNullable(x.getUsers()).toString()); return schollList; } /** * School -- findOne users为null * @param reqVo * @return */ @RequestMapping(value = "t3", method = RequestMethod.POST) @ResponseBody public School t3(@RequestBody String reqVo) { School scholl=schoolDao.findById(1).get(); return scholl; } /** * School -- findOne,通过遍历调用,users有值 * @param reqVo * @return */ @RequestMapping(value = "t4", method = RequestMethod.POST) @ResponseBody public School t4(@RequestBody String reqVo) { School scholl=schoolDao.findById(1).get(); Optional.ofNullable(scholl.getUsers()).toString(); return scholl; } /** * User -- findAll 通过遍历 school有值 * @return */ @RequestMapping(value = "t5", method = RequestMethod.POST) @ResponseBody public List<User> t5() { List<User> list=userDao.findAll(); list.forEach(x-> Optional.ofNullable(x.getSchool()).toString()); return list; } /** * User -- findOne school为null * @return */ @RequestMapping(value = "t6", method = RequestMethod.POST) @ResponseBody public User t6() { User user=userDao.findById(1).get(); return user; } }
八 测试
1 api/test/t1
没有调用users,所以users为null
[ { "id": 1, "name": "h1", "users": null }, { "id": 2, "name": "h2", "users": null }, { "id": 3, "name": "t3", "users": null }, { "id": 4, "name": "h4", "users": null } ]
2 api/test/t2
调用了schollList.forEach(x->Optional.ofNullable(x.getUsers()).toString());
所以users有数据
[ { "id": 1, "name": "h1", "users": [ { "id": 1, "name": "u1", "schoolId": 1, "school": { "id": 1, "name": "h1" } }, { "id": 2, "name": "u2", "schoolId": 1, "school": { "id": 1, "name": "h1" } } ] }, { "id": 2, "name": "h2", "users": [ { "id": 3, "name": "u3", "schoolId": 2, "school": { "id": 2, "name": "h2" } } ] }, { "id": 3, "name": "t3", "users": [] }, { "id": 4, "name": "h4", "users": [] } ]
3 api/test/t3
没有调用users
{ "id": 1, "name": "h1", "users": null }
4 api/test/t4
调用了Users Optional.ofNullable(scholl.getUsers()).toString();
{ "id": 1, "name": "h1", "users": [ { "id": 1, "name": "u1", "schoolId": 1, "school": { "id": 1, "name": "h1" } }, { "id": 2, "name": "u2", "schoolId": 1, "school": { "id": 1, "name": "h1" } } ] }
5 api/test/t5
调用了school list.forEach(x-> Optional.ofNullable(x.getSchool()).toString());
[ { "id": 1, "name": "u1", "schoolId": 1, "school": { "id": 1, "name": "h1" } }, { "id": 2, "name": "u2", "schoolId": 1, "school": { "id": 1, "name": "h1" } }, { "id": 3, "name": "u3", "schoolId": 2, "school": { "id": 2, "name": "h2" } }, { "id": 4, "name": "u4", "schoolId": null, "school": null } ]
6 api/test/t6
没有调用School
{ "id": 1, "name": "u1", "schoolId": 1, "school": null }
总结
可以看到,满足了Lazy (循环引用,N+1,使用关联对象)的功能。
1 使用jackson-datatype-hibernate5 配置 WebMvcConfig 解决懒加载的序列化问题。
2 使用@BatchSize(size=100) 解决N+1问题(支持JPA和EntityManager)
@Fetch(FetchMode.SUBSELECT) 也可以,但是无法支持EntityManager的查询
3 使用@JsonIgnoreProperties(value = { "school" }) 避免循环引用,school指向关联对象中的school
一般设置在主表就可以。此处演示,我也设置了@JsonIgnoreProperties(value = { "users" })
4 使用Optional.ofNullable(xxx).toString(); 是为了避免 null.toString();
5 不要使用Debug断点,不然永远会加载关联对象
6 Service中的查询方法需要添加@Transactional(readOnly = true),防止no session问题