我們都知道數(shù)據(jù)庫(kù)表與表之間有如下四種關(guān)系
1:1(一對(duì)一耍攘,相應(yīng)的注解叫@OneToOne)
1:n(一對(duì)多仁期,相應(yīng)的注解叫@OneToMany)
n:1(多對(duì)一藏杖,相應(yīng)的注解叫@ManyToOne)
n:n(多對(duì)多,相應(yīng)的注解叫@ManyToMany)
環(huán)境說(shuō)明:
首先我們?cè)谥v解之前北启,我們先約定幾個(gè)表關(guān)系
然后我在代碼中定義了一個(gè)抽象的entity父類
AbstractEntity
卜朗,所有的entity都繼承于它
@Data
@Accessors(chain = true)
@MappedSuperclass
@JsonIgnoreProperties(value = {"handler","hibernateLazyInitializer","fieldHandler"})
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") //防止entity互相引用導(dǎo)致json解析進(jìn)入死循環(huán)
public abstract class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增主鍵
Long id;
@Temporal(TemporalType.TIMESTAMP) // 時(shí)間格式:YYYY-MM-dd HH:mm:ss
@Column(name = "gmt_create", columnDefinition = "timestamp DEFAULT CURRENT_TIMESTAMP comment '創(chuàng)建時(shí)間'")
Date gmtCreate;
@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "gmt_modified", columnDefinition = "timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '最后修改時(shí)間'")
Date gmtModified;
}
在講解這四個(gè)注解之前,我們還需要先了解一下@JoinColumn
這個(gè)注解
@JoinColumn用于注釋表中的字段咕村,與@Column不同的是它要保存表與表之間關(guān)系的字段场钉;
- name:是用來(lái)標(biāo)記表中對(duì)應(yīng)的字段的名稱。如果不設(shè)置name的值懈涛,默認(rèn)情況下逛万,name的取值規(guī)則如下:name=關(guān)聯(lián)的表的名稱 + "_" + 關(guān)聯(lián)表主鍵的字段名。
- referencedColumnName:默認(rèn)情況下批钠,關(guān)聯(lián)的實(shí)體的主鍵一般用來(lái)做外鍵的宇植。如果不想用主鍵作為外鍵,則需要設(shè)置referencedColumnName屬性价匠,如:
@JoinColumn(name="emp_id", referencedColumnName="emp_no")
下面我們分別來(lái)對(duì)四個(gè)注解來(lái)進(jìn)行講解
一. @OneToOne
表 employees 和 address 是一對(duì)一的關(guān)系
在jpa中是這樣定義的:
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
public class Address extends AbstractEntity {
// ...其它字段
@OneToOne
@JoinColumn(name = "emp_id")
private Employee employee;
}
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "employees")
public class Employee extends AbstractEntity {
@OneToOne
@JoinColumn(name = "addr_id")
private Address address;
// ...
}
@OneToOne有如下幾個(gè)可選參數(shù)
targetEntity=void.class
当纱,關(guān)聯(lián)的實(shí)體類。
cascade={}
踩窖,可選值有CascadeType.ALL
,CascadeType.PERSIST
,CascadeType.MERGE
,CascadeType.REMOVE
,CascadeType.REFRESH
,CascadeType.DETACH
坡氯。
fetch=FetchType.EAGER
,可選值有FetchType.EAGER
,FetchType.LAZY
。
optional=true
箫柳,可選值有true
,false
手形。
mappedBy=""
。
orphanRemoval=false
悯恍,可選值有false
库糠,true
。
我們對(duì)以上幾個(gè)參數(shù)做下詳細(xì)的說(shuō)明:
optional:表示關(guān)聯(lián)的實(shí)體是否能夠存在null值涮毫。默認(rèn)為true瞬欧,表示可以存在null值。如果為false罢防,則要同時(shí)配合使用 @JoinColumn 標(biāo)記艘虎。
fetch:在一對(duì)一關(guān)系中,fetch 默認(rèn)是 FetchType.EAGER的咒吐,也就是立即提取關(guān)聯(lián)的實(shí)體野建,F(xiàn)etchType.LAZY 表示懶加載,只有你在 get 這個(gè)關(guān)聯(lián)實(shí)體的時(shí)候恬叹,jpa 才會(huì)去數(shù)據(jù)庫(kù)執(zhí)行這個(gè)子查詢候生。
targetEntity: 該參數(shù)可以不指定,JPA會(huì)自動(dòng)將外鍵關(guān)聯(lián)到Employee绽昼,如果沒(méi)有@JoinColumn參數(shù)指定關(guān)聯(lián)字段唯鸭,默認(rèn)生成的外鍵字段名稱為
屬性名_主鍵名
, 如Address中Employee屬性名稱為employee绪励,Employee表的主鍵名稱為 emp_no肿孵,那么生成的外鍵字段名稱就為employee_emp_no
。cascade:級(jí)聯(lián)操作疏魏,JPA允許您將狀態(tài)轉(zhuǎn)換從父實(shí)體傳播到子實(shí)體停做。為此,JPA javax.persistence.CascadeType定義了各種級(jí)聯(lián)操作類型大莫。對(duì)于這個(gè)參數(shù)蛉腌,我們來(lái)做個(gè)實(shí)驗(yàn)。
在上面我們創(chuàng)建entity的時(shí)候只厘,沒(méi)有指定由誰(shuí)管理外鍵烙丛,雙方都有對(duì)方的外鍵字段,也就是employee中有外鍵addr_id羔味,address中也有外鍵emp_id河咽。
如果此時(shí)我們不指定cascade,也就是cascade默認(rèn)為{}
的時(shí)候赋元,我們插入一條數(shù)據(jù)
Employee employee = new Employee()
.setFirstName("Tom")
.setLastName("Welliam")
.setBirthDate(new Date())
.setHireDate(new Date())
.setGender(Employee.Gender.F)
.setAddress(new Address().setHome("保利國(guó)際B1棟4703"));
employeeRepository.save(employee);
我們插入一條員工數(shù)據(jù)忘蟹,并且set了員工的地址信息飒房,發(fā)現(xiàn)插入報(bào)錯(cuò),如下:
因?yàn)閍ddress在數(shù)據(jù)庫(kù)中還不存在媚值,無(wú)法保存員工地址信息狠毯。
如果我們?cè)贏ddress的employee字段上加上cascade = CascadeType.PERSIST
會(huì)出現(xiàn)什么結(jié)果呢?
public class Address extends AbstractEntity {
@OneToOne(cascade = CascadeType.PERSIST)
private Employee employee;
}
同樣運(yùn)行上面的保存員工信息的代碼褥芒,發(fā)現(xiàn)還是報(bào)同樣的錯(cuò)誤嚼松,為什么呢?因?yàn)槲覀僺ave的是Employee锰扶,在Address 中的 cascade 對(duì)save Employee是無(wú)效的献酗。 如果要想看到效果,那么我們可以把上面的代碼修改一下坷牛,改為保存Address:
Address address = new Address().setHome("保利國(guó)際B1棟4703")
.setEmployee(new Employee().setFirstName("Tom")
.setLastName("Welliam")
.setBirthDate(new Date())
.setHireDate(new Date())
.setGender(Employee.Gender.F));
addressRepository.save(address);
可以看到凌摄,當(dāng)我們save address的時(shí)候,jpa執(zhí)行了兩條SQL語(yǔ)句漓帅,先插入employee,然后再插入address痴怨。
這里我們先來(lái)明確一個(gè)概念:父表和字表忙干。父表和子表的概念我們也可以理解為主、副之分浪藻,比如此處員工和地址捐迫,一般我們認(rèn)為,某地址是屬于某個(gè)員工的爱葵,那么我們說(shuō)員工表應(yīng)該為父表(主表)施戴,地址表為子表(副表)。所以我們應(yīng)該把cascade = CascadeType.PERSIST
作用在父表 Employee 的 address 字段上萌丈,表示由employee來(lái)管理address赞哗。所以員工和地址的一對(duì)一關(guān)系應(yīng)該改成下面這樣
public class Employee extends AbstractEntity {
@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "addr_id")
private Address address;
}
public class Address extends AbstractEntity {
@OneToOne
@JoinColumn(name = "emp_id")
private Employee employee;
}
至于cascade 的不同取值說(shuō)明如下:
CascadeType.PERSIST
: 關(guān)聯(lián)持久化(插入)
CascadeType.MERGE
:級(jí)聯(lián)合并(更新)
CascadeType.REMOVE
:關(guān)聯(lián)刪除
CascadeType.REFRESH
: 關(guān)聯(lián)刷新(查詢)
CascadeType.DETACH
:脫離關(guān)聯(lián),也就是關(guān)閉外鍵檢查辆雾,放在父表時(shí)肪笋,那么就可以單獨(dú)刪除父表數(shù)據(jù),而不影響子表數(shù)據(jù)度迂。放在子表時(shí)刪除子表數(shù)據(jù)無(wú)效藤乙。
CascadeType.ALL
:包含以上所有關(guān)聯(lián)操作邏輯
- mappedBy: 上面講到父表和子表的區(qū)分,在上面我們并沒(méi)有區(qū)分員工和地址誰(shuí)是父誰(shuí)是子惭墓,父表是有外鍵的一方坛梁,而子表是沒(méi)有外鍵的一方。如果我們?cè)陔p方都沒(méi)有指定mappedBy參數(shù)腊凶,那么雙方將互為父子關(guān)系划咐,雙方都有外鍵字段拴念。上面我們分析過(guò),員工和地址表之間尖殃,員工應(yīng)該為父表丈莺,地址應(yīng)該為子表,通常我們查詢員工信息的時(shí)候需要帶出員工的地址信息送丰,所以員工表中應(yīng)該存在地址表的外鍵字段缔俄,而地址表中不需要員工的外鍵字段。如何表示這個(gè)關(guān)系呢器躏?這就是 mappedBy 的作用了俐载,我們?cè)谧颖?Address 的 employee 字段上加上
mappedBy="address"
(注意這個(gè)address是屬性名稱,如果你在 Employee 中定義 Address 的屬性名稱為 addr ,那么這里就要寫成mappedBy="addr"
)登失,它表示將Address 交給 Employee 去管理遏佣。
public class Address extends AbstractEntity {
@OneToOne(mappedBy = "address")
@JoinColumn(name = "emp_id")
private Employee employee;
}
public class Employee extends AbstractEntity {
@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "addr_id")
private Address address; // mappedBy 要指定的名稱
}
- orphanRemoval:我懶得去做測(cè)試了揽浙,這里有人已經(jīng)做過(guò)測(cè)試https://www.oschina.net/question/925076_157346状婶,簡(jiǎn)單來(lái)說(shuō),可以將它理解為 CascadeType.REMOVE 的加強(qiáng)馅巷, CascadeType.REMOVE 是刪除膛虫,而 orphanRemoval 可以僅移除關(guān)聯(lián)關(guān)系,也就是將外鍵設(shè)置為null钓猬,數(shù)據(jù)依然保留(也許理解不一定正確稍刀,如有錯(cuò)誤,還請(qǐng)不吝指教)敞曹。
二. @OneToMany 和 @ManyToOne
employees 和 salaries 是一對(duì)多的關(guān)系
在jpa中是這樣定義的:
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "Salaries")
public class Salary extends AbstractEntity {
@ManyToOne
private Employee employee;
}
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "employees")
public class Employee extends AbstractEntity {
@OneToMany(mappedBy = "employee")
private Set<Salary> salaries;
}
@OneToMany 的可選參數(shù)
targetEntity=void.class
cascade={}
fetch=FetchType.LAZY
mappedBy=""
orphanRemoval=false
@ManyToOne 只有四個(gè)可選參數(shù)
targetEntity=void.class
cascade={}
fetch=FetchType.EAGER
optional=true
他們作用在一對(duì)一的關(guān)系中已經(jīng)講的很清楚了账月,他們只是默認(rèn)取值不一樣而已。
可以看到澳迫,在 @OneToMany 中有 mappedBy 局齿,而 @ManyToOne 中沒(méi)有 mappedBy ,如果你理解了我上面說(shuō)的 mappedBy 的作用就應(yīng)該很清楚了纲刀,在一對(duì)多的關(guān)系中项炼,外鍵只可能存在于多的那一方,所以示绊,在 @ManyToOne 這個(gè)注解中不可能存在 mappedBy 這個(gè)參數(shù)就好理解了锭部。既然如此,那為什么 @OneToMany 中還需要 mappedBy 這個(gè)參數(shù)呢面褐?默認(rèn) mappedBy 為多的一方不久好了嗎拌禾?那是因?yàn)樵谝粚?duì)多的關(guān)系中,通常我們是不會(huì)再需要一個(gè)中間表去關(guān)聯(lián)的展哭,只會(huì)在多的一方添加一個(gè)外鍵字段即可湃窍。這時(shí)候我們就需要手動(dòng)指定 mappedBy 參數(shù)闻蛀,如果不指定它為多的一方,那么默認(rèn)JPA會(huì)幫我們自動(dòng)生成一個(gè)中間表您市,這可能不是我們想看到的觉痛。當(dāng)然,除此之外茵休,我們也可以用 @JoinColumn 注解達(dá)到同樣的效果薪棒。
特別說(shuō)明一下在 @OneToMany 和 @ManyToOne 中,mappedBy 參數(shù)不能和@JoinTable 以及 @JoinColumn 同時(shí)出現(xiàn)榕莺,在 @OneToOne 中卻是可以的俐芯。
三. @ManyToMany
employees 和 departments 是多對(duì)多的關(guān)系
在jpa中是這樣定義的:
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "departments")
public class Department extends AbstractEntity{
@ManyToMany
@JoinTable(name="dept_emp",
joinColumns={@JoinColumn(name="dept_id")},
inverseJoinColumns={@JoinColumn(name="emp_id")})
private Set<Employee> employees;
}
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "employees")
public class Employee extends AbstractEntity {
@ManyToMany
@JoinTable(name="dept_emp",
joinColumns={@JoinColumn(name="emp_id")},
inverseJoinColumns={@JoinColumn(name="dept_id")})
private Set<Department> departments;
}
@ManyToMany 也是只四個(gè)可選參數(shù)
targetEntity=void.class
cascade={}
fetch=FetchType.LAZY
mappedBy=""
在這里我們出現(xiàn)了一個(gè)新的 @JoinTable 注解,這個(gè)注解定義了中間表钉鸯。在 @ManyToMany 的關(guān)系中吧史,必定會(huì)出現(xiàn)一個(gè)中間表,如果不用 @JoinTable 注解唠雕,默認(rèn)JPA生成的中間表表名為 employees_departments 或 departments_employees贸营。
至于這個(gè)其中誰(shuí)在前,誰(shuí)在后由 mappedBy 決定岩睁。如果不指定mappedBy莽使,又沒(méi)有 @JoinTable 注解來(lái)指定中間表名稱,那么JPA自動(dòng)給我們生成的表會(huì)是這樣: