- 概述
- 假设模型
- 添加数据
- 解决方案
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 courses;
}
添加数据
public void setUp() {
List 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{ @Query(value = "select * from student", nativeQuery = true) List
- 使用nativeQuery而非JPQL,因为JPQL还是依赖于@Entity层的
- 返回数据使用List
>通用容器承载而非POJO对象,如果是POJO对象,则需要自定义类型转换的Converter - 关联性关系的查询返回数据结构体中,key类型为原表的主键ID类型,value类型为关联表的主键ID类型
- 上述得到的Map
结构中的key的格式是下划线式的而非@Entity中属性的驼峰式,所以需要增加通用的下划线式转驼峰式的辅助方法,类似如下:
private MapconvertToCamelCaseKey(Map record) { Map 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
转换成对应的@Entity实体,则还需要通用的辅助转换方法,类似如下:
public static finalT convert(Object fromValue, Class 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);
}
});
}



