[JavaEE - JPA] 7. ORM的核心註解 - 關係類型

本文繼續介紹JPA ORM的核心註解中和關係映射相關的部分。

關係映射的處理絕對是一個JPA應用最爲重要的部分之一。關係映射處理的好,不僅僅是建模上的成功,而且在程序性能上也會更勝一籌。關係映射處理的不好很容易造成程序性能底下,各種Bug頻繁出現,而且這些Bug通常還會比較隱蔽,總是在關鍵時刻掉鏈子。我想這也是爲什麼很多開發人員說JPA入門容易,精通難得原因之一。因爲關係確實不是那麼好處理的,不僅需要對業務有相當深刻的見解,更需要對JPA提供的各種關係映射類型有入木三分的理解。

本文就嘗試來理一理JPA中的各種關係映射類型。

關係的基本術語

在介紹JPA提供的幾種關係映射類型之前,有必要先來學習一下關於關係的三個基本術語:角色,方向和基數。這對於理解關係的本質十分有幫助。

角色(Role)

所謂”一個巴掌拍不響”,一個關係不可能只有一個參與方,而且任何由多個參與方組成的關係必定都可以拆解成兩兩關係。因此,在這裏我們也只考慮由兩個參與方所組成的關係。比如我們常見的僱傭關係,就是企業和員工之間的一種關係,那麼企業和員工在這層關係中就分別扮演着僱傭者和被僱傭者的角色。而且在現實生活中,一個人是可以同時扮演者多種角色的,比如被僱傭者在家庭中可以作爲妻子/丈夫/孩子/父親/母親等角色,反映到程序中就是一個實體可以被別的實體所引用,一旦被引用,即代表了關係的建立。被引用的次數越多,那麼就表示這個實體所承擔的角色就越多。這一點很好理解,比如當Employee實體在Department實體中被引用,表示Employee承擔的是部門員工的角色;當Employee實體在Payroll實體中被引用時,就表示Employee此時承擔的是薪酬領取者的角色。

方向(Directionality)

除了角色之外,方向是關係的另一個要素。關係不會毫無緣由的誕生,總需要有一個角色來打破僵局,建立這層關係。那麼主動建立的一方我們可以將其稱爲源角色(Source Role,簡稱Source),而被動響應的一方則可以被稱爲目標角色(Target Role,簡稱Target)。

反映到程序中,關係的方向指的就是主動引用,比如我們在Employee類型中引用Department類型,那麼就是由Employee指向Department的一層關係,Employee扮演的是Source,Department扮演的是Target。如果在Department類型中也引用了Employee類型,那麼就是由Department指向Employee的一層關係,Department扮演的是Source,Employee扮演的是Target。

因此如果互相引用,那關係就是一個雙向關係了。就好比我知道我爸爸是誰,我爸爸也知道我是誰。

而單向關係在這個世界中其實更多一些,比如我知道馬雲是誰,而馬雲肯定不知道我是誰。又或者一個男生暗戀一個女生,這些都是單向關係。

基數(Cardinality)

關係的最後一個要素便是基數。所謂的基數實際上描述的是一個很簡單的概念,比如法律上的一夫一妻制和一夫多妻制。即參與到關係的兩個角色,在數量上的特徵。一個部門可以有多個員工,而一個員工通常只屬於一個部門。一個學生可以參與到多個社團,而一個社會也可以擁有多個學生作爲其成員。

反映到程序中,就是引用類型是一個單值類型,還是一個集合類型的區別。如果在Employee類型中引用Department類型,通常只會引用聲明一個department實體。但是反過來再Department類型中引用其Employee類型的時候,通常會使用一個集合來表示其下所有的Employee。

關係映射

基礎概念介紹完畢,下面開始進入正題。

首先,根據關係中目標角色的數量,可以將關係簡單分爲兩種:

  1. 單值映射(Single-Valued Mapping)
  2. 集合映射(Collection-Viewed Mapping)

單值映射

所謂單值映射,就是目標角色的基數(Cardinality)等於1。也就意味着存在兩種情況:

一對一(One-to-One)

典型的例子比如,Employee類型(僱員)和Workspace類型(工位)之間的關係。此時使用JPA提供的@OneToOne註解進行描述:

@Entity
public class Employee {
    @Id 
    private int id;

    private String name;

    @OneToOne
    @JoinColumn(name="WSPACE_ID")
    private Workspace workspace;
    // ...
}

@Entity
public class Workspace {
    @Id 
    private int id;

    private String location;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上述代碼是一對一單向關係的示例代碼。其中出現了一個名爲@JoinColumn的註解,可以將這個註解理解成外鍵。而這個外鍵的列名則是通過@JoinColumn註解中的name屬性進行聲明。同時,還需要注意的是當@OneToOne@JoinColumn一起使用的時候,這個外鍵所在的列實際上還需要滿足唯一性的約束。因爲每個Workspace的實例實際上是被唯一的一個Employee實例所獨佔的,所以在該外鍵列中不可能存在相等的值。

那麼一對一雙向關係如何用JPA提供的註解進行聲明呢?比如下面這一段代碼,Employee不僅引用了Workspace類型,Workspace中也同時引用了Employee類型:

@Entity
public class Employee {
    @Id 
    private int id;

    private String name;

    @OneToOne
    @JoinColumn(name="WSPACE_ID")
    private Workspace workspace;
    // ...
}

@Entity
public class Workspace {
    @Id 
    private int id;

    private String location;

    @OneToOne(mappedBy = "workspace")
    private Employee employee;
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

注意在上述的Workspace類中,也使用了@OneToOne註解來聲明一個從Workspace指向Employee的關係。但是這裏並沒有使用@JoinColumn註解來聲明外鍵的相關信息。也就是說,上述實體類對應的數據庫表中並不會含有引用Employee類型的外鍵列。這是JPA中規定的對於雙向一對一關係的映射方式。尤其注意@OneToOne註解中的mappedBy屬性,這個屬性的值實際上是關係中源角色(Employee)一方引用目標角色(Workspace)一方的引用名稱,假如我們把Employee類中的workspace改成ws,那麼相應的Workspace類中@OneToOnemappedBy屬性也需要被改成ws。這種定義方式,保證了所定義的雙向關係是由參與的兩個@OneToOne來完成的。

如果不考慮規範的話,更加直觀地定義雙向一對一的方式應該是這樣的:

@Entity
public class Employee {
    @Id 
    private int id;

    private String name;

    @OneToOne
    @JoinColumn(name="WSPACE_ID")
    private Workspace workspace;
    // ...
}

@Entity
public class Workspace {
    @Id 
    private int id;

    private String location;

    @OneToOne
    @JoinColumn(name="E_ID")
    private Employee employee;
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

這種定義方式當然可以,但是這樣定義的兩個一對一關係之間就沒有了聯繫,它們實際上定義了兩個單向的一對一關係。JPA不會認爲這兩個一對一關係實際上共同構成了一個雙向的一對一關係。體現在表結構上,就是在Workspace對應的表結構中,也會有一個外鍵列用來引用Employee的相應記錄。

因此,如果你希望定義的是一個雙向的一對一關係,還是遵守JPA的規範,正確地使用@OneToOne註解及其mappedBy屬性和用於定義外鍵列的@JoinColumn註解吧。

最後補充一點,由於@JoinColumn是用來定義外鍵關係的,那麼擁有該註解的一方可以被稱爲關係中的所有方(Owning Side),而被引用的一方則被稱爲反轉方(Inverse Side)。所以如果是定義雙向關係的話,在反轉方一般就需要使用mappedBy屬性了。

多對一(Many-to-One)

多對一這種關係的例子可謂是隨手拈來,比如上文中多次提到的Employee和Department之間的關係,就可以這樣描述:

@Entity
public class Employee {
    @Id 
    private int id;

    @ManyToOne
    @JoinColumn(name="DEPT_ID")
    private Department department;
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

使用@ManyToOne註解來定義一個多對一關係。對於多對一關係而言,它一般需要一個@JoinColumn來幫助定義其對應的外鍵信息。因此根據上面的定義,多對一關係中的”多”方,總是可以被視爲此段關係中的所有方(Owning Side),而響應的”一”方,在這種情況下可以被視爲反轉方(Inverse Side)。因此,在上面的例子中,Employee的表結構中就會有一個名爲DEPT_ID的列作爲引用Department記錄的外鍵列。

有了”多對一”,相應地就會有”一對多”。這便是我們即將要介紹的關係。

集合映射

所謂集合映射,就是目標角色的基數(Cardinality)大於1。也就意味着存在下面兩種情況:

一對多(One-to-Many)

一對多這種關係一般是不會單獨出現的,比如我們只想定義從Department指向Employee的一層關係。假設一個Department實例中引用了五個Employee實例,在程序中使用集合類型來描述這層關係很方便,也沒有什麼障礙。但是考慮一下換到數據庫表結構中,應該如何描述這種結構呢?顯然在Department對應的表中是沒法描述的,畢竟一行是無法引用多個(並且數量還不定)其它表結構中的記錄的。所以一對多一般伴隨着多對一作爲雙向關係出現,而且由於@JoinColumn總是會出現在”多”方,因此我們需要在@OneToMany註解中使用mappedBy屬性來聲明這一點,畢竟此時的”一”方是反轉方(Inverse Side),在反轉方中就需要使用mappedBy屬性,這是JPA規範中的一部分。

@Entity
public class Department {
    @Id 
    private int id;

    private String name;
    @OneToMany(mappedBy="department")
    private Collection<Employee> employees;
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

那麼是不是一定要使用mappedBy來表示這個一對多關係是一個雙向關係中的一部分呢?也不一定。
如果你只想定義一個單向的一對多關係,那麼可以不使用mappedBy屬性,此時JPA會推斷你想使用聯接表(Join Table)來完成關係的定義,雖然這個聯接表有其默認的命名規範,但是顯式地聲明它通常是更明智的方案,比如下面這樣:

@Entity
public class Department {
    @Id 
    private int id;

    private String name;

    @OneToMany
    @JoinTable(name="DEPT_EMP",
        joinColumns=@JoinColumn(name="DEPT_ID"),
        inverseJoinColumns=@JoinColumn(name="EMP_ID"))
    private Collection<Employee> employees;
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

此時,Department和Employee的一對多關係會通過表DEPT_EMP進行管理。@JoinTable註解的joinColumns屬性聲明瞭聯接表中引用此關係中的所有方的外鍵列名爲DEPT_ID,inverseJoinColumns屬性聲明瞭聯接表中引用此關係中的反轉方的外鍵名爲EMP_ID。

多對多(Many-to-Many)

一個簡單的多對多的例子是,一個員工可以工作在多個項目中,而一個項目也有多個員工工作在其中:

@Entity
public class Employee {
    @Id 
    private int id;

    private String name;

    @ManyToMany
    @JoinTable(name="EMP_PROJ",
        joinColumns=@JoinColumn(name="EMP_ID"),
        inverseJoinColumns=@JoinColumn(name="PROJ_ID"))
    private Collection<Project> projects;
    // ...
}

@Entity
public class Project {
    @Id 
    private int id;

    private String name;

    @ManyToMany(mappedBy="projects")
    private Collection<Employee> employees;
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

很顯然,這是一個雙向多對多的例子。之所以爲雙向,主要還是取決於在Project類中聲明的@ManyToMany(mappedBy="projects"),如果缺少了這個mappedBy屬性的相關定義,那麼JPA實現在分析這段代碼中出現的兩個@ManyToMany的時候,會認爲這是兩個單向而獨立的多對多關係。因此會嘗試讀取兩個聯接表:一個是我們顯式聲明的表EMP_PROJ,另一個則是根據實體類的名字生成的默認聯接表Project_Employee。顯然生成兩個獨立的聯接表在通常情況下並非我們的目的,所以在使用多對多關係的時候,一定要注意mappedBy屬性的聲明。

原文地址:http://blog.csdn.net/dm_vincent/article/details/52877296

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章