• JPA 中使用 @OneToMany 、@ManyToOne 等关系映射注解


    JPA 做 ORM(Object Relational Mapping,对象关系映射)时,为了开发效率,通常会在实体类上用 hibernate 的关系关联注解。 包括:@OneToOne@OneToMany@ManyToOne@ManyToMany@JoinTable、以及 @JoinColumn 以及 OrderBy

    JPA 中 @JoinColumn 与 关联注解之间用法

    @JoinColumn 定义多个字段之间的关联关系,配合@OneToOne@ManyToOne 以及 @OneToMany一起使用

    @Repeatable(JoinColumns.class)
    @Target({ElementType.METHOD, ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface JoinColumn {
    	// 关联字段(子)
        String name() default "";
    	// 关键实体字段(父)),非必填
        String referencedColumnName() default "";
    
        boolean unique() default false;
    
        boolean nullable() default true;
        // 是否更新 插入操作
        boolean insertable() default true;
    
        boolean updatable() default true;
    
        String columnDefinition() default "";
    
        String table() default "";
    
        ForeignKey foreignKey() default @ForeignKey(ConstraintMode.PROVIDER_DEFAULT);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    @OneToMany 注解为例,源代码如下

    @Target({ElementType.METHOD, ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface OneToMany {
        // 关联对象实体类
        Class targetEntity() default void.class;
    	
        // 级联类型,
        // ALL, 级联所有操作
        // PERSIST,级联持久化
        // MERGE, 级联更新合并
        // REMOVE,级联删除
        // REFRESH,  刷新后更新
        //DETACH;  级联脱管操作
        CascadeType[] cascade() default {};
    	
        //  获取方式
        // LAZY 懒加载,需要时加载;
        // EARLY 提前加载
        FetchType fetch() default FetchType.LAZY;
    	// 被映射实体类名,当关联实体类上注有 ManyToOne 时,该参数必填;
        String mappedBy() default "";
        boolean orphanRemoval() default false;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    @OneToMany 使用时可以结合 @JoinColumn 一起使用,既可以单向,也可以双向关联;单向指的是只配置一方,双向指两方都配置,实例如下:

    假设小组(GroupClass)与学生(Student)关系,一个组可以容纳多个学生,一对多 ;多个学生组件成一个组(多对一);

    外键关联,一般子实体类这边维护,因此由Student 来关联 :

    @Entity
    @Table(name = "student")
    public class Student {
        
        // 主键自动生成策略
        @Id
    
        private Integer id;
        
        private String name;
        
        @Column(name = "student_no",unique = true)
        private String studentNo;
        
        @Column(name = "group_class_id")
        private Integer groupClassId;
    	
     	// 关联关系多对一,级联关系,可更新,持久化, 获取方式懒加载;
        @ManyToOne(targetEntity = GroupClass.class,
                cascade = {CascadeType.MERGE,CascadeType.PERSIST,CascadeType.REFRESH},
                fetch = FetchType.LAZY)
          // 以 group_id 为外键进行关联,referencedColumnName 默认为被关联实体类主键,可以忽略;
        @JoinColumn(name="group_class_id",referencedColumnName = "id")
        private GroupClass groupClass;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    而双向关联的 GroupClass 实体类(父类),定义如下:

    @Data
    @Entity
    @Table(name = "group_class")
    public class GroupClass {
        @Id
        private Integer id;
    
        private String name;
    
        @Column(name="group_no",unique = true)
        private String groupNo;
    
    	// 由于外键关联由 Student 维护,这里不需要在做处理,只需要引用 
        // @OneToMany中的 mappedBy 参数即可;
        @OneToMany(mappedBy = "groupClass")
        private List<Student> students = new ArrayList<>();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    其它关联注解用法类同:

    • @ManyToMany :多对多;
    • @OneToOne :一对一;

    @OrderBy 字段排序

    @OrderBy 顾名思义目的是为根据某个字段进行排序,注解一般与 @OneToMany@ManyToMany 同时使用,

    @OneToMany(mappedBy = "groupClass")
    // 根据名字对 学生对象逆向排序 
    @OrderBy("name DESC")
    private List<Student> students = new ArrayList<>();
    
    • 1
    • 2
    • 3
    • 4

    使用实例如下,首先插入1条 GroupClass 和 两条Student 记录,并将两个 Student 关联至 GroupClass

      // 持久化 GroupClass
      GroupClass group = new GroupClass();
      group.setGroupNo("Ano");
      group.setName("名字");
      group.setId(2L);
      //持久化 Student;
      Student student = new Student();
      student.setStudentNo("12");
      student.setId(1);
      student.setName("name1");
      student.setGroupClass(group);
      Student student2 = new Student();
      student2.setStudentNo("122");
      student2.setId(2);
      student2.setGroupClass(group);
      student2.setName("name12");
      // save 保存
      List<Student> students = new ArrayList<>();
      students.add(student);
      students.add(student2);
      group.setStudents(students);
      groupClassRepository.save(group);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    根据Id 查询 GroupClass 对象,在输出打印对象的toString 方法钱,需要重新写一下 对象的 toString 函数,防止循环嵌套导致的堆栈溢出问题

       //Student.class 
        @Override
        public String toString() {
            return "Student [id=" + id + ", name=" +name + ", studentNo=" +studentNo + ", groupClassId=" + groupClassId
                    + "]";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在上面例子中, GroupClass 对象中 toString 函数引用到 Student,而 StudenttoString 同时引用到 GroupClass,使用时会触发嵌套循环,造成栈溢出问题;

    image-20221115090622405

    输出 groupClass.toString后,就会发现 groupClass 关联的 students 属性是按照 @OrderBy 注解输出,如下:

     Optional<GroupClass> groupClassOptional = groupClassRepository.findById(2L);
     if(groupClassOptional.isPresent()) {
         GroupClass groupClass = groupClassOptional.get();
         log.info("groupClass str is {}",groupClass.toString());
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    日志输出如下:

    groupClass str is GroupClass(id=2, name=名字, groupNo=Ano, students=[Student [id=2, name=cname, studentNo=122, groupClassId=2], Student [id=1, name=bname1, studentNo=12, groupClassId=2]])

    重写实体类 toString 方法,可以解决了父子实体类打印输出时,因相互依赖循环嵌套中堆栈溢出问题;但当实体类通过 SpringBoot 中 @ResponseBody以 Json 格式输出给前端时,会出现序列化堆栈溢出问题

    Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)

    该错误解决方法:在父类或子类对应的关联字段上,加上 JsonIgnore 注解,即在 Json序列化 时忽略该属性

    // Json 序列化时忽略该属性
    @JsonIgnore
    @ManyToOne(targetEntity = GroupClass.class,
               cascade = CascadeType.ALL,
               fetch = FetchType.LAZY)
    @JoinColumn(name="group_class_id",referencedColumnName = "id", insertable = false, updatable = false)
    private GroupClass groupClass;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    另一方面,我们在项目配置中打开JPA日志输出调试设置

    srping:
      jpa:
        show-sql: true
    
        hibernate:
          ddl-auto: none
    
        properties:
          # 将 hibernate 执行语句以 sql 形式打印
          hibernate.format_sql: true
          hibernate.id.new_generator_mappings: false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    因此可以从控制台日志输出来查看内部执行的sql逻辑

      select
            groupclass0_.id as id1_5_0_,
            groupclass0_.group_no as group_no2_5_0_,
            groupclass0_.name as name3_5_0_,
            students1_.group_class_id as group_cl2_6_1_,
            students1_.id as id1_6_1_,
            students1_.id as id1_6_2_,
            students1_.group_class_id as group_cl2_6_2_,
            students1_.name as name3_6_2_,
            students1_.student_no as student_4_6_2_ 
        from
            group_class groupclass0_ 
        left outer join
            student students1_ 
                on groupclass0_.id=students1_.group_class_id 
        where
            groupclass0_.id=? 
        order by
            students1_.name desc
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 相关阅读:
    可燃气体报警器:户外工地安全预警先锋,定期检定保障安全无忧
    Vue2.7 + Vite3.2 + Rollup 组件库开发指南
    2023 海外工具站 2 月复盘
    艾泊宇产品战略:灵感于鬼屋,掌握打造卓越用户体验的关键要素
    牛客 NC24307 [USACO 2012 Dec S]Milk Routing
    c++ goto语句
    backup (攻防世界)
    01.从零到1,怎么初始化办公电脑
    javascript中的错误类型
    PostgreSQL数据库从入门到精通系列之五:安装时序数据库TimescaleDB的详细步骤
  • 原文地址:https://blog.csdn.net/weixin_42512684/article/details/128058544