这次具体讲述一下,对于懒加载遇到(循环引用,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问题