【參考】
https://hellokoding.com/one-to-one-mapping-in-jpa-and-hibernate/
本文介紹Spring Jpa的One-To-One關聯(lián)的4種方式,包含:
外鍵關聯(lián)下的雙向和單向關聯(lián):
外鍵關聯(lián)的意思是兩張表都有自己的主鍵,一張表的主鍵作為外鍵存在于另一張表中:

共同主鍵下的雙向和單向關聯(lián):
共同主鍵的意思是兩張表的主鍵一樣,一張表的主鍵同時又是另一張表的主鍵+外鍵:

1. 外鍵關聯(lián)
【數據模型】學生,和學生證:一個學生只有一張學生證,學生證也只屬于某一個學生。

1.1 使用@OneToOne和@JoinColumn來實現雙向的外鍵關聯(lián)
雙向的One-To-One關聯(lián),在兩個entity中都需要用到注解@OneToOne:
- 在
Student實體中,@OneToOne是主動的一方,@JoinColumn表示外鍵的列,name屬性是用來標識表中所對應的字段的名稱。 - 在
StudentCard中,則是被關聯(lián)的一方,@OneToOne需要寫mappedBy的值,這個的含義是被映射(即這方不用管關聯(lián)關系),指向Student實體類。
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToOne(cascade = CascadeType.ALL, optional = false)
@JoinColumn(name = "student_card_id")
private StudentCard studentCard;
}
@Entity
@Table(name = "student_card")
public class StudentCard {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true, nullable = false)
private String code = UUID.randomUUID().toString();
@OneToOne(mappedBy = "studentCard")
private Student student;
}
【測試a】主entity(student)中findById,能查到兩邊的數據,sql:
select
student0_.id as id1_3_0_,
student0_.name as name2_3_0_,
student0_.student_card_id as student_3_3_0_,
studentcar1_.id as id1_4_1_,
studentcar1_.code as code2_4_1_
from
student student0_
inner join
student_card studentcar1_
on student0_.student_card_id=studentcar1_.id
where
student0_.id=?
【測試b】子entity(student_card)中findById,能查到兩邊的數據,sql:
select
studentcar0_.id as id1_4_0_,
studentcar0_.code as code2_4_0_,
student1_.id as id1_3_1_,
student1_.name as name2_3_1_,
student1_.student_card_id as student_3_3_1_
from
student_card studentcar0_
left outer join
student student1_
on studentcar0_.id=student1_.student_card_id
where
studentcar0_.id=?
【測試c】在主entity中測試插入,將會插入數據到兩張表中:
@Test
public void saveTest() {
Student student = Student.builder().name("test1").studentCard(new StudentCard()).build();
studentRepository.save(student);
}
【測試d】在子entity中無法插入主entity的數據,但能單獨創(chuàng)建子entity自己的數據,不過因為它的id并沒有關聯(lián)到主entity的表中,也就沒有什么意義(像是一個沒有學生的學生證)。
【測試e】在主entity中刪除數據,同時會刪除關聯(lián)的子entity數據。
【測試f】在子entity中嘗試刪除數據,但因為有外鍵的關聯(lián),無法刪除數據。(除非它本身的數據并沒有被主entity關聯(lián)到,是個orphan data):
@Test
public void delete() {
studentCardRepository.deleteById(1);
}
1.2 使用@OneToOne和@JoinColumn來實現單向的外鍵關聯(lián)
數據模型和#1.1一樣。
單向的One-To-One關聯(lián),只需要在Student實體中用到注解@OneToOne:
- 在
Student實體中,@OneToOne是主動的一方,@JoinColumn表示外鍵的列,name屬性是用來標識表中所對應的字段的名稱。(同雙向的關聯(lián)時配置一樣)。 - 在
StudentCard中,則不需要配置。
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToOne(cascade = CascadeType.ALL, optional = false)
@JoinColumn(name = "student_card_id")
private StudentCard studentCard;
}
@Entity
@Table(name = "student_card")
public class StudentCard {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true, nullable = false)
private String code = UUID.randomUUID().toString();
}
【測試a】主entity中的findById,查詢同上述#1.1一樣,會inner join子entity,返回兩張表的數據。
【測試b】而由于是單向關聯(lián),子entity中findById 并不會同上述#1.1一樣有l(wèi)eft outer join,只會返回自己的數據,sql:
select
studentcar0_.id as id1_4_0_,
studentcar0_.code as code2_4_0_
from
student_card studentcar0_
where
studentcar0_.id=?
【測試c】主entity插入同上述#1.1一樣,執(zhí)行后可以同時插入兩張表的數據。
【測試d】子entity插入同上述#1.1一樣,可單獨插入自己的數據(不過并沒有意義)。
【測試e】主entity刪除同上述#1.1一樣,執(zhí)行后可以同時刪除兩張表的數據。
【測試f】子entity能單獨刪除自己的數據。
2. 共同主鍵
【數據模型】員工和員工的薪水:一個員工有自己的薪水,每個員工的薪水都不一樣,薪水表中的employee_id即為主表employee中的id。

2.1 使用@OneToOne和@MapsId來實現雙向的共同主鍵關聯(lián)
共同主鍵表示兩個實體共享主鍵,兩個實體類都需要寫@OneToOne。在子entity中,主鍵同時又是外鍵。
- 雙向的關系,所以需要在主entity也加上
@OneToOne。主entity上有mappedBy,因為共同主鍵關聯(lián)時,子entity占據了主導地位。 - 在子entity中,需要再加上
@MapsId,表示該列也是主鍵。而該列同時需要加上@JoinColumn,表示它是外鍵(即關聯(lián)到主entity的id)。
@Entity
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "employee")
private EmployeeSalary employeeSalary;
}
@Entity
@Table(name = "employee_salary")
public class EmployeeSalary {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private double salary;
@OneToOne(cascade = CascadeType.ALL, optional = false)
@JoinColumn(name = "employee_id")
@MapsId
private Employee employee;
}
【測試a】主entity中findById,能查到兩邊的數據,sql:
select
employee0_.id as id1_1_0_,
employee0_.name as name2_1_0_,
employeesa1_.employee_id as employee1_2_1_,
employeesa1_.salary as salary2_2_1_
from
employee employee0_
left outer join
employee_salary employeesa1_
on employee0_.id=employeesa1_.employee_id
where
employee0_.id=?
【測試b】子entity中findById,能查到兩邊的數據,這里的sql會查兩遍,第一遍是子entity自己的表,即employee_salary,第二遍是自己的表+left outer join關聯(lián)到主entity表。這樣看來效率比較低下。具體sql:
select
employeesa0_.employee_id as employee1_2_0_,
employeesa0_.salary as salary2_2_0_
from
employee_salary employeesa0_
where
employeesa0_.employee_id=?
select
employee0_.id as id1_1_0_,
employee0_.name as name2_1_0_,
employeesa1_.employee_id as employee1_2_1_,
employeesa1_.salary as salary2_2_1_
from
relation_study_employee employee0_
left outer join
employee_salary employeesa1_
on employee0_.id=employeesa1_.employee_id
where
employee0_.id=?
【測試c】在#2.1一開始解釋了,雖然employee是主entity,但其實共同主鍵子entity(即employee_salary)占據了主導地位,所以在主entity中想要插入兩張表的數據,需要將employee entity set回salary中才可以,如:
@Test
public void save() {
Employee employee = Employee.builder().name("mark").build();
EmployeeSalary employeeSalary = new EmployeeSalary();
employeeSalary.setSalary(9000d);
employee.setEmployeeSalary(employeeSalary);
employeeSalary.setEmployee(employee);
employeeRepository.save(employee);
}
【測試d】嘗試在employee_salary中插入兩張表的數據,成功:
@Test
public void saveTest() {
EmployeeSalary employeeSalary = new EmployeeSalary();
employeeSalary.setSalary(9000d);
employeeSalary.setEmployee(Employee.builder().name("mark").build());
employeeSalaryRepository.save(employeeSalary);
}
【測試e】在主entity中刪除數據,同時會刪除關聯(lián)的子entity數據。
【測試e】在子entity中刪除數據,同時會刪除關聯(lián)的主entity數據。
2.2 使用@OneToOne和@PrimaryKeyJoinColumn來實現雙向的共同主鍵
數據模型和#2.1一樣。
共同主鍵表示兩個實體共享主鍵,在子entity中,主鍵同時又是外鍵:
- 因為是單向的關聯(lián),主entity不需要配置。
- 子entity需要配置
@OneToOne和@PrimaryKeyJoinColumn,在@PrimaryKeyJoinColumn中需要指定本表的列(employee_id),以及要關聯(lián)到另一張表的列名(id)。
@Entity
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
}
@Entity
@Table(name = "employee_salary")
public class EmployeeSalary {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int employeeId;
private double salary;
@OneToOne(cascade = CascadeType.ALL, optional = false)
@PrimaryKeyJoinColumn(name = "employee_id", referencedColumnName = "id")
private Employee employee;
}
【測試a】由于是單向關聯(lián),主entity(employee)中findById只會返回自己的數據,sql:
select
employee0_.id as id1_1_0_,
employee0_.name as name2_1_0_
from
relation_study_employee employee0_
where
employee0_.id=?
【測試b】子entity(employee_salary)中findById,和#2.1一樣,會返回兩張表數據。唯一不同的是,這次只會查詢一遍,即使用left outer join返回所有數據,并不會查兩遍,具體sql:
select
employeesa0_.employee_id as employee1_2_0_,
employeesa0_.salary as salary2_2_0_
from
relation_study_employee_salary employeesa0_
where
employeesa0_.employee_id=?
Hibernate:
select
employee0_.id as id1_1_0_,
employee0_.name as name2_1_0_
from
relation_study_employee employee0_
where
employee0_.id=?
【測試c】在主entity中嘗試插入數據,由于是單向關聯(lián),只會插入本表數據。
【測試d】在子entity中嘗試插入數據,需要分別調用各自的repository才能插入兩張表的數據:
@Test
public void saveTest() {
Employee employee = Employee.builder().name("mark").build();
employeeRepository.save(employee);
EmployeeSalary employeeSalary = new EmployeeSalary();
employeeSalary.setSalary(9000d);
employeeSalary.setEmployee(employee);
employeeSalary.setEmployeeId(employee.getId());
employeeSalaryRepository.save(employeeSalary);
}
【測試e】在主entity中嘗試刪除數據,只能刪除自己表的數據。
【測試f】在子entity中嘗試刪除數據,能同時刪除兩張表數據。