日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

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)成了聚合的生命周期。

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

  1. 首先,用戶操作下單,使用提交數(shù)據(jù)為其創(chuàng)建一個(gè) Order 對(duì)象,版本 V1;
  2. 隨后,用戶進(jìn)行改地址操作,調(diào)用 Order 對(duì)象的 modifyAddress 方法,Order 從原來的 V1 變成 V2;
  3. 用戶完成支付后,調(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):

  1. 業(yè)務(wù)操作沒變,仍舊依次完成 下單、改地址、支付等操作
  2. 引入持久化存儲(chǔ)(MySQL),可以將 Order 對(duì)象存儲(chǔ)于關(guān)系數(shù)據(jù)庫(kù)
  3. 配合 Order 的生命周期,操作中增加 save、load 和 update 等操作
  1. 用戶下單創(chuàng)建 Order 對(duì)象,通過 save 方法將 Order 對(duì)象持久化到 DB
  2. 接收到業(yè)務(wù)操作,需執(zhí)行l(wèi)oad,從 DB 加載數(shù)據(jù)到內(nèi)存 并對(duì) Order 對(duì)象的狀態(tài)進(jìn)行恢復(fù)
  3. 在業(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ì)聚合根生命周期的管理,所以必須提供三組操作:

  1. 保存。將聚合根同步到底層存儲(chǔ)進(jìn)行持久化處理;
  2. 查詢。根據(jù) ID 或?qū)傩詮牡讓哟鎯?chǔ)引擎中讀取數(shù)據(jù)并恢復(fù)為內(nèi)存對(duì)象,也就是聚合根對(duì)象;
  3. 更新。聚合對(duì)象發(fā)生變更后,可以將新的狀態(tài)同步到存儲(chǔ)引擎,以便完成數(shù)據(jù)更新;

有人會(huì)說,這和 DAO 沒啥區(qū)別吧!!!

DAO 是單表單實(shí)體操作,Repository 操作的是整個(gè)聚合甚至包括繼承關(guān)系,這就是最大的區(qū)別。也就是Repository 必須能夠:

  1. 維護(hù)一個(gè)完整的對(duì)象組,也就是必須能處理對(duì)象的組合關(guān)系;
  2. 維護(hù)一個(gè)完整的繼承體系,也就是必須能夠處理對(duì)象繼承關(guān)系;

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

聚合根是一個(gè)對(duì)象組,包含各種關(guān)系,并不是每個(gè)業(yè)務(wù)操作都需要聚合根內(nèi)的所有實(shí)體。
舉個(gè)例子,在電商訂單聚合根內(nèi),包括:

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

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

為了滿足不同的業(yè)務(wù)場(chǎng)景,Repository 需要具備兩個(gè)高級(jí)特性:

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

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

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

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è)維度:

  1. 數(shù)量維度。指關(guān)聯(lián)關(guān)系兩端對(duì)象的數(shù)量,包括
  1. 一對(duì)一:一個(gè)實(shí)體對(duì)象只能關(guān)聯(lián)到另一個(gè)實(shí)體對(duì)象,例如 公司 和 營(yíng)業(yè)執(zhí)照,一個(gè)公司只會(huì)有一個(gè)營(yíng)業(yè)執(zhí)照;
  2. 一對(duì)多:一個(gè)實(shí)體對(duì)象可以關(guān)聯(lián)到多個(gè)實(shí)體對(duì)象,例如 訂單 和 訂單項(xiàng),一個(gè)訂單關(guān)聯(lián)多個(gè)訂單項(xiàng);
  3. 多對(duì)一:多個(gè)實(shí)體對(duì)象可以關(guān)聯(lián)到同一個(gè)實(shí)體對(duì)象,例如 訂單項(xiàng) 和 訂單,一個(gè)訂單項(xiàng)只屬于一個(gè)訂單;
  4. 多對(duì)多:多個(gè)實(shí)體對(duì)象可以互相關(guān)聯(lián),例如 社團(tuán) 和 學(xué)生,一個(gè)社團(tuán)包含多個(gè)學(xué)生,一個(gè)學(xué)生也可以參加多個(gè)社團(tuán);
  1. 方向維度。指對(duì)象的引用關(guān)系
  2. 單向關(guān)聯(lián),只能從一端訪問另一端,比如 訂單存在訂單項(xiàng)的引用,訂單項(xiàng)沒有到訂單的引用;
  3. 雙向關(guān)聯(lián),可以互相訪問,訂單存在訂單項(xiàng)的引用,訂單項(xiàng)也有到訂單的引用;

兩者組合,情況更加復(fù)雜,會(huì)產(chǎn)生:

  1. 單向多對(duì)一
  2. 雙向多對(duì)一
  3. 單向一對(duì)多
  4. 雙向一對(duì)多
  5. 單向一對(duì)一
  6. 雙向一對(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í)際工作中主要涉及:

  1. 單向一對(duì)一
  2. 單向一對(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ù)如下

其中:

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

  1. 單表繼承策略(SINGLE_TABLE)。父類實(shí)體和子類實(shí)體共用一張數(shù)據(jù)庫(kù)表,在表中通過一列辨別字段來區(qū)別不同類別的實(shí)體;
  2. Joined 策略(JOINED)。父類和子類分別對(duì)應(yīng)一張表,父類對(duì)應(yīng)的表中只有父類自己的字段,子類對(duì)應(yīng)的表中中有自己的字段和父類的主鍵字段,兩者間通過 Join 方式來處理關(guān)聯(lián);
  3. 每個(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)證策略有:

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

為了保障系統(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)鍵注解:

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

其中:

  1. activity_type 用于區(qū)分當(dāng)前數(shù)據(jù)對(duì)應(yīng)的策略類型;
  2. VipLevel類型下,只有 status 和 levels 生效,服務(wù)于 VipLevelMatcher,其他全部為 null;
  3. SpecifyUser 類型下,只有 status 和 user_ids 生效,服務(wù)于 SpecifyUserMatcher,其他全部為 null;
  4. 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)鍵注解:

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

  1. 主表存儲(chǔ)各個(gè)子類共享的父類數(shù)據(jù);
  2. 子表通過字段與主表相關(guān)聯(lián);
  3. 主表有所有子表的數(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):

  1. @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS),添加在父類實(shí)體,用于說明當(dāng)前使用的是 TABLE_PER_CLASS 策略;
  2. @GeneratedValue(strategy = GenerationType.AUTO) 不要使用IDENTITY,需要保障每個(gè)子類的 id 都不重復(fù);
  3. 抽象父類不需要表與之對(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):

  1. 每個(gè)具體的子類對(duì)應(yīng)一張表,表中存儲(chǔ)父類和子類的數(shù)據(jù);
  2. 為每個(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)單如下:

  1. 單表策略。

子類的數(shù)據(jù)量不大,且與父類的屬性差別不大;

ß可以使用單表繼承策略來減少表的數(shù)量;

  1. Joined 策略。
  2. 子類的屬性較多,且與父類的屬性差別較大;
  3. 需要一個(gè)主表,用于對(duì)所有的子類進(jìn)行管理;
  4. 每個(gè)實(shí)體一個(gè)表策略。
  5. 子類的屬性較多,且與父類的屬性差別較大;
  6. 子類過于離散,無需統(tǒng)一管理;

當(dāng)子類過多或數(shù)據(jù)量過大時(shí),Joined 和 table per class 在查詢場(chǎng)景存在明顯的性能問題,這個(gè)需要格外注意。

3.3. 立即加載&延遲加載

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

  1. 一對(duì)一關(guān)聯(lián),默認(rèn)獲取策略是立即加載(EAGER),查詢一個(gè)對(duì)象,會(huì)把它關(guān)聯(lián)的對(duì)象都查出來初始化到屬性中;
  2. 一對(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

從日志輸出可見:

  1. JPA 使用多張表的join,通過一個(gè)復(fù)雜的 sql 一次性獲取了所有數(shù)據(jù);
  2. 在訪問關(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ù)查詢,提高查詢效率。

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

  1. 表間的延遲加載:在表關(guān)聯(lián)情況下,進(jìn)行數(shù)據(jù)庫(kù)查詢時(shí),并不會(huì)立即查詢關(guān)聯(lián)表,而是要等到使用時(shí)才會(huì)去查數(shù)據(jù)庫(kù);
  2. 表中屬性的延遲加載:比如大型字段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)景:

  1. 只更新變更實(shí)體:在保存一組對(duì)象時(shí),只對(duì)狀態(tài)發(fā)生變化的實(shí)體進(jìn)行更新;
  2. 只更新變更字段:保存一個(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=?

從日志輸出可見:

  1. 對(duì)聚合中 的實(shí)體進(jìn)行了加載操作;
  2. 但,僅對(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ì)象組視角需要支持:

  1. 對(duì)象間的組合關(guān)系;
  2. 對(duì)象間的繼承關(guān)系;

從系統(tǒng)性能角度需要支持:

  1. 延遲加載:只有在使用時(shí)才觸發(fā)實(shí)體加載;
  2. 按需更新:只對(duì)狀態(tài)變更實(shí)體或字段進(jìn)行更新;

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

對(duì)于 Command 和 Query 分離架構(gòu),最佳組合是:

  1. Command 側(cè)以 DDD 和 JPA 為核心,享受面向?qū)ο髲?qiáng)大設(shè)計(jì)力,享受 JPA 所帶來的便利性,從而解放雙手,提升開發(fā)效率;
  2. Query 側(cè)以 DTO 和 MyBatis 為核心,享受 MyBatis 對(duì) SQL 強(qiáng)大控制力,更好的壓榨 MySQL 性能,從而降低成本;

分享到:
標(biāo)簽:DDD
用戶無頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定