JPA 與 DDD 的==聚合寫== 是絕配,但在 “讀” 場(chǎng)景 往往會(huì)引發(fā)各種性能問題。這也是很多公司棄用 JPA 而選擇 MyBatis 的主要原因,就其本質(zhì)并不是框架的錯(cuò),而是將框架用在了錯(cuò)誤的場(chǎng)景。
1. 初始 Repository
在 DDD 中,Repository 是一個(gè)非常重要的概念,它是領(lǐng)域?qū)拥囊粋€(gè)組件,用來管理聚合根的生命周期和持久化。
1.1. 核心為狀態(tài)管理
DDD 是由領(lǐng)域?qū)ο蟪休d業(yè)務(wù)邏輯,所有的業(yè)務(wù)操作均在模型對(duì)象上完成,同一聚合上不同的業(yè)務(wù)操作構(gòu)成了聚合的生命周期。
我們以訂單為例,如下圖所示:
- 首先,用戶操作下單,使用提交數(shù)據(jù)為其創(chuàng)建一個(gè) Order 對(duì)象,版本 V1;
- 隨后,用戶進(jìn)行改地址操作,調(diào)用 Order 對(duì)象的 modifyAddress 方法,Order 從原來的 V1 變成 V2;
- 用戶完成支付后,調(diào)用 Order 對(duì)象的 paySuccess 方法,Order 從 V2 變成 V3;
整個(gè)流程很好的體現(xiàn)了 Order 聚合根的生命周期。
1.2. 為什么需要 Repository?
假設(shè),有一臺(tái)非常牛逼的計(jì)算機(jī),計(jì)算資源無限、內(nèi)存大小無限、永不掉電、永不宕機(jī),那最簡(jiǎn)單高效的方式便是將模型對(duì)象全部放在內(nèi)存中。
但,現(xiàn)實(shí)不存在這樣的機(jī)器,我們不得不將內(nèi)存對(duì)象寫入磁盤,下次使用時(shí),在將其從磁盤讀入到內(nèi)存。
整體結(jié)構(gòu)如下圖所示:
和上圖相比,具有如下特點(diǎn):
- 業(yè)務(wù)操作沒變,仍舊依次完成 下單、改地址、支付等操作
- 引入持久化存儲(chǔ)(MySQL),可以將 Order 對(duì)象存儲(chǔ)于關(guān)系數(shù)據(jù)庫(kù)
- 配合 Order 的生命周期,操作中增加 save、load 和 update 等操作
- 用戶下單創(chuàng)建 Order 對(duì)象,通過 save 方法將 Order 對(duì)象持久化到 DB
- 接收到業(yè)務(wù)操作,需執(zhí)行l(wèi)oad,從 DB 加載數(shù)據(jù)到內(nèi)存 并對(duì) Order 對(duì)象的狀態(tài)進(jìn)行恢復(fù)
- 在業(yè)務(wù)操作完成后,需執(zhí)行update,將 Order 對(duì)象的最新狀態(tài)同步的 DB
相對(duì)全內(nèi)存版本確實(shí)增加了不小的復(fù)雜性,為了更好的對(duì)這些復(fù)雜性進(jìn)行管理,引入 Repository 模式。
在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)中,Repository 是一種設(shè)計(jì)模式,它是用來存儲(chǔ)領(lǐng)域?qū)ο蟮娜萜鳌K峁┝艘环N統(tǒng)一的方式來查詢和存儲(chǔ)領(lǐng)域?qū)ο蟆epository提供了對(duì)底層數(shù)據(jù)存儲(chǔ)的抽象,允許應(yīng)用程序在沒有直接與數(shù)據(jù)存儲(chǔ)技術(shù)交互的情況下訪問數(shù)據(jù),同時(shí)該抽象允許在不修改應(yīng)用程序代碼的情況下更改數(shù)據(jù)存儲(chǔ)技術(shù)。
【注】在 DDD 中,Repository 并不是一個(gè) DAO,它的職責(zé)比 DAO 要多得多,它管理的是整個(gè)聚合根,而不是單個(gè)實(shí)體對(duì)象。同時(shí),Repository 還需要提供一些查詢接口,用來查詢聚合根的狀態(tài)。
2. 什么是好的 Repository?
Repository 主要用于完成對(duì)聚合根生命周期的管理,所以必須提供三組操作:
- 保存。將聚合根同步到底層存儲(chǔ)進(jìn)行持久化處理;
- 查詢。根據(jù) ID 或?qū)傩詮牡讓哟鎯?chǔ)引擎中讀取數(shù)據(jù)并恢復(fù)為內(nèi)存對(duì)象,也就是聚合根對(duì)象;
- 更新。聚合對(duì)象發(fā)生變更后,可以將新的狀態(tài)同步到存儲(chǔ)引擎,以便完成數(shù)據(jù)更新;
有人會(huì)說,這和 DAO 沒啥區(qū)別吧!!!
DAO 是單表單實(shí)體操作,Repository 操作的是整個(gè)聚合甚至包括繼承關(guān)系,這就是最大的區(qū)別。也就是Repository 必須能夠:
- 維護(hù)一個(gè)完整的對(duì)象組,也就是必須能處理對(duì)象的組合關(guān)系;
- 維護(hù)一個(gè)完整的繼承體系,也就是必須能夠處理對(duì)象繼承關(guān)系;
既支持組合又支持繼承,DAO 就沒辦法更好的承載了。
那就完了嗎?并沒有!!!
聚合根是一個(gè)對(duì)象組,包含各種關(guān)系,并不是每個(gè)業(yè)務(wù)操作都需要聚合根內(nèi)的所有實(shí)體。
舉個(gè)例子,在電商訂單聚合根內(nèi),包括:
- 訂單(Order)。記錄用戶的一次生單,主要保存用戶、支付金額、訂單狀態(tài)等;
- 訂單項(xiàng)(OrderItem)。購(gòu)買的單個(gè)商品,主要保存商品單價(jià)、售價(jià)、應(yīng)付金額等;
- 支付記錄(Pay)。用戶的支付信息,包括支付渠道、支付金額、支付時(shí)間等;
- 收貨地址(Address)。用戶的收貨地址;
在改價(jià)流程里,需要修改 Order、OrderItem、Pay 三組實(shí)體。
在更新地址流程里,僅需要修改 Address 和 Order 兩組實(shí)體。
為了滿足不同的業(yè)務(wù)場(chǎng)景,Repository 需要具備兩個(gè)高級(jí)特性:
- 延遲加載。只有在第一次訪問關(guān)聯(lián)實(shí)體時(shí)才對(duì)其進(jìn)行加載,避免過早加載但實(shí)際上并沒有使用所造成資源浪費(fèi)問題;
- 按需更新。不管加載了多少組實(shí)體,在保存時(shí)僅對(duì)發(fā)生變更的實(shí)體進(jìn)行更新,減少對(duì)底層存儲(chǔ)引擎的操作次數(shù),從而提升性能;
總體來說,能夠具備以下特性的 Repository 才是好的 Repository:
- 支持組合關(guān)系
- 支持繼承關(guān)系
- 支持延遲加載
- 支持按需更新
3. JPA 實(shí)例
綜合調(diào)研各類 ORM 框架,只有 JPA 具備上述特性,而且和 DDD 是絕配。
3.1. 組合關(guān)系
組合是一種面向?qū)ο缶幊痰闹匾拍睿敢粋€(gè)類的對(duì)象可以將其他類的對(duì)象作為自己的組成部分。組合在DDD中使用場(chǎng)景最為廣泛,這也是聚合的主要工作方式。也就是將一組對(duì)象保存到存儲(chǔ)引擎,然后在從存儲(chǔ)引擎中獲取完整的對(duì)象組。
從數(shù)據(jù)視角,組合關(guān)系存在兩個(gè)維度:
- 數(shù)量維度。指關(guān)聯(lián)關(guān)系兩端對(duì)象的數(shù)量,包括
- 一對(duì)一:一個(gè)實(shí)體對(duì)象只能關(guān)聯(lián)到另一個(gè)實(shí)體對(duì)象,例如 公司 和 營(yíng)業(yè)執(zhí)照,一個(gè)公司只會(huì)有一個(gè)營(yíng)業(yè)執(zhí)照;
- 一對(duì)多:一個(gè)實(shí)體對(duì)象可以關(guān)聯(lián)到多個(gè)實(shí)體對(duì)象,例如 訂單 和 訂單項(xiàng),一個(gè)訂單關(guān)聯(lián)多個(gè)訂單項(xiàng);
- 多對(duì)一:多個(gè)實(shí)體對(duì)象可以關(guān)聯(lián)到同一個(gè)實(shí)體對(duì)象,例如 訂單項(xiàng) 和 訂單,一個(gè)訂單項(xiàng)只屬于一個(gè)訂單;
- 多對(duì)多:多個(gè)實(shí)體對(duì)象可以互相關(guān)聯(lián),例如 社團(tuán) 和 學(xué)生,一個(gè)社團(tuán)包含多個(gè)學(xué)生,一個(gè)學(xué)生也可以參加多個(gè)社團(tuán);
- 方向維度。指對(duì)象的引用關(guān)系
- 單向關(guān)聯(lián),只能從一端訪問另一端,比如 訂單存在訂單項(xiàng)的引用,訂單項(xiàng)沒有到訂單的引用;
- 雙向關(guān)聯(lián),可以互相訪問,訂單存在訂單項(xiàng)的引用,訂單項(xiàng)也有到訂單的引用;
兩者組合,情況更加復(fù)雜,會(huì)產(chǎn)生:
- 單向多對(duì)一
- 雙向多對(duì)一
- 單向一對(duì)多
- 雙向一對(duì)多
- 單向一對(duì)一
- 雙向一對(duì)一
聚合根是一組對(duì)象訪問的入口,聚合內(nèi)的所有操作都必須通過聚合根進(jìn)行,所以,聚合根于其他實(shí)體的關(guān)系只能是 一對(duì)多 和 一對(duì)一;同時(shí),所有的業(yè)務(wù)操作都是從聚合根發(fā)起,通過聚合根能關(guān)聯(lián)到內(nèi)部實(shí)體即可,因此也不存在雙向。綜上所述,DDD 對(duì)組合進(jìn)行了大量簡(jiǎn)化,實(shí)際工作中主要涉及:
- 單向一對(duì)一
- 單向一對(duì)多
3.1.1. 單向一對(duì)一
通過外鍵的方式實(shí)現(xiàn)單向一對(duì)一關(guān)系,需要在主表中添加一個(gè)指向另一個(gè)表的外鍵,通過外鍵信息獲取關(guān)聯(lián)數(shù)據(jù)。
實(shí)體如下:
// 聚合根實(shí)現(xiàn)
@Entity
@Table(name = "order_info")
public class Order{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 增加 @.NEToOne 注解
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Pay pay;
// 增加 @OneToOne 注解
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Address address;
// 忽略其他屬性
}
// Pay 實(shí)體實(shí)現(xiàn)
@Entity
@Table(name = "pay_info")
public class Pay {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 忽略其他屬性
}
// Address 實(shí)現(xiàn)
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 忽略其他屬性
}
插入記錄后,order_Infor 表數(shù)據(jù)如下
其中:
- address_id 存儲(chǔ)的是 Address 實(shí)體的主鍵;
- pay_id 存儲(chǔ)的事 Pay 實(shí)體的主鍵;
其中,插入數(shù)據(jù)的sql如下:
Hibernate: insert into address (detail) values (?)
Hibernate: insert into pay_info (order_id, price) values (?, ?)
Hibernate: insert into order_info (address_id, pay_id, status, total_price, total_selling_price, user_id) values (?, ?, ?, ?, ?, ?)
可見,執(zhí)行時(shí)先插入 address 和 pay 獲取主鍵后,在插入到 order_info 表,從而維護(hù)外鍵的有效性。
3.1.2. 單向一對(duì)多
實(shí)體定義如下:
// 聚合根實(shí)體
@Entity
@Table(name = "order_info")
public class Order{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 添加 @OneToMany 注解
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
// 指定多端的關(guān)聯(lián)列(如果不指定,會(huì)使用第三張表來保存關(guān)系
@JoinColumn(name = "order_id")
private List<OrderItem> orderItems = new ArrayList<>();
// 忽略其他屬性
}
// OrderItem 實(shí)現(xiàn)
@Entity
@Table(name = "order_item")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 忽略其他屬性
}
插入記錄后,表數(shù)據(jù)如下:
order 表數(shù)據(jù):
order+item表數(shù)據(jù):
其中 order_item 表中的 order_id 指向 order_info 表的主鍵。
3.2. 繼承關(guān)系
繼承是面向?qū)ο缶幊痰暮诵奶匦裕@一特性確與數(shù)據(jù)庫(kù)的關(guān)系模型產(chǎn)生巨大阻抗。
JPA 中提供了三種繼承模型,包括:
- 單表繼承策略(SINGLE_TABLE)。父類實(shí)體和子類實(shí)體共用一張數(shù)據(jù)庫(kù)表,在表中通過一列辨別字段來區(qū)別不同類別的實(shí)體;
- Joined 策略(JOINED)。父類和子類分別對(duì)應(yīng)一張表,父類對(duì)應(yīng)的表中只有父類自己的字段,子類對(duì)應(yīng)的表中中有自己的字段和父類的主鍵字段,兩者間通過 Join 方式來處理關(guān)聯(lián);
- 每個(gè)實(shí)體一個(gè)表策略(TABLE_PER_CLASS)。每個(gè)實(shí)體對(duì)應(yīng)一個(gè)表,會(huì)生成多張表,父類對(duì)應(yīng)的表只有自己的字段。子類對(duì)應(yīng)的表中除了有自己特有的字段外,也有父類所有的字段。
為了更好的對(duì)比各種策略,我們以一個(gè)業(yè)務(wù)場(chǎng)景為案例進(jìn)行分析。
在優(yōu)惠計(jì)算過程中,需要根據(jù)不同的配置策略對(duì)當(dāng)前用戶進(jìn)行驗(yàn)證,以判斷用戶是否能夠享受優(yōu)惠,常見的驗(yàn)證策略有:
- 只有特定用戶才能享受。
- 只有男士或女士才能享受。
- 只有VIP特定等級(jí)才能享受。
- 未來還有很多
為了保障系統(tǒng)有良好的擴(kuò)展性,引入策略模式,整體設(shè)計(jì)如下:
那接下來便是將這些實(shí)現(xiàn)類存儲(chǔ)到數(shù)據(jù)庫(kù),然后在方便的查詢出來。
3.2.1. 單表繼承
單表繼承非常簡(jiǎn)單,也最為實(shí)用,數(shù)據(jù)庫(kù)表只有一張,通過一列辨別字段來區(qū)別不同類別的實(shí)體。
它的使用涉及幾個(gè)關(guān)鍵注解:
- @Inheritance(strategy = InheritanceType.SINGLE_TABLE),添加在父類實(shí)體,用于說明當(dāng)前使用的是 單表策略;
- @DiscriminatorColumn(name="區(qū)分類型存放的列名"),添加在父類實(shí)體,用于說明使用哪個(gè)列來區(qū)分具體類型;
- @DiscriminatorValue(value = "當(dāng)前類型的標(biāo)識(shí)") 添加到子類實(shí)體上,用于說明當(dāng)前子類的具體類型;
相關(guān)實(shí)體代碼如下:
// 父類
@Entity
// 單表表名
@Table(name = "activity_matcher")
// 當(dāng)前策略為單表策略
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
// activity_type 列用于存儲(chǔ)對(duì)應(yīng)的類型
@DiscriminatorColumn(name = "activity_type")
@Data
public abstract class BaseActivityMatcher implements ActivityMatcher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private ActivityMatcherStatus status = ActivityMatcherStatus.ENABLE;
}
// SpecifyUserMatcher 實(shí)現(xiàn)類
@Entity
// 使用 SpecifyUser 作為標(biāo)記
@DiscriminatorValue("SpecifyUser")
public class SpecifyUserMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
// SexMatcher 實(shí)現(xiàn)類
@Entity
// 使用 Sex 作為標(biāo)記
@DiscriminatorValue("Sex")
public class SexMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
@Entity
// 使用 VipLevel 作為標(biāo)記
@DiscriminatorValue("VipLevel")
public class VipLevelMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
每種策略保存一條數(shù)據(jù)后,數(shù)據(jù)庫(kù)表activity_matcher數(shù)據(jù)如下圖所示:
其中:
- activity_type 用于區(qū)分當(dāng)前數(shù)據(jù)對(duì)應(yīng)的策略類型;
- VipLevel類型下,只有 status 和 levels 生效,服務(wù)于 VipLevelMatcher,其他全部為 null;
- SpecifyUser 類型下,只有 status 和 user_ids 生效,服務(wù)于 SpecifyUserMatcher,其他全部為 null;
- Sex類型下,只有 status 和 sex 生效,服務(wù)于 SexMatcher,其他全部為 null;
單表繼承策略,最大的優(yōu)點(diǎn)便是簡(jiǎn)單,但由于父類實(shí)體和子類實(shí)體共用一張表,因此表中會(huì)有很多空字段,造成浪費(fèi)。
3.2.2. Joined 策略
Joined策略,父類實(shí)體和子類實(shí)體分別對(duì)應(yīng)數(shù)據(jù)庫(kù)中不同的表,子類實(shí)體的表中只存在其擴(kuò)展的特殊屬性,父類的公共屬性保存在父類實(shí)體映射表中。
它的使用涉及幾個(gè)關(guān)鍵注解:
- @Inheritance(strategy = InheritanceType.JOINED),添加在父類實(shí)體,用于說明當(dāng)前使用的是 Joined 策略;
- @PrimaryKeyJoinColumn(name="子類主鍵列名稱"),添加在子類實(shí)體,用于說明使用哪個(gè)列來關(guān)聯(lián)父類;
相關(guān)實(shí)體代碼如下:
// 父類
@Entity
@Table(name = "activity_joined_matcher")
// 當(dāng)前策略為Joined策略
@Inheritance(strategy = InheritanceType.JOINED)
@Data
public abstract class BaseActivityMatcher implements ActivityMatcher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private ActivityMatcherStatus status = ActivityMatcherStatus.ENABLE;
}
// SpecifyUserMatcher 實(shí)現(xiàn)類
@Entity
@Table(name = "user_joined_matcher")
@PrimaryKeyJoinColumn(name = "matcher_id")
public class SpecifyUserMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
// SexMatcher 實(shí)現(xiàn)類
@Entity(name = "JoinedSexMatcher")
@Table(name = "sex_joined_matcher")
@PrimaryKeyJoinColumn(name = "matcher_id")
public class SexMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
// VipLevelMatcher 實(shí)現(xiàn)類
@Entity(name = "JoinedVipLevelMatcher")
@Table(name = "vip_joined_matcher")
@PrimaryKeyJoinColumn(name = "matcher_id")
public class VipLevelMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
每種策略保存一條數(shù)據(jù)后,各個(gè)表數(shù)據(jù)如下:
activity_joined_matcher 如下:
user_joined_matcher 如下:
sex_joined_matcher 如下:
vip_joined_matcher 如下:
具有以下特點(diǎn):
- 主表存儲(chǔ)各個(gè)子類共享的父類數(shù)據(jù);
- 子表通過字段與主表相關(guān)聯(lián);
- 主表有所有子表的數(shù)據(jù),每個(gè)子表只有他特有的數(shù)據(jù);
從表數(shù)據(jù)上可以看出,Joined策略可以減少冗余的空字段,但是查詢時(shí)需要多表連接,效率較低。
3.2.3. 每個(gè)實(shí)體一個(gè)表策略
TABLE_PER_CLASS 策略,父類實(shí)體和子類實(shí)體每個(gè)類分別對(duì)應(yīng)一張數(shù)據(jù)庫(kù)中的表,子類表中保存所有屬性,包括從父類實(shí)體中繼承的屬性。
它的使用主要涉及以下幾個(gè)點(diǎn):
- @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS),添加在父類實(shí)體,用于說明當(dāng)前使用的是 TABLE_PER_CLASS 策略;
- @GeneratedValue(strategy = GenerationType.AUTO) 不要使用IDENTITY,需要保障每個(gè)子類的 id 都不重復(fù);
- 抽象父類不需要表與之對(duì)應(yīng),非抽象父類也需要表用于存儲(chǔ);
相關(guān)實(shí)體代碼如下:
// 父類
@Entity
// 當(dāng)前策略為 TABLE_PER_CLASS 策略
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Data
public abstract class BaseActivityMatcher implements ActivityMatcher {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// 省略屬性和方法
}
// SpecifyUserMatcher 實(shí)現(xiàn)類
@Entity
@Table(name = "user_per_class_matcher")
public class SpecifyUserMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
// SexMatcher 實(shí)現(xiàn)類
@Entity
@Table(name = "sex_per_class_matcher")
public class SexMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
// VipLevelMatcher 實(shí)現(xiàn)類
@Entity
@Table(name = "vip_per_class_matcher")
public class VipLevelMatcher
extends BaseActivityMatcher
implements ActivityMatcher {
// 省略屬性和方法
}
每種策略保存一條數(shù)據(jù)后,各個(gè)表數(shù)據(jù)如下:
user_per_class_matcher 如下:
sex_per_class_matcher 如下:
vip_per_class_matcher 如下:
具有以下特點(diǎn):
- 每個(gè)具體的子類對(duì)應(yīng)一張表,表中存儲(chǔ)父類和子類的數(shù)據(jù);
- 為每個(gè)子類生成id,所生成的 id 不重復(fù);
從表數(shù)據(jù)上可以看出,子類中有相同的屬性,則每個(gè)子類都需要?jiǎng)?chuàng)建一遍,會(huì)導(dǎo)致表結(jié)構(gòu)冗余,影響查詢效率。
3.2.4. 小節(jié)
三種策略各具特色,都有最佳應(yīng)用場(chǎng)景,簡(jiǎn)單如下:
- 單表策略。
子類的數(shù)據(jù)量不大,且與父類的屬性差別不大;
ß可以使用單表繼承策略來減少表的數(shù)量;
- Joined 策略。
- 子類的屬性較多,且與父類的屬性差別較大;
- 需要一個(gè)主表,用于對(duì)所有的子類進(jìn)行管理;
- 每個(gè)實(shí)體一個(gè)表策略。
- 子類的屬性較多,且與父類的屬性差別較大;
- 子類過于離散,無需統(tǒng)一管理;
當(dāng)子類過多或數(shù)據(jù)量過大時(shí),Joined 和 table per class 在查詢場(chǎng)景存在明顯的性能問題,這個(gè)需要格外注意。
3.3. 立即加載&延遲加載
JPA提供了兩種加載策略:立即加載和延遲加載。
- 一對(duì)一關(guān)聯(lián),默認(rèn)獲取策略是立即加載(EAGER),查詢一個(gè)對(duì)象,會(huì)把它關(guān)聯(lián)的對(duì)象都查出來初始化到屬性中;
- 一對(duì)多關(guān)聯(lián),默認(rèn)獲取策略是懶加載(LAZY),即只有在使用到相關(guān)聯(lián)數(shù)據(jù)時(shí)才會(huì)查詢數(shù)據(jù)庫(kù);
如果默認(rèn)策略不符合要求,可以通過手工設(shè)置注解上 fetch 配置,對(duì)默認(rèn)策略進(jìn)行重寫。
3.3.1. 立即加載
立即加載會(huì)在查詢主實(shí)體類的同時(shí)查詢它所有關(guān)聯(lián)實(shí)體類,并綁定到實(shí)體屬性上。
立即加載的好處是能夠提高查詢效率,因?yàn)椴恍枰~外的查詢操作。但是,使用立即加載會(huì)增加數(shù)據(jù)庫(kù)的查詢負(fù)擔(dān),查詢出所有關(guān)聯(lián)實(shí)體類,會(huì)導(dǎo)致查詢結(jié)果的數(shù)據(jù)量比較大。
實(shí)體配置如下:
@Entity
@Table(name = "order_info")
public class Order{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "order_id")
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Pay pay;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Address address;
// 忽略其他屬性和方法
}
測(cè)試腳本如下:
Order order = this.orderRepository.findById(this.order.getId()).get();
Assertions.assertNotNull(order);
System.out.println("訪問 item");
Assertions.assertEquals(3, order.getOrderItems().size());
System.out.println("訪問 address");
Assertions.assertNotNull(order.getAddress().getDetail());
System.out.println("訪問 pay");
Assertions.assertNotNull(order.getPay().getPrice());
日志輸出如下:
Hibernate: select order0_.id as id1_3_0_, order0_.address_id as address_6_3_0_, order0_.pay_id as pay_id7_3_0_, order0_.status as status2_3_0_, order0_.total_price as total_pr3_3_0_, order0_.total_selling_price as total_se4_3_0_, order0_.user_id as user_id5_3_0_, address1_.id as id1_2_1_, address1_.detail as detail2_2_1_, orderitems2_.order_id as order_id6_4_2_, orderitems2_.id as id1_4_2_, orderitems2_.id as id1_4_3_, orderitems2_.price as price2_4_3_, orderitems2_.product_id as product_3_4_3_, orderitems2_.quantity as quantity4_4_3_, orderitems2_.selling_price as selling_5_4_3_, pay3_.id as id1_5_4_, pay3_.price as price2_5_4_ from order_info order0_ left outer join address address1_ on order0_.address_id=address1_.id left outer join order_item orderitems2_ on order0_.id=orderitems2_.order_id left outer join pay_info pay3_ on order0_.pay_id=pay3_.id where order0_.id=?
訪問 item
訪問 address
訪問 pay
從日志輸出可見:
- JPA 使用多張表的join,通過一個(gè)復(fù)雜的 sql 一次性獲取了所有數(shù)據(jù);
- 在訪問關(guān)聯(lián)實(shí)體時(shí),未觸發(fā)任何加載操作;
3.3.2. 延遲加載
延遲加載是指在進(jìn)行數(shù)據(jù)庫(kù)查詢時(shí),并不會(huì)立即查詢關(guān)聯(lián)表數(shù)據(jù),而是要等到使用時(shí)才會(huì)去查,這樣可以避免不必要的數(shù)據(jù)庫(kù)查詢,提高查詢效率。
延遲加載又分為兩種情況:
- 表間的延遲加載:在表關(guān)聯(lián)情況下,進(jìn)行數(shù)據(jù)庫(kù)查詢時(shí),并不會(huì)立即查詢關(guān)聯(lián)表,而是要等到使用時(shí)才會(huì)去查數(shù)據(jù)庫(kù);
- 表中屬性的延遲加載:比如大型字段blob,需要等到使用時(shí)才加載,這樣可以避免不必要的數(shù)據(jù)庫(kù)查詢,提高查詢效率;
在此,重點(diǎn)介紹表間關(guān)聯(lián)的延遲加載:
實(shí)體代碼如下所示:
@Entity
@Table(name = "order_info")
public class Order{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Pay pay;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Address address;
// 忽略其他字段和方法
}
查詢代碼如下:
Order order = this.orderRepository.findById(this.order.getId()).get();
Assertions.assertNotNull(order);
System.out.println("訪問 item");
Assertions.assertEquals(3, order.getOrderItems().size());
System.out.println("訪問 address");
Assertions.assertNotNull(order.getAddress().getDetail());
System.out.println("訪問 pay");
Assertions.assertNotNull(order.getPay().getPrice());
控制臺(tái)輸出如下:
Hibernate: select order0_.id as id1_3_0_, order0_.address_id as address_6_3_0_, order0_.pay_id as pay_id7_3_0_, order0_.status as status2_3_0_, order0_.total_price as total_pr3_3_0_, order0_.total_selling_price as total_se4_3_0_, order0_.user_id as user_id5_3_0_ from order_info order0_ where order0_.id=?
訪問 item
Hibernate: select orderitems0_.order_id as order_id6_4_0_, orderitems0_.id as id1_4_0_, orderitems0_.id as id1_4_1_, orderitems0_.price as price2_4_1_, orderitems0_.product_id as product_3_4_1_, orderitems0_.quantity as quantity4_4_1_, orderitems0_.selling_price as selling_5_4_1_ from order_item orderitems0_ where orderitems0_.order_id=?
訪問 address
Hibernate: select address0_.id as id1_2_0_, address0_.detail as detail2_2_0_ from address address0_ where address0_.id=?
訪問 pay
Hibernate: select pay0_.id as id1_5_0_, pay0_.price as price2_5_0_ from pay_info pay0_ where pay0_.id=?
從日志輸出可知,關(guān)聯(lián)實(shí)體只有在屬性被訪問時(shí)才會(huì)觸發(fā)自動(dòng)加載。
延遲加載在聚合更新時(shí)極為重要,面對(duì)一個(gè)大聚合,每次修改只會(huì)涉及少量相關(guān)聯(lián)的實(shí)體,由于延遲加載機(jī)制的保障,對(duì)于那些沒有必要訪問的實(shí)體并不會(huì)執(zhí)行實(shí)際的加載操作,從而大幅提升性能。
3.4. 按需更新
簡(jiǎn)單理解按需更新,就是只有在有必要時(shí)才會(huì)對(duì)數(shù)據(jù)進(jìn)行更新。
按需更新可以分為兩個(gè)場(chǎng)景:
- 只更新變更實(shí)體:在保存一組對(duì)象時(shí),只對(duì)狀態(tài)發(fā)生變化的實(shí)體進(jìn)行更新;
- 只更新變更字段:保存一個(gè)實(shí)體時(shí),只對(duì)狀態(tài)發(fā)生變化的字段進(jìn)行更新;
3.4.1. 只更新變更實(shí)體
在數(shù)據(jù)保存時(shí),JPA 會(huì)自動(dòng)識(shí)別發(fā)生變更的實(shí)體,僅對(duì)變更實(shí)體執(zhí)行 update 語句。
測(cè)試代碼如下:
Order order = this.orderRepository.findById(this.order.getId()).get();
order.getOrderItems().size(); // 獲取未更新
order.getPay().getPrice(); // 獲取未更新
order.getAddress().setDetail("新地址"); // 獲取并更新
System.out.println("更新數(shù)據(jù)");
this.orderRepository.save(order);
控制臺(tái)輸出如下:
Hibernate: select order0_.id as id1_3_0_, order0_.address_id as address_6_3_0_, order0_.pay_id as pay_id7_3_0_, order0_.status as status2_3_0_, order0_.total_price as total_pr3_3_0_, order0_.total_selling_price as total_se4_3_0_, order0_.user_id as user_id5_3_0_ from order_info order0_ where order0_.id=?
Hibernate: select orderitems0_.order_id as order_id6_4_0_, orderitems0_.id as id1_4_0_, orderitems0_.id as id1_4_1_, orderitems0_.price as price2_4_1_, orderitems0_.product_id as product_3_4_1_, orderitems0_.quantity as quantity4_4_1_, orderitems0_.selling_price as selling_5_4_1_ from order_item orderitems0_ where orderitems0_.order_id=?
Hibernate: select pay0_.id as id1_5_0_, pay0_.price as price2_5_0_ from pay_info pay0_ where pay0_.id=?
Hibernate: select address0_.id as id1_2_0_, address0_.detail as detail2_2_0_ from address address0_ where address0_.id=?
更新數(shù)據(jù)
Hibernate: update address set detail=? where id=?
從日志輸出可見:
- 對(duì)聚合中 的實(shí)體進(jìn)行了加載操作;
- 但,僅對(duì)變更的 address 實(shí)體執(zhí)行了 update 語句;
3.4.2. 只更新變更字段
只更新變更字段,是指只更新實(shí)體類中有變化的字段,而不是全部字段。為了實(shí)現(xiàn)按需更新,需要在實(shí)體類中使用@DynamicUpdate注解,表示只更新有變化的字段。
實(shí)體代碼見:
@Entity
@Table(name = "order_info")
@DynamicUpdate
public class Order{
// 其他忽略
}
測(cè)試代碼如下:
Order order = this.orderRepository.findById(this.order.getId()).get();
order.setUserId(RandomUtils.nextLong()); // 僅更新 user id
System.out.println("更新數(shù)據(jù)");
this.orderRepository.save(order);
控制臺(tái)輸出如下:
Hibernate: select order0_.id as id1_3_0_, order0_.address_id as address_6_3_0_, order0_.pay_id as pay_id7_3_0_, order0_.status as status2_3_0_, order0_.total_price as total_pr3_3_0_, order0_.total_selling_price as total_se4_3_0_, order0_.user_id as user_id5_3_0_ from order_info order0_ where order0_.id=?
更新數(shù)據(jù)
Hibernate: update order_info set user_id=? where id=?
如果移除 @DynamicUpdate 注解,控制臺(tái)輸出如下:
Hibernate: select order0_.id as id1_3_0_, order0_.address_id as address_6_3_0_, order0_.pay_id as pay_id7_3_0_, order0_.status as status2_3_0_, order0_.total_price as total_pr3_3_0_, order0_.total_selling_price as total_se4_3_0_, order0_.user_id as user_id5_3_0_ from order_info order0_ where order0_.id=?
更新數(shù)據(jù)
Hibernate: update order_info set address_id=?, pay_id=?, status=?, total_price=?, total_selling_price=?, user_id=? where id=?
對(duì)比輸出可知:使用@DynamicUpdate注解后,當(dāng)修改實(shí)體類中的某個(gè)字段時(shí),JPA會(huì)自動(dòng)將該字段標(biāo)記為“臟數(shù)據(jù)”,并只更新標(biāo)記為“臟數(shù)據(jù)”的字段,這樣可以減少數(shù)據(jù)庫(kù)的IO操作,提高更新效率。
4. 小節(jié)
本章從 DDD 聚合生命周期講起,當(dāng)我們面對(duì)一組高內(nèi)聚對(duì)象時(shí),如何更好的對(duì)這一對(duì)象組進(jìn)行維護(hù)。
從高內(nèi)聚對(duì)象組視角需要支持:
- 對(duì)象間的組合關(guān)系;
- 對(duì)象間的繼承關(guān)系;
從系統(tǒng)性能角度需要支持:
- 延遲加載:只有在使用時(shí)才觸發(fā)實(shí)體加載;
- 按需更新:只對(duì)狀態(tài)變更實(shí)體或字段進(jìn)行更新;
JPA 與 DDD 的==聚合寫== 是絕配,但在 “讀” 場(chǎng)景 往往會(huì)引發(fā)各種性能問題。這也是很多公司棄用 JPA 而選擇 MyBatis 的主要原因,就其本質(zhì)并不是框架的錯(cuò),而是將框架用在了錯(cuò)誤的場(chǎng)景。
對(duì)于 Command 和 Query 分離架構(gòu),最佳組合是:
- Command 側(cè)以 DDD 和 JPA 為核心,享受面向?qū)ο髲?qiáng)大設(shè)計(jì)力,享受 JPA 所帶來的便利性,從而解放雙手,提升開發(fā)效率;
- Query 側(cè)以 DTO 和 MyBatis 為核心,享受 MyBatis 對(duì) SQL 強(qiáng)大控制力,更好的壓榨 MySQL 性能,從而降低成本;