概述
Spring Data Jpa
底层默认使用Hibernate
作为ORM框架, Hibernate
有一个非常典型的N+1
查询的问题。这个问题在有些场景下会非常影响系统稳定性。
下面是实际工作中遇到的问题,主要背景如下:
- 系统使用
Hibernate
对配置模型进行抽象建模,配置模型存储在MySQL数据库中; - 系统每隔一段时间,会全量加载所有配置信息,刷新实例本地的内存配置,由于使用了
@ManyToMany
、@ManyToOne
、@OneToMany
等注解,系统在加载配置过程中,会产生大量的MySQL查询请求,单实例查询次数可高达几百条,整个集群在很短时间内会对MySQL数据发起几万次查询; - 系统已经稳定运行很长一段时间,
@Entity
层已经相对稳定,随意调整该层会影响到配置的更新逻辑。
由于上述背景,需要想办法在不影响现有@Entity
模型的整体基础上,设计一种新的配置数据加载逻辑,解决N+1
的问题。
假设模型
- 课程实体
@Getter
@Setter
@Entity
public class Course {
@Id
private Long id;
private String name;
}
- 学生实体
@Getter
@Setter
@Entity
public class Student {
@Id
private Long id;
private String name;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses;
}
添加数据
public void setUp() {
List<Course> courseList = new ArrayList<>();
Course course = new Course();
course.setId(1L);
course.setName("语文");
courseRepository.save(course);
courseList.add(course);
course = new Course();
course.setId(2L);
course.setName("数学");
courseRepository.save(course);
courseList.add(course);
Student student = new Student();
student.setId(1L);
student.setName("jingxuan");
student.setCourses(courseList);
studentRepository.save(student);
}
解决方案
- 定义
StudentRepository
对象:
public interface StudentRepository extends CrudRepository<Student, Long> {
@Query(value = "select * from student", nativeQuery = true)
List<Map<String, ?>> findAllSimpleRecord();
@Query(value = "select * from student_course", nativeQuery = true)
List<Map<Long, Long>> findAllRelation();
}
- 使用
nativeQuery
而非JPQL
,因为JPQL
还是依赖于@Entity
层的- 返回数据使用
List<Map<String, ?>>
通用容器承载而非POJO
对象,如果是POJO
对象,则需要自定义类型转换的Converter
- 关联性关系的查询返回数据结构体中,
key
类型为原表的主键ID类型,value
类型为关联表的主键ID类型
- 上述得到的
Map<String, ?>
结构中的key
的格式是下划线式的而非@Entity
中属性的驼峰式,所以需要增加通用的下划线式转驼峰式的辅助方法,类似如下:
private Map<String, ?> convertToCamelCaseKey(Map<String, ?> record) {
Map<String, Object> result = new HashMap<>(record.size());
for (String key : record.keySet()) {
result.put(CamelCaseUtils.fromSnakeCase(key), record.get(key));
}
return result;
}
public static String fromSnakeCase(String snake) {
StringBuilder sb = new StringBuilder();
int index = 0;
while (index < snake.length() - 1) {
char c = snake.charAt(index);
index = index + 1;
if (c != '_') {
sb.append(c);
continue;
}
while (index < snake.length()) {
c = snake.charAt(index);
index = index + 1;
if (c != '_') {
sb.append(Character.toUpperCase(c));
break;
}
}
}
if (index < snake.length() && snake.charAt(index) != '_') {
sb.append(snake.charAt(index));
}
return sb.toString();
}
- 需要将查询得到的
Map<String, ?>
转换成对应的@Entity
实体,则还需要通用的辅助转换方法,类似如下:
public static final <T> T convert(Object fromValue, Class<T> toValueType) {
return OBJECT_MAPPER.convertValue(fromValue, toValueType);
}
- 组合起来的代码类似如下:
public void load() {
this.courseMap = courseRepository
.findAllSimpleRecord()
.stream()
.map(record -> {
return ModelUtils.convert(convertToCamelCaseKey(record), Course.class);
})
.collect(Collectors.toMap(Course::getId, Function.identity()));
this.studentMap = studentRepository
.findAllSimpleRecord()
.stream()
.map(record -> {
Student student = ModelUtils.convert(convertToCamelCaseKey(record), Student.class);
student.setCourses = new ArrayList();
})
.collect(Collectors.toMap(Student::getId, Function.identity()));
// 构造因子间的依赖关系
studentRepository.findAllRelation()
.forEach(record -> {
Student student = studentMap.get(record.get("student_id"));
Course course = courseMap.get(record.get("course_id"));
// 更新依赖关系
if (student != null && course != null) {
student.getCourses().add(course);
}
});
}