成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

解密DDD:高內聚對象組的維護之道

開發 前端
JPA 與 DDD 的==聚合寫== 是絕配,但在 “讀” 場景 往往會引發各種性能問題。這也是很多公司棄用 JPA 而選擇 MyBatis 的主要原因,就其本質并不是框架的錯,而是將框架用在了錯誤的場景。

1. 初始 Repository

在 DDD 中,Repository 是一個非常重要的概念,它是領域層的一個組件,用來管理聚合根的生命周期和持久化。

1.1. 核心為狀態管理

DDD 是由領域對象承載業務邏輯,所有的業務操作均在模型對象上完成,同一聚合上不同的業務操作構成了聚合的生命周期。

我們以訂單為例,如下圖所示:

  1. 首先,用戶操作下單,使用提交數據為其創建一個 Order 對象,版本 V1;
  2. 隨后,用戶進行改地址操作,調用 Order 對象的 modifyAddress 方法,Order 從原來的 V1 變成 V2;
  3. 用戶完成支付后,調用 Order 對象的 paySuccess 方法,Order 從 V2 變成 V3;

整個流程很好的體現了 Order 聚合根的生命周期。

1.2. 為什么需要 Repository?

假設,有一臺非常牛逼的計算機,計算資源無限、內存大小無限、永不掉電、永不宕機,那最簡單高效的方式便是將模型對象全部放在內存中。

但,現實不存在這樣的機器,我們不得不將內存對象寫入磁盤,下次使用時,在將其從磁盤讀入到內存。

整體結構如下圖所示:

和上圖相比,具有如下特點:

  1. 業務操作沒變,仍舊依次完成 下單、改地址、支付等操作
  2. 引入持久化存儲(MySQL),可以將 Order 對象存儲于關系數據庫
  3. 配合 Order 的生命周期,操作中增加 save、load 和 update 等操作
  1. 用戶下單創建 Order 對象,通過 save 方法將 Order 對象持久化到 DB
  2. 接收到業務操作,需執行load,從 DB 加載數據到內存 并對 Order 對象的狀態進行恢復
  3. 在業務操作完成后,需執行update,將 Order 對象的最新狀態同步的 DB

相對全內存版本確實增加了不小的復雜性,為了更好的對這些復雜性進行管理,引入 Repository 模式。

在領域驅動設計(DDD)中,Repository 是一種設計模式,它是用來存儲領域對象的容器。它提供了一種統一的方式來查詢和存儲領域對象。Repository提供了對底層數據存儲的抽象,允許應用程序在沒有直接與數據存儲技術交互的情況下訪問數據,同時該抽象允許在不修改應用程序代碼的情況下更改數據存儲技術。

【注】在 DDD 中,Repository 并不是一個 DAO,它的職責比 DAO 要多得多,它管理的是整個聚合根,而不是單個實體對象。同時,Repository 還需要提供一些查詢接口,用來查詢聚合根的狀態。

2. 什么是好的 Repository?

Repository 主要用于完成對聚合根生命周期的管理,所以必須提供三組操作:

  1. 保存。將聚合根同步到底層存儲進行持久化處理;
  2. 查詢。根據 ID 或屬性從底層存儲引擎中讀取數據并恢復為內存對象,也就是聚合根對象;
  3. 更新。聚合對象發生變更后,可以將新的狀態同步到存儲引擎,以便完成數據更新;

有人會說,這和 DAO 沒啥區別吧!??!

DAO 是單表單實體操作,Repository 操作的是整個聚合甚至包括繼承關系,這就是最大的區別。也就是Repository 必須能夠:

  1. 維護一個完整的對象組,也就是必須能處理對象的組合關系;
  2. 維護一個完整的繼承體系,也就是必須能夠處理對象繼承關系;

既支持組合又支持繼承,DAO 就沒辦法更好的承載了。
那就完了嗎?并沒有!!!

聚合根是一個對象組,包含各種關系,并不是每個業務操作都需要聚合根內的所有實體。
舉個例子,在電商訂單聚合根內,包括:

  1. 訂單(Order)。記錄用戶的一次生單,主要保存用戶、支付金額、訂單狀態等;
  2. 訂單項(OrderItem)。購買的單個商品,主要保存商品單價、售價、應付金額等;
  3. 支付記錄(Pay)。用戶的支付信息,包括支付渠道、支付金額、支付時間等;
  4. 收貨地址(Address)。用戶的收貨地址;

在改價流程里,需要修改 Order、OrderItem、Pay 三組實體。
在更新地址流程里,僅需要修改 Address 和 Order 兩組實體。

為了滿足不同的業務場景,Repository 需要具備兩個高級特性:

  1. 延遲加載。只有在第一次訪問關聯實體時才對其進行加載,避免過早加載但實際上并沒有使用所造成資源浪費問題;
  2. 按需更新。不管加載了多少組實體,在保存時僅對發生變更的實體進行更新,減少對底層存儲引擎的操作次數,從而提升性能;

總體來說,能夠具備以下特性的 Repository 才是好的 Repository:

  1. 支持組合關系
  2. 支持繼承關系
  3. 支持延遲加載
  4. 支持按需更新

3. JPA 實例

綜合調研各類 ORM 框架,只有 JPA 具備上述特性,而且和 DDD 是絕配。

3.1. 組合關系

組合是一種面向對象編程的重要概念,指一個類的對象可以將其他類的對象作為自己的組成部分。組合在DDD中使用場景最為廣泛,這也是聚合的主要工作方式。也就是將一組對象保存到存儲引擎,然后在從存儲引擎中獲取完整的對象組。

從數據視角,組合關系存在兩個維度:

  1. 數量維度。指關聯關系兩端對象的數量,包括
  1. 一對一:一個實體對象只能關聯到另一個實體對象,例如 公司 和 營業執照,一個公司只會有一個營業執照;
  2. 一對多:一個實體對象可以關聯到多個實體對象,例如 訂單 和 訂單項,一個訂單關聯多個訂單項;
  3. 多對一:多個實體對象可以關聯到同一個實體對象,例如 訂單項 和 訂單,一個訂單項只屬于一個訂單;
  4. 多對多:多個實體對象可以互相關聯,例如 社團 和 學生,一個社團包含多個學生,一個學生也可以參加多個社團;
  1. 方向維度。指對象的引用關系
  2. 單向關聯,只能從一端訪問另一端,比如 訂單存在訂單項的引用,訂單項沒有到訂單的引用;
  3. 雙向關聯,可以互相訪問,訂單存在訂單項的引用,訂單項也有到訂單的引用;

兩者組合,情況更加復雜,會產生:

  1. 單向多對一
  2. 雙向多對一
  3. 單向一對多
  4. 雙向一對多
  5. 單向一對一
  6. 雙向一對一

聚合根是一組對象訪問的入口,聚合內的所有操作都必須通過聚合根進行,所以,聚合根于其他實體的關系只能是 一對多 和 一對一;同時,所有的業務操作都是從聚合根發起,通過聚合根能關聯到內部實體即可,因此也不存在雙向。綜上所述,DDD 對組合進行了大量簡化,實際工作中主要涉及:

  1. 單向一對一
  2. 單向一對多
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 表數據如下

其中:

  1. address_id 存儲的是 Address 實體的主鍵;
  2. 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 中提供了三種繼承模型,包括:

  1. 單表繼承策略(SINGLE_TABLE)。父類實體和子類實體共用一張數據庫表,在表中通過一列辨別字段來區別不同類別的實體;
  2. Joined 策略(JOINED)。父類和子類分別對應一張表,父類對應的表中只有父類自己的字段,子類對應的表中中有自己的字段和父類的主鍵字段,兩者間通過 Join 方式來處理關聯;
  3. 每個實體一個表策略(TABLE_PER_CLASS)。每個實體對應一個表,會生成多張表,父類對應的表只有自己的字段。子類對應的表中除了有自己特有的字段外,也有父類所有的字段。

為了更好的對比各種策略,我們以一個業務場景為案例進行分析。

在優惠計算過程中,需要根據不同的配置策略對當前用戶進行驗證,以判斷用戶是否能夠享受優惠,常見的驗證策略有:

  1. 只有特定用戶才能享受。
  2. 只有男士或女士才能享受。
  3. 只有VIP特定等級才能享受。
  4. 未來還有很多

為了保障系統有良好的擴展性,引入策略模式,整體設計如下:

那接下來便是將這些實現類存儲到數據庫,然后在方便的查詢出來。

3.2.1. 單表繼承

單表繼承非常簡單,也最為實用,數據庫表只有一張,通過一列辨別字段來區別不同類別的實體。

它的使用涉及幾個關鍵注解:

  1. @Inheritance(strategy = InheritanceType.SINGLE_TABLE),添加在父類實體,用于說明當前使用的是 單表策略;
  2. @DiscriminatorColumn(name="區分類型存放的列名"),添加在父類實體,用于說明使用哪個列來區分具體類型;
  3. @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數據如下圖所示:

其中:

  1. activity_type 用于區分當前數據對應的策略類型;
  2. VipLevel類型下,只有 status 和 levels 生效,服務于 VipLevelMatcher,其他全部為 null;
  3. SpecifyUser 類型下,只有 status 和 user_ids 生效,服務于 SpecifyUserMatcher,其他全部為 null;
  4. Sex類型下,只有 status 和 sex 生效,服務于 SexMatcher,其他全部為 null;

單表繼承策略,最大的優點便是簡單,但由于父類實體和子類實體共用一張表,因此表中會有很多空字段,造成浪費。

3.2.2. Joined 策略

Joined策略,父類實體和子類實體分別對應數據庫中不同的表,子類實體的表中只存在其擴展的特殊屬性,父類的公共屬性保存在父類實體映射表中。

它的使用涉及幾個關鍵注解:

  1. @Inheritance(strategy = InheritanceType.JOINED),添加在父類實體,用于說明當前使用的是 Joined 策略;
  2. @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 如下:

具有以下特點:

  1. 主表存儲各個子類共享的父類數據;
  2. 子表通過字段與主表相關聯;
  3. 主表有所有子表的數據,每個子表只有他特有的數據;

從表數據上可以看出,Joined策略可以減少冗余的空字段,但是查詢時需要多表連接,效率較低。

3.2.3. 每個實體一個表策略

TABLE_PER_CLASS 策略,父類實體和子類實體每個類分別對應一張數據庫中的表,子類表中保存所有屬性,包括從父類實體中繼承的屬性。

它的使用主要涉及以下幾個點:

  1. @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS),添加在父類實體,用于說明當前使用的是 TABLE_PER_CLASS 策略;
  2. @GeneratedValue(strategy = GenerationType.AUTO) 不要使用IDENTITY,需要保障每個子類的 id 都不重復;
  3. 抽象父類不需要表與之對應,非抽象父類也需要表用于存儲;

相關實體代碼如下:

// 父類
@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 如下:

具有以下特點:

  1. 每個具體的子類對應一張表,表中存儲父類和子類的數據;
  2. 為每個子類生成id,所生成的 id 不重復;

從表數據上可以看出,子類中有相同的屬性,則每個子類都需要創建一遍,會導致表結構冗余,影響查詢效率。

3.2.4. 小節

三種策略各具特色,都有最佳應用場景,簡單如下:

  1. 單表策略。

子類的數據量不大,且與父類的屬性差別不大;

?可以使用單表繼承策略來減少表的數量;

  1. Joined 策略。
  2. 子類的屬性較多,且與父類的屬性差別較大;
  3. 需要一個主表,用于對所有的子類進行管理;
  4. 每個實體一個表策略。
  5. 子類的屬性較多,且與父類的屬性差別較大;
  6. 子類過于離散,無需統一管理;

當子類過多或數據量過大時,Joined 和 table per class 在查詢場景存在明顯的性能問題,這個需要格外注意。

3.3. 立即加載&延遲加載

JPA提供了兩種加載策略:立即加載和延遲加載。

  1. 一對一關聯,默認獲取策略是立即加載(EAGER),查詢一個對象,會把它關聯的對象都查出來初始化到屬性中;
  2. 一對多關聯,默認獲取策略是懶加載(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

從日志輸出可見:

  1. JPA 使用多張表的join,通過一個復雜的 sql 一次性獲取了所有數據;
  2. 在訪問關聯實體時,未觸發任何加載操作;
3.3.2. 延遲加載

延遲加載是指在進行數據庫查詢時,并不會立即查詢關聯表數據,而是要等到使用時才會去查,這樣可以避免不必要的數據庫查詢,提高查詢效率。

延遲加載又分為兩種情況:

  1. 表間的延遲加載:在表關聯情況下,進行數據庫查詢時,并不會立即查詢關聯表,而是要等到使用時才會去查數據庫;
  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. 按需更新

簡單理解按需更新,就是只有在有必要時才會對數據進行更新。

按需更新可以分為兩個場景:

  1. 只更新變更實體:在保存一組對象時,只對狀態發生變化的實體進行更新;
  2. 只更新變更字段:保存一個實體時,只對狀態發生變化的字段進行更新;
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=?

從日志輸出可見:

  1. 對聚合中 的實體進行了加載操作;
  2. 但,僅對變更的 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 聚合生命周期講起,當我們面對一組高內聚對象時,如何更好的對這一對象組進行維護。

從高內聚對象組視角需要支持:

  1. 對象間的組合關系;
  2. 對象間的繼承關系;

從系統性能角度需要支持:

  1. 延遲加載:只有在使用時才觸發實體加載;
  2. 按需更新:只對狀態變更實體或字段進行更新;

JPA 與 DDD 的==聚合寫== 是絕配,但在 “讀” 場景 往往會引發各種性能問題。這也是很多公司棄用 JPA 而選擇 MyBatis 的主要原因,就其本質并不是框架的錯,而是將框架用在了錯誤的場景。

對于 Command 和 Query 分離架構,最佳組合是:

  1. Command 側以 DDD 和 JPA 為核心,享受面向對象強大設計力,享受 JPA 所帶來的便利性,從而解放雙手,提升開發效率;
  2. Query 側以 DTO 和 MyBatis 為核心,享受 MyBatis 對 SQL 強大控制力,更好的壓榨 MySQL 性能,從而降低成本;
責任編輯:武曉燕 來源: geekhalo
相關推薦

2025-06-10 02:00:15

GRASP高內聚代碼

2020-09-08 06:32:57

項目低耦合高內聚

2022-03-30 09:50:09

迪米特法則構造器接口

2024-11-14 09:42:32

2024-12-04 10:58:57

TomcatJetty高并發

2023-09-26 01:18:55

解密系統業務

2021-08-26 09:31:40

Nacos配置注冊

2022-09-02 08:17:40

MapStruct代碼工具

2020-08-18 13:50:04

Tomcat高并發Java

2022-03-18 09:11:56

高并發搶購系統架構

2016-11-10 18:57:19

雙十一高并發

2016-12-02 16:39:17

2018-06-29 09:59:26

高并發互聯網網卡

2010-04-07 09:25:00

2023-11-06 08:32:17

FastAPIPython

2023-08-05 13:31:20

工廠方法模式對象

2017-08-03 16:31:43

微服務架構領域驅動設計數字化

2016-12-02 11:11:11

2011-08-19 17:47:53

清華哈佛高管

2020-09-03 14:30:40

Tomcat 拆解調優
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美精品综合在线 | 亚洲 欧美 日韩 精品 | 三级成人片 | 国产视频日韩 | 日韩高清中文字幕 | 精品久久久久久久久久久久久 | 精品免费国产一区二区三区四区介绍 | 午夜精品| av中文字幕在线观看 | 亚洲精品视频免费 | 亚洲一区久久 | 国产精品电影在线观看 | 激情五月婷婷 | 国产精品99久久久久久大便 | 一本色道精品久久一区二区三区 | 日本不卡视频在线播放 | 天堂久久天堂综合色 | 亚洲免费视频在线观看 | www.一区二区| 国产一区二区毛片 | 欧美精品一级 | 91国产在线视频在线 | 国产无套一区二区三区久久 | 精品国产乱码久久久久久牛牛 | 久久九| 中文字幕日本一区二区 | 黄色毛片网站在线观看 | 日本高清视频在线播放 | 婷婷亚洲综合 | 国产在线精品免费 | 成人午夜视频在线观看 | 午夜激情小视频 | 成人一区二区三区在线观看 | 欧美一区二区在线播放 | 亚洲精品欧美 | 国产欧美性成人精品午夜 | 成人欧美在线 | 宅女噜噜66国产精品观看免费 | 国产资源在线视频 | 91久久综合亚洲鲁鲁五月天 | 综合九九|