关联关系映射

数据实体之间通常不是孤立存在的,而是存在关联关系的,例如教师和学生通常是多对多关系,而教室和学生则是一对多关系。关系型数据库大多是支持通过JOIN连接查询和子查询来实现关联查询的,此外我们也可以在应用层通过多次查询获取有关联的数据,JPA注解支持在实体类上定义关联关系映射,自动生成关联查询操作。

单向关联和双向关联

实体类中,关联其实可以被定义为单向关联和双向关联两种方式。例如教室和学生的1 - N关系中,如果我们仅在学生实体中包含教室的引用,那么这就是单向关联;如果我们既在学生中包含教室的引用,也在教室中包含学生集合的引用,这就是双向关联。因此实际上,JPA中我们总共可以归纳出7种关联映射关系。

  • 单向1 - 1
  • 单向1 - N
  • 单向N - 1
  • 单向N - N
  • 双向1 - 1
  • 双向1 - N
  • 双向N - N

单向N - 1

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注解,它的几个重要属性如下:

  • cascade:级联策略,PERSIST级联保存操作,MERGE级联合并操作,REMOVE级联删除操作,REFRESH级联刷新操作,ALL级联以上全部操作
  • fetch:关联抓取策略,可以设置为EAGER立即加载或LAZY懒加载,默认是立即加载的
  • targetEntity:关联的实体类,如果不指定,JPA将使用反射自行判断。推荐指定该属性,它能够避免反射,因此性能更好一些

下面代码中,我们在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

单向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

单向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要复杂一点,我们知道关系型数据库中表达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

双向关联其实就是实体的两边都有对方的引用,我们学会单向关联后,其实双向关联也就知道该如何编写了。对于双向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实体这一方不控制关联关系。这是什么意思呢?举例来说,假设我们要保存若干RoomStudentRoom实体这一方不控制关联关系就意味着保存数据时我们保存添加了Room引用的Student就会同时插入RoomStudent,而保存添加了Student引用的Room则仅会保存Room,不会保存Student

在数据库中,1 - N关系的外键在N的一方上,因此我们通常都把mappedBy放在1的实体上,将关联关系交给N的一方维护,这样操作也符合大多数人的直觉。对于1 - 1也是类似的,我们应该尽量将mappedBy放在没有外键的一方上,将维护权交给带外键的实体。

双向1 - 1

假设前面例子的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

双向N - N例子如下,我们改造了之前StudentTeacher实体之间的关联映射配置,实现了双向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实体一方。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap