JPA 表關(guān)聯(lián)注解的實(shí)驗(yàn)

我們都知道數(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)系

image.png

然后我在代碼中定義了一個(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河咽。


表employee的外鍵

表address的外鍵

如果此時(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痴怨。


address

employee

這里我們先來(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 要指定的名稱
}
此時(shí),在Address表中就不會(huì)出現(xiàn)emp_id這個(gè)外鍵字段了
  • 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;
}
自動(dòng)生成的表結(jié)構(gòu)

@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贸营。


image.png

至于這個(gè)其中誰(shuí)在前,誰(shuí)在后由 mappedBy 決定岩睁。如果不指定mappedBy莽使,又沒(méi)有 @JoinTable 注解來(lái)指定中間表名稱,那么JPA自動(dòng)給我們生成的表會(huì)是這樣:


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末笙僚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子灵再,更是在濱河造成了極大的恐慌肋层,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翎迁,死亡現(xiàn)場(chǎng)離奇詭異栋猖,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)汪榔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蒲拉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人痴腌,你說(shuō)我怎么就攤上這事雌团。” “怎么了士聪?”我有些...
    開(kāi)封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵锦援,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我剥悟,道長(zhǎng)灵寺,這世上最難降的妖魔是什么曼库? 我笑而不...
    開(kāi)封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮略板,結(jié)果婚禮上毁枯,老公的妹妹穿的比我還像新娘。我一直安慰自己叮称,他們只是感情好种玛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著颅拦,像睡著了一般蒂誉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上距帅,一...
    開(kāi)封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天右锨,我揣著相機(jī)與錄音,去河邊找鬼碌秸。 笑死绍移,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的讥电。 我是一名探鬼主播蹂窖,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼恩敌!你這毒婦竟也來(lái)了瞬测?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤纠炮,失蹤者是張志新(化名)和其女友劉穎月趟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恢口,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡孝宗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耕肩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片因妇。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖猿诸,靈堂內(nèi)的尸體忽然破棺而出婚被,到底是詐尸還是另有隱情,我是刑警寧澤梳虽,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布摔寨,位于F島的核電站,受9級(jí)特大地震影響怖辆,放射性物質(zhì)發(fā)生泄漏是复。R本人自食惡果不足惜删顶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望淑廊。 院中可真熱鬧逗余,春花似錦、人聲如沸季惩。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)画拾。三九已至啥繁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間青抛,已是汗流浹背旗闽。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蜜另,地道東北人适室。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像举瑰,于是被迫代替她去往敵國(guó)和親捣辆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355