講明白Spring Data JPA實體關聯註解

寫在前面

如果覺得有所收穫,記得點個關注和點個贊哦,非常感謝支持
Spring Data JPA是Spring Data系列一個較大的模塊,可輕鬆實現基於JPA的存儲庫。該模塊對處理基於JPA的數據訪問層進行了增強支持。它使構建Spring支持的應用程序訪問數據變得更加容易。我們知道,Spring實現應用程序的數據訪問層已經很長一段時間了。通常爲了執行簡單查詢以及執行分頁和審覈,必須編寫太多樣板代碼。Spring Data JPA旨在通過將工作量減少到實際需要的數量來顯着改善數據訪問層的實現。作爲開發人員,我們只需要將編寫包括自定義finder方法在內的存儲庫接口,Spring會自動提供實現。如果需要了解更多Spring Data JPA的相關知識,可以到官網文檔進行了解,這一篇文章主要是講解@ManyToMany,@OneToMany等註解的使用問題。

相關注解含義

我們都知道,Spring Data JPA默認使用的是Hibernate作爲ORM框架,而Hibernate作爲關係型數據庫,就會涉及到不同表之間的連接問題,到這裏很多人就會說了,不同表之間的連接問題,不就可以通過實體之間的業務邏輯關係進行控制嘛,這個問題有啥好講的。通過業務邏輯進行控制沒有錯,不過我們想一想,處理表之間的連接的代碼邏輯基本相同,難道每個需要進行處理的表之間,我們都需要寫一篇相同的代碼麼?當然不是,不要忘了,Spring Data JPA旨在通過將工作量減少到實際需要的數量來顯着改善數據訪問層的實現,所以我們可以通過註解的方式規定實體之間的關係,在實體類加載之初就加載好了數據。

注意,本篇文章講解的相關注解是Spring Data JPA的默認Hibernate 的註解,如果你使用的是Mybatis,不適用與本篇文章的相關內容

註解說明

在正式講解用法之前,我們先來了解一下相關注解的含義,這樣有助於我們後面講解使用的時候,理解代碼,當然,也可以直接跳過這一部分,需要的時候再自己查。

  • @Transient:@Transient表示該屬性並非一個到數據庫表的字段的映射,ORM框架將忽略該屬性。如果一個屬性並非數據庫表的字段映射,就務必將其標示爲@Transient,否則ORM框架默認其註解爲@Basic。
  • @JsonIgnoreProperties:此註解是類註解,作用是json序列化時將java bean中的一些屬性忽略掉,序列化和反序列化都受影響。
  • @JsonIgnore:此註解用於屬性或者方法上(最好是屬性上),作用和上面的@JsonIgnoreProperties一樣。
  • @JsonFormat:此註解用於屬性或者方法上(最好是屬性上),可以方便的把Date類型直接轉化爲我們想要的模式,比如@JsonFormat(pattern = “yyyy-MM-dd HH-mm-ss”)
  • @JsonSerialize:此註解用於屬性或者getter方法上,用於在序列化時嵌入我們自定義的代碼,比如序列化一個double時在其後面限制兩位小數點。
  • 關聯關係註解:關聯關係註解包括@JoinColumn、@OneToOne、@OneToMany、@ManyToOne、@ManyToMany、@JoinTable、@OrderBy。
  • Cascade 級聯關係:實際業務中,我們通常會遇到以下情況:用戶和用戶的收貨地址是一對多關係,當用戶被刪除時,這個用戶的所有收貨地址也應該一併刪除。訂單和訂單中的商品也是一對多關係,但訂單被刪除時,訂單所關聯的商品肯定不能被刪除。此時只要配置正確的級聯關係,就能達到想要的效果。級聯關係類型:
    • CascadeType.REFRESH:級聯刷新,當多個用戶同時作操作一個實體,爲了用戶取到的數據是實時的,在用實體中的數據之前就可以調用一下refresh()方法
    • CascadeType.REMOVE:級聯刪除,當調用remove()方法刪除Order實體時會先級聯刪除OrderItem的相關數據
    • CascadeType.MERGE:級聯更新,當調用了Merge()方法,如果Order中的數據改變了會相應的更新OrderItem中的數據
    • CascadeType.ALL:包含以上所有級聯屬性
    • CascadeType.PERSIST:級聯保存,當調用了Persist() 方法,會級聯保存相應的數據

下面的重點介紹是OneToMany和ManyToMany,因爲這兩種映射關係用的比較多,其他的映射關係也會略帶的講一下。

理解和使用

首先我們需要知道,java和jpa 中所有的關係都是單向的,在這一點上和關係數據庫有所不同,對於關係數據庫,可以通過外鍵定義並查詢,使得反向查詢總是存在的。JPA還定義了一個OneToMany關係,它與ManyToMany關係類似,但反向關係(如果已定義)是ManyToOne關係。OneToMany與JPA中ManyToMany關係的主要區別在於,ManyToMany總是使用中間關係連接表來存儲關係,OneToMany可以使用連接表或者目標對象的表引用中的外鍵源對象表的主鍵,像如下這樣。

@OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "OPR_WARE_SYSCONFIG_ID",foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private List<WarehouseVO> warehouse;

上面只是讓你嚐嚐鮮,不理解沒關係,這裏多說一句,ORM( Object-Relational Mapping ),對象關係映射,我們平常操縱的數據庫都是關係型數據庫(關係:表與表之間存在關係,例如從【部門表】可以查到【員工表】),術語一些: ORM,把關係數據庫的表結構映射到對象上。接下來,我們來理清一些概念,從大類上分,有兩類:數據庫、實體。下面舉個例子,如下:

  • 數據庫:表 + 字段
├── 部門表
│   ├── 部門ID (字段)
│   └── 部門名(字段)
└── 員工表
    ├── 員工ID (字段)
    └── 員工名(字段)
  • 實體:類 + 屬性
├── 部門類
│   ├── 部門ID (屬性)
│   └── 部門名(屬性)
└── 員工類
    ├── 員工ID (屬性)
    └── 員工名(屬性)

數據庫中,表之間的關聯,是通過外鍵做到的,具體內容我們不討論,這個去看數據庫和 SQL 相關。實體中,類之間的關聯,可以通過註解來實現,這是我們這篇要詳細討論的。

註解是 JDK 1.5 引入的內容,原理基於反射,使用起來非常方便,Spring 中大量使用,具體不細敘。使用註解開發實體關聯,有兩種途徑:

  • @OneToOne、@OneToMany、@ManyToOne、@ManyToMany 這四個註解是一類。除了 @ManyToOne 這個註解之外,其他三個註解都有 mappedBy 屬性,用於關聯實體。
  • @JoinColumn 註解,用於關聯實體。(但是還是要加上 @OneToOne 等關聯註解的,兩個註解一起使用)

這裏要說明一下了,上面這兩種填寫屬性的方式是不同的

  • mappedBy :類層面,關聯的全都是【實體類】中的【屬性名】
// departmentId :類中的屬性名
@OneToOne(mappedBy = "departmentId")

  • @JoinColumn:字段層面,關聯的全都是【表】中的【字段名】
// department_id :表中的字段名
@JoinColumn(name = "department_id")

多說一句, @Table、 @Column、 @JoinTable、 @JoinColumn、@JoinColumns,這些都是一類的,填寫的全都是數據庫中的字段名(下劃線命名法的那些)。

在對註解正式講解之前呢,我們先建兩個實體類,Department 和 Employee ,各有 id 和 name 兩個屬性,如下。

@Data
@Entity
@Table(name = "pz_department")
public class Department implements Serializable { // 部門類

    @Id
    @Column(name = "department_id")
    private String departmentId;

    @Column(name = "department_name")
    private String departmentName;

}
@Data
@Entity
@Table(name = "pz_employee")
public class Employee implements Serializable { // 員工類

    @Id
    @Column(name = "employee_id")
    private String employeeId;

    @Column(name = "employee_name")
    private String employeeName;

}

後續代碼將會對上面這部分共同代碼略寫。

@OneToOne 一對一映射

場景:一個部門裏只有一個員工,同樣的,一個員工只屬於一個部門。

@JoinColumn

public class Department implements Serializable { // 部門實體
    // ...(省略)
    @OneToOne
    @JoinColumn(name = "own_employee_name", referencedColumnName = "employee_name")
    private Employee ownEmployee;
}

註解中的屬性:

  • name :【己方】實體的【數據庫字段】
  • referencedColumnName :【對方】實體的【數據庫字段】(如果是主鍵,可以省略)

在這裏插入圖片描述

@OneToOne(mappedBy = “…”)

public class Department implements Serializable { // 部門實體
    // ...(省略)
    @OneToOne
    @JoinColumn(name = "own_employee_id")
    private Employee ownEmployee;
}

public class Employee implements Serializable { // 員工實體
    // ...(省略)
    @OneToOne(mappedBy = "ownEmployee")
    private Department belongDepartment;
}

對於 @OneToOne 註解,mappedBy 只有一種使用方法,那就是對方先關聯自己,自己反關聯回去。(因此無法通過 mappedBy 來實現一對一的單向關聯,如若一對一關係使用 mappedBy ,必定是雙向關聯)。上面的代碼實現了這樣的功能:【部門類】首先關聯了【員工類】(通過 @JoinColumn 註解),把員工作爲自己的一個屬性。【員工類】通過 mappedBy 反關聯回去【部門類】,其中 mappedBy 所指向的值,就是部門類已經關聯好的員工類屬性。換句話說,一對一的關聯關係是由【部門類】所創建和維護的,mappedBy 自身不關聯,它只是順着這層已經存在的單層關聯,順藤摸瓜地反關聯回去。
在這裏插入圖片描述

@OneToMany 一對多映射

場景:一個部門裏有多個員工。

@JoinColumn

public class Department implements Serializable { // 部門實體
    // ...(省略)
    @OneToMany
    @JoinColumn(name = "employee_name", referencedColumnName = "own_employee_id")
    private List<Employee> ownEmployeeList;
}

註解中的屬性:

  • name :【對方】實體的【數據庫字段】
  • referencedColumnName :【己方】實體的【數據庫字段】(如果是主鍵,可以省略)
    (發現了嗎,剛好是跟一對一關係是反過來的)
    在這裏插入圖片描述
    這是個很有意思的事情,爲什麼這裏反過來了呢?這個我們在後文的分析中再討論。

@OneToMany(mappedBy = “…”)

一對多的情況下,mappedBy 有兩種使用方式。

  • 跟一對一關聯一樣,首先對面已經關聯好自己,自己只需要反向關聯回去即可,mappedBy 的值是自己在對方類中的屬性名。(在這種情況下,必須雙向關聯)
public class Department implements Serializable {
    // ...(省略)
    @OneToMany(mappedBy = "employeeName") // 匹配自己在對方的實體屬性
    private List<Employee> ownEmployeeList;
}
  • 無需對方關聯,直接去關聯對方的外鍵屬性。(在這種情況下,雖然使用了 mappedBy ,但是依舊是單向關聯)
public class Department implements Serializable {
    // ...(省略)
    @OneToMany(mappedBy = "departmentId") // 匹配對方的外鍵
    private List<Employee> ownEmployeeList;
}

但是這樣單向關聯有一個前提:對方的外鍵關聯自己時,必須關聯自己的主鍵。比較簡單,就不畫圖了。

@ManyToOne 多對一映射

場景:多個員工歸屬於同一個部門。

@JoinColumn

public class Employee implements Serializable { // 員工實體
    // ...(省略)
    @ManyToOne
    @JoinColumn(name = "belong_department_name", referencedColumnName = "department_name")
    private Department belongDepartment;
}

註解中的屬性:

  • name :【己方】實體的【數據庫字段】
  • referencedColumnName :【對方】實體的【數據庫字段】(如果是主鍵,可以省略)

多對一關聯( ManyToOne ),和一對一關聯( OneToOne ),在使用 @JoinColumn 時,是一模一樣的。也就是說,一對一、多對一的關聯,和一對多的關聯,在 name 和 referencedColumnName 上,是剛好相反的,這個我們一會分析。

mappedBy

@ManyToOne 不存在 mappedBy 屬性。因爲 mappedBy 的原理是把關聯的任務交給對面去做,員工有N個,部門只有1個,員工讓部門去維護關聯,一個部門是無法同時關聯N個員工的,因此不存在 mappedBy 屬性。

@ManyToMany 多對多映射

場景:一個部門內有多個員工,但是同時,一個員工也可以屬於多個部門。

@JoinTable

public class Department implements Serializable { // 部門實體
    // ...(省略)
    @ManyToMany
    @JoinTable(name = "pz_ref",
            joinColumns = {@JoinColumn(name = "ref_department_id")},
            inverseJoinColumns = {@JoinColumn(name = "ref_employee_id")})
    @JSONField(serialize = false)
    private List<Employee> employeeList;
}

多對多關聯,是需要自己建一張中間表的。粗略一想就會發現,多對多,雙方都是多,無法實現一方用外鍵關聯另一方,所以必須有中間表。(但是不需要爲這張中間表創建實體類)。除了新增一張表之外,連註解也發生了改變。原先是 @JoinColumn ,join 到字段中,現在是 @JoinTable ,join 到表中。

註解中的屬性:

  • name :【中間表】的【表名】
  • joinColumns :【己方表】與【中間表】關聯(按 @OneToMany 的方式來)
  • inverseJoinColumns:【對方表】與【中間表】關聯(按 @OneToMany 的方式來)

在這裏插入圖片描述

@ManyToMany(mappedBy = “…”)

public class Employee implements Serializable { // 員工實體
    // ...(省略)
    @ManyToMany(mappedBy = "employeeList")
    private List<Department> departmentList;
}

只有一種使用方法,跟一對一關聯( OneToOne )、一對多關聯( OneToMany )都具有的使用方法一致:mappedBy 屬性值是自己在對面實體類中的屬性名,即必須雙向映射。

具體分析

四類關聯:一對一、一對多、多對一、多對多,已經都走過一遍了。現在分析一下兩種註解方式( @JoinTable 就懶得提了)。

@JoinColumn

我所理解的 @JoinColumn ,它的本質是 @Column ,從本質上來說,它並不是在做關聯,它是在做映射,它把【數據庫】和【實體】映射起來,使用這類註解能夠實現:數據庫中的一個字段,對應着,實體類中的一個字段。所以,它在做的事情,並不是把【部門】和【實體】關聯起來,而是把【表】和【實體類】映射起來(但是與此同時,也就關聯起來了兩個實體)

@OneToOne
@JoinColumn(name = "自己", referencedColumnName = "對方")

@OneToMany
@JoinColumn(name = "對方", referencedColumnName = "自己")

@ManyToOne
@JoinColumn(name = "自己", referencedColumnName = "對方")

剛纔我們發現,【1 - 1】、【N - 1】的使用方法是相同的,但是【1 - N】剛好反了過來,這是爲什麼。是因爲,@JoinColumn 根本就不關心它所在的實體類是誰,它的 name 屬性指向的,永遠都是外鍵。因爲外鍵始終在【多】的一方(一對一的話就默認自己是多),因此 name 屬性值爲【多的一方的外鍵】。有關 @JoinColumn 自動建表的事情,我還沒有弄清楚。

mappedBy = “…”

mappedBy 通常出現,都是爲了做雙向關聯,而且對於 @OneToOne 和 @ManyToOne 而言,mappedBy 只能做雙向關聯。我們在文章開頭就指出,mappedBy 是針對【實體類】而做操作的,它的值是本類在對方類的屬性名。我們再理一遍,它要等對方關聯自己之後,自己順着這層【已經建立起來的聯繫】,反關聯回去。

這麼做的道理是,A 關聯 B,B 不應該再去建立新的關聯關係,去重新關聯 A(當然你硬要這麼做也可以),而應該根據 A 關聯 B 的這層關係,自動地找回去。這叫做:

本類放棄控制關聯關係,關聯由對方去控制。

很奇怪的一件事是,對於三種能使用 mappedBy 屬性的註解: @OneToOne 、@OneToMany 、 @ManyToOne ,它們有一種統一的使用方法(即本類在對方類的屬性名)。但是對於 @OneToMany ,它有第二種使用方法,它彷彿可以不需要對面先建立聯繫,直接使用 mappedBy 指向對方類的外鍵屬性。

這樣做的原理是,依舊讓對方維護關聯關係,但是必須由對方的【外鍵】關聯己方的【主鍵】(如果使用 @JoinColumn 可以由對方的【外鍵】關聯己方的【任意鍵】)。

也就是說,在一對多的關係中,【一方】想去關聯【多方】,但是又不想自己去維護關聯關係(因爲一對多時,維護關聯關係的話,代碼會自動地創建出來一張新表),因此【一方】使用 mappedBy 讓對面來處理關聯關係。對面是怎麼做關聯的呢,是通過外鍵關聯主鍵的方式關聯的。

使用中碰到的問題

駝峯命名法和下劃線命名法的自動轉換

我還沒查清具體的原因,是 Spring 框架還是 hibernate ,總之現在框架能自動把數據庫中的【下劃線命名法】映射到實體類中的【駝峯命名法】。例如,正常來講,實體類中的屬性應該要通過 @Column 配置映射關係。

@Table(name = "pz_department")
public class Department implements Serializable {

    // 這裏通過 @Column 註解
    // 將部門表中的【department_name】字段映射到部門類中的【departmentName】
    @Column(name = "department_name")
    private String departmentName;

}

但是實際上,就是不加 @Column 註解,框架也能自動映射。

@Table(name = "pz_department")
public class Department implements Serializable {

    // 部門表中的【department_name】字段,自動映射到部門類中的【departmentName】
    // 框架能夠自動將下劃線命名,轉換爲駝峯命名
    private String departmentName;

}

但是!一定不要這麼做!因爲在做關聯時,有可能會生成新表,如果之前沒有加 @Column 註解映射到數據庫的話,新表的字段,將不會是原表中的字段名(下劃線命名),而將是實體類中的屬性名(駝峯命名),這時再去做關聯,會報錯。

Caused by: org.hibernate.MappingException: Unable to find column with logical name: employee_name in org.hibernate.mapping.Table(pz_employee) and its related supertables and secondary tables

報錯信息:【employee_name】字段,在【pz_employee】表以及其他相關表中找不到。報錯原因:因爲在其他相關表(自動創建)中,字段名是【employeeName】。

進行雙向關聯時,循環打印

部門關聯員工,員工關聯回部門,部門再關聯回員工……程序運行本身不會出現問題,但是如果打印出來,就會造成關聯上的死循環,直至溢出。想要解決的話,就在其中一個類的該屬性上加上 JSON 相關的註解,讓這個屬性不進行序列化。例如通過 fastjson 中的 @JSONField(serialize = false) 註解,或者@JsonIgnore。

@JSONField(serialize = false)
private Department department;

@JoinColumn(name = “…”) 屬性映射不能重複

上文中分析過,@JoinColumn 註解本質上是對數據庫和實體類進行映射。如果某一數據庫中的字段,已經映射到某屬性上了,在 @JoinColumn 中的 name 屬性裏再次映射,就會出現問題:到底映射的是哪一個呢?

@Column(name = "belong_department_id")
private String belongDepartmentId;

@ManyToOne
@JoinColumn(name = "belong_department_id", referencedColumnName = "department_id")
private Department belongDepartment;

例如上面這段代碼,就會報錯:

Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: com.app.ykym.modules.test.entityAndRepository.Employee column: belong_department_id (should be mapped with insert="false" update="false")

解決方法在報錯信息裏也說明了:重複映射的兩個屬性,選一個,讓它 insert=“false” update=“false” (寫在註解裏),意思是讓其中一個屬性放棄更新和插入數據庫的權限。(但是 @OneToMany 時,@JoinColumn(name = “…”) 是可以重複的)

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