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);
}
以 @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;
}
@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;
}
而双向关联的 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<>();
}
其它关联注解用法类同:
@ManyToMany
:多对多;@OneToOne
:一对一;
@OrderBy
字段排序
@OrderBy
顾名思义目的是为根据某个字段进行排序,注解一般与 @OneToMany
和 @ManyToMany
同时使用,
@OneToMany(mappedBy = "groupClass")
// 根据名字对 学生对象逆向排序
@OrderBy("name DESC")
private List<Student> students = new ArrayList<>();
使用实例如下,首先插入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);
根据Id
查询 GroupClass 对象,在输出打印对象的toString
方法钱,需要重新写一下 对象的 toString
函数,防止循环嵌套导致的堆栈溢出问题
//Student.class
@Override
public String toString() {
return "Student [id=" + id + ", name=" +name + ", studentNo=" +studentNo + ", groupClassId=" + groupClassId
+ "]";
}
在上面例子中, GroupClass
对象中 toString
函数引用到 Student
,而 Student
的 toString
同时引用到 GroupClass
,使用时会触发嵌套循环,造成栈溢出问题;
输出 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());
}
日志输出如下:
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;
另一方面,我们在项目配置中打开JPA日志输出调试设置
srping:
jpa:
show-sql: true
hibernate:
ddl-auto: none
properties:
# 将 hibernate 执行语句以 sql 形式打印
hibernate.format_sql: true
hibernate.id.new_generator_mappings: false
因此可以从控制台日志输出来查看内部执行的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