JPA查询下@ManyToMany导致的N+1次query的问题

这里写自定义目录标题

概述

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);
                    }
                });
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

镜悬xhs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值