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