数据实体之间通常不是孤立存在的,而是存在关联关系的,例如教师和学生通常是多对多关系,而教室和学生则是一对多关系。关系型数据库大多是支持通过JOIN连接查询和子查询来实现关联查询的,此外我们也可以在应用层通过多次查询获取有关联的数据,JPA注解支持在实体类上定义关联关系映射,自动生成关联查询操作。
实体类中,关联其实可以被定义为单向关联和双向关联两种方式。例如教室和学生的1 - N
关系中,如果我们仅在学生实体中包含教室的引用,那么这就是单向关联;如果我们既在学生中包含教室的引用,也在教室中包含学生集合的引用,这就是双向关联。因此实际上,JPA中我们总共可以归纳出7种关联映射关系。
N - 1
关联非常常用,下面例子中包含了学生实体(Student)和教室实体(Room),它们具有单向N - 1
关联关系。
CREATE TABLE `t_student` (
`id` bigint NOT NULL AUTO_INCREMENT,
`room_id` bigint NOT NULL,
`name` varchar(50) NOT NULL,
`age` int DEFAULT NULL,
`create_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `t_room` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
数据实体定义如下,代码中我们的Student
实体包含了关联Room
实体的引用,这个字段上使用了@ManyToOne
注解标注了N - 1
关系,@JoinColumn
注解标注了关联字段。
Student.java
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
@Entity
@Table(name = "t_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "create_time")
private Date createTime;
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, targetEntity = Room.class)
@JoinColumn(name = "room_id")
private Room room;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
Room.java
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
@Entity
@Table(name = "t_room")
public class Room implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
对于@ManyToOne
注解,它的几个重要属性如下:
PERSIST
级联保存操作,MERGE
级联合并操作,REMOVE
级联删除操作,REFRESH
级联刷新操作,ALL
级联以上全部操作EAGER
立即加载或LAZY
懒加载,默认是立即加载的下面代码中,我们在EJB里查询了Student
实体,并取出了关联的Room
实体。
package com.gacfox.netstore.ejb;
import com.gacfox.netstore.api.StudentService;
import com.gacfox.netstore.api.model.RoomDto;
import com.gacfox.netstore.api.model.StudentDto;
import com.gacfox.netstore.ejb.entity.Room;
import com.gacfox.netstore.ejb.entity.Student;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Stateless
public class StudentServiceImpl implements StudentService {
@PersistenceContext(unitName = "jpa-netstorer-pu")
private EntityManager entityManager;
@Override
public StudentDto getStudentById(Long id) {
Student student = entityManager.find(Student.class, id);
if (student != null) {
Room room = student.getRoom();
RoomDto roomDto = new RoomDto(room.getId(), room.getName());
return new StudentDto(student.getId(), student.getName(), student.getAge(), roomDto, student.getCreateTime());
} else {
return null;
}
}
}
单向1 - 1
其实和前面介绍的单向N - 1
关联约束是相同的,表结构和之前没有变化。JPA中,我们将@ManyToOne
换成@OneToOne
即可,其它参数都完全相同。假设我们的学生和教室是1 - 1
关系,写法如下。
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, targetEntity = Room.class)
@JoinColumn(name = "room_id")
private Room room;
单向1 - N
的表结构和之前还是一样的。不过在JPA中,需要把我们的第一个例子反过来,让教室Room
实体引用学生Student
实体集合。下面代码中定义了单向1 - N
关联,其中Room
实体使用了@OneToMany
注解,标注该注解的字段是集合类型。
Room.java
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
import java.util.Set;
@Entity
@Table(name = "t_room")
public class Room implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, targetEntity = Student.class)
@JoinColumn(name = "room_id")
private Set<Student> studentSet;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
Student.java
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
@Entity
@Table(name = "t_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "create_time")
private Date createTime;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
EJB中,我们定义了一个根据Room
实体主键获取对象的方法。
package com.gacfox.netstore.ejb;
import com.gacfox.netstore.api.RoomService;
import com.gacfox.netstore.api.model.RoomDto;
import com.gacfox.netstore.api.model.StudentDto;
import com.gacfox.netstore.ejb.entity.Room;
import com.gacfox.netstore.ejb.entity.Student;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.ArrayList;
import java.util.List;
@Stateless
public class RoomServiceImpl implements RoomService {
@PersistenceContext(unitName = "jpa-netstorer-pu")
private EntityManager entityManager;
@Override
public RoomDto getRoom(Long id) {
Room room = entityManager.find(Room.class, id);
if (room != null) {
List<StudentDto> studentDtoList = new ArrayList<>();
RoomDto roomDto = new RoomDto(room.getId(), room.getName(), studentDtoList);
if (room.getStudentSet() != null && !room.getStudentSet().isEmpty()) {
for (Student student : room.getStudentSet()) {
StudentDto studentDto = new StudentDto(student.getId(), student.getName(), student.getAge(), student.getCreateTime());
studentDtoList.add(studentDto);
}
}
return roomDto;
}
return null;
}
}
单向N - N
要复杂一点,我们知道关系型数据库中表达N - N
关联关系需要引入中间表,我们这里的例子中使用Student
学生实体和Teacher
教师实体,它们具有N - N
关系,表结构定义如下。
CREATE TABLE `t_student` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`age` int DEFAULT NULL,
`create_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `t_teacher` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `t_student_teacher` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_id` bigint NOT NULL,
`teacher_id` bigint NOT NULL,
PRIMARY KEY (`id`)
);
实体类定义如下。
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
@Entity
@Table(name = "t_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "create_time")
private Date createTime;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
import java.util.Set;
@Entity
@Table(name = "t_teacher")
public class Teacher implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, targetEntity = Student.class)
@JoinTable(name = "t_student_teacher",
joinColumns = @JoinColumn(name = "teacher_id"),
inverseJoinColumns = @JoinColumn(name = "student_id"))
private Set<Student> studentSet;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
我们这里使用了@ManyToMany
注解定义多对多关系,多对多使用了中间表关联,因此还需要@JoinTable
注解指定中间表相关的信息。
joinColumns
:该属性指定中间表中,对应于当前实体的外键字段inverseJoinColumns
:该属性指定中间表中,对应于被关联实体的外键字段配置好中间表后,我们的单向多对多关系就可以生效了。下面代码我们在EJB中定义了一个根据主键查询教师实体的方法,其中教师关联的学生集合也会被关联取出。
package com.gacfox.netstore.ejb;
import com.gacfox.netstore.api.TeacherService;
import com.gacfox.netstore.api.model.StudentDto;
import com.gacfox.netstore.api.model.TeacherDto;
import com.gacfox.netstore.ejb.entity.Student;
import com.gacfox.netstore.ejb.entity.Teacher;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.ArrayList;
import java.util.List;
@Stateless
public class TeacherServiceImpl implements TeacherService {
@PersistenceContext(unitName = "jpa-netstorer-pu")
private EntityManager entityManager;
@Override
public TeacherDto getTeacherById(Long id) {
Teacher teacher = entityManager.find(Teacher.class, id);
if (teacher != null) {
List<StudentDto> studentDtoList = new ArrayList<>();
TeacherDto teacherDto = new TeacherDto(teacher.getId(), teacher.getName(), studentDtoList);
if (teacher.getStudentSet() != null && !teacher.getStudentSet().isEmpty()) {
for (Student student : teacher.getStudentSet()) {
StudentDto studentDto = new StudentDto(student.getId(), student.getName(), student.getAge(), student.getCreateTime());
studentDtoList.add(studentDto);
}
}
return teacherDto;
}
return null;
}
}
双向关联其实就是实体的两边都有对方的引用,我们学会单向关联后,其实双向关联也就知道该如何编写了。对于双向1 - N
,我们需要在N
的一边标注@ManyToOne
,在1
的一边标注@OneToMany
。
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
@Entity
@Table(name = "t_room")
public class Room implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "room", targetEntity = Student.class)
private Set<Student> studentSet;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Table(name = "t_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "create_time")
private Date createTime;
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, targetEntity = Room.class)
@JoinColumn(name = "room_id")
private Room room;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
注意我们这里Room
实体中用到了mappedBy
属性,它表示将关联关系的控制权交给另一方,Room
实体这一方不控制关联关系。这是什么意思呢?举例来说,假设我们要保存若干Room
和Student
,Room
实体这一方不控制关联关系就意味着保存数据时我们保存添加了Room
引用的Student
就会同时插入Room
和Student
,而保存添加了Student
引用的Room
则仅会保存Room
,不会保存Student
。
在数据库中,1 - N
关系的外键在N
的一方上,因此我们通常都把mappedBy
放在1
的实体上,将关联关系交给N
的一方维护,这样操作也符合大多数人的直觉。对于1 - 1
也是类似的,我们应该尽量将mappedBy
放在没有外键的一方上,将维护权交给带外键的实体。
假设前面例子的Student
实体和Room
实体是1 - 1
关系,实体类定义如下。
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Table(name = "t_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "create_time")
private Date createTime;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, targetEntity = Room.class)
@JoinColumn(name = "room_id")
private Room room;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
@Entity
@Table(name = "t_room")
public class Room implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@OneToOne(mappedBy = "room", targetEntity = Student.class)
private Student student;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
我们在双方实体类都添加了@OneToOne
注解,由于外键在Student
实体上,因此Room
实体中我们使用mappedBy
将关联关系维护权交给对方。
双向N - N
例子如下,我们改造了之前Student
和Teacher
实体之间的关联映射配置,实现了双向N - N
。
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
import java.util.Set;
@Entity
@Table(name = "t_teacher")
public class Teacher implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, targetEntity = Student.class)
@JoinTable(name = "t_student_teacher",
joinColumns = @JoinColumn(name = "teacher_id"),
inverseJoinColumns = @JoinColumn(name = "student_id"))
private Set<Student> studentSet;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
@Entity
@Table(name = "t_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "create_time")
private Date createTime;
@ManyToMany(mappedBy = "studentSet", targetEntity = Teacher.class)
private Set<Teacher> teacherSet;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
代码中,我们在Student
实体中使用了mappedBy
,这表示将关联关系的维护交给Teacher
实体一方。