【Spring JPA總結】JPA One-To-One實現的4種方式

【參考】
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)下的雙向和單向關聯(lián)

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

共同主鍵下的雙向和單向關聯(lián)


1. 外鍵關聯(lián)

【數據模型】學生,和學生證:一個學生只有一張學生證,學生證也只屬于某一個學生。


image.png
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。
image.png
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中嘗試刪除數據,能同時刪除兩張表數據。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,623評論 18 399
  • 一. Java基礎部分.................................................
    wy_sure閱讀 3,988評論 0 11
  • 總結精煉于http://www.yiibai.com/jpa/jpa_architecture.html其他不錯的...
    靜心安分讀書閱讀 931評論 0 0
  • Hibernate 序 創(chuàng)建 hibernate 工程示例: 創(chuàng)建工程,引入 jar 包 創(chuàng)建配置文件 hiber...
    WJunF閱讀 1,111評論 0 3
  • 一、重點知識 git 監(jiān)視的是文件內容的修改 $ git checkout -- abc.txt : 其實是用版本...
    一花一世界yu閱讀 965評論 0 2

友情鏈接更多精彩內容