栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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 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> findAllSimpleRecord();

    @Query(value = "select * from student_course", nativeQuery = true)
    List> findAllRelation();
}
  • 使用nativeQuery而非JPQL,因为JPQL还是依赖于@Entity层的
  • 返回数据使用List>通用容器承载而非POJO对象,如果是POJO对象,则需要自定义类型转换的Converter
  • 关联性关系的查询返回数据结构体中,key类型为原表的主键ID类型,value类型为关联表的主键ID类型
  • 上述得到的Map结构中的key的格式是下划线式的而非@Entity中属性的驼峰式,所以需要增加通用的下划线式转驼峰式的辅助方法,类似如下:
    private Map convertToCamelCaseKey(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 final  T 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);
                    }
                });
    }
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/672163.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号