Spring Boot+JPA實現DDD(二)


從聚合根開始

上一篇已經把業務需求描述清楚了,現在我們來實現它。

環境

  • JDK1.8+
  • Maven3.5+
  • Mysql8.0
  • Intellij Idea lombok 插件(注意安裝插件要給Idea配置代理,否則裝不上)
  1. 新建Spring Boot工程
    start.spring.io新建一個productcenter的項目。注意右邊勾選lombok,Spring Data JPA和Mysql Driver。點擊“GENERATE”生成項目。

  2. 新建包結構
    我們知道DDD有四層架構。

  • 用戶接口層
  • 應用層
  • 領域層
  • 基礎設施層
    按照這個結構我們分別建4個包: ui, application, domain, infrastructure
  1. 實現模型
    沒有表結構突然不知道從哪里開始了?以前因為已經有表結構了,我們一開始用工具自動生成entity,然后就開始寫controller,service,dao了。
    DDD是以領域為核心的,領域里的模型是穩定的,不管外部怎么變化,我們的模型是保持不變的。注意這里說的“穩定”、“不變”是指項目上線后不變,在開發階段,模型是要不斷優化調整的。所以我們就從模型開始。當然如果你的項目要先跟別人定好接口再開發,那你可以先從controller開始。然后構建模型。

在domain下新建 model.product.Product類。

/**
 * 商品聚合根
 */
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "product_no", length = 32, nullable = false, unique = true)
    private String productNo;
    @Column(name = "name", length = 64, nullable = false)
    private String name;
    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    @Column(name = "category_id", nullable = false)
    private Integer categoryId;

    @Column(name = "product_status", nullable = false)
    private Integer productStatus;
    @Column(name = "remark", length = 256)
    private String remark;

    @Column(name = "allow_across_category", nullable = false)
    private Boolean allowAcrossCategory;

    public Product(Long id, String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, 
                                              String remark, Boolean allowAcrossCategory) {
        this.id = id;
        this.productNo = productNo;
        this.name = name;
        this.price = price;
        this.categoryId = categoryId;
        this.productStatus = productStatus;
        this.remark = remark;
        this.allowAcrossCategory = allowAcrossCategory;
    }

}

類的屬性用JPA的@Column跟db表的字段對應起來,並且類的屬性跟業務密切相關。商品有名稱,價格,類目,狀態,是否允許跨類目,備注字段。
此外,除了一個自增的主鍵,商品應該還一個唯一的產品編碼。這個唯一的產品編碼就是業務主鍵,跟外部交互的時候都使用這個業務主鍵。這至少有3個好處:

  • 對前端不會暴露我們的實現
  • 如果有一天需要遷移數據的時候,因為業務主鍵是穩定的,很好遷移。而物理主鍵是會變的,遷移到另一張表可能還會有主鍵沖突,到時候就很難受。
  • 業務主鍵是可讀的,並且其本身包含了一些有用信息。

因為hibernate需要entity提供一個無參構造函數,我們用lombok注解@NoArgsConstructor(access = AccessLevel.PROTECTED)。注意到,這里的訪問權限給的是protected,這樣是防止外部直接new Product()創建一個空的商品。

現在觀察一下我們的有參數構造函數訪問權限是public。這意味着,其他地方可以隨意的創建一個商品。問題是他們知道如何正確的創建一個商品嗎?
也許你會說,我們把創建商品需要的業務規則都放在這個構造函數里不就行了嗎? 行是行,就是不靈活了。假如某一天我們想返回Product的一個子類怎么辦?

所以我們應該提供一個工廠方法。由這個工廠方法統一創建商品。 雙擊構造函數名稱,右擊鼠標 Refactor >> Replace Constructor with Factory Method
輸入工廠方法名of。 你會看到,idea自動把構造函數變成了私有的方法:

private Product(Long id, String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, String remark,  
                                           Boolean allowAcrossCategory) {
        this.id = id;
        this.productNo = productNo;
        this.name = name;
        this.price = price;
        this.categoryId = categoryId;
        this.productStatus = productStatus;
        this.remark = remark;
        this.allowAcrossCategory = allowAcrossCategory;
}

public static Product of(Long id, String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, 
                                                    String remark, Boolean allowAcrossCategory) {
    return new Product(id, productNo, name, price, categoryId, productStatus, remark, allowAcrossCategory);
}

再看看代碼,好像有點“壞味道”,既然已經用了lombok,為什么還要自己寫一個構造函數呢。
把有參構造函數刪掉, 在類上加一個 @AllArgsConstructor(access = AccessLevel.PRIVATE)

有參數構造函數的訪問權限是private。 第一次見到這個你可能會覺得不可思議,因為以前你從來沒想過要把構造函數變成私有的。
不僅如此,setter和getter也是隨便給。這是不對的,DDD的代碼要嚴格控制訪問權限,這樣才能最大程度上保證模型的穩定。不然就會出現一個屬性的值不知道在什么地方被改了,你卻不知道的情況。一旦出現這樣的bug,簡直就是災難。

雖然實體是可被修改的,但不代表所有屬性都隨便調用setter輕輕松松就改掉了。
如果確實需要修改某個屬性,請提供一個具體的方法,比如changeProductStatus,這個方法跟業務上也應該有對應關系,否則就沒必要單獨寫一個方法。
這才叫封裝嘛,你說是不是?

沒有setter和getter,hibernate還能實現持久化嗎?
以前的hibernate要求entity必須有setter和getter,現在不需要了。

工廠方法也有點問題。主鍵id是自動生成的,怎么能讓程序傳進來呢。所以工廠方法刪除id這個參數,在調用Product有參構造函數的時候id傳一個null。

Product類最后變成這個樣子:

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Product implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "product_no", length = 32, nullable = false, unique = true)
    private String productNo;
    @Column(name = "name", length = 64, nullable = false)
    private String name;
    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    @Column(name = "category_id", nullable = false)
    private Integer categoryId;

    @Column(name = "product_status", nullable = false)
    private Integer productStatus;
    @Column(name = "remark", length = 256)
    private String remark;

    @Column(name = "allow_across_category", nullable = false)
    private Boolean allowAcrossCategory;

    public static Product of(String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, 
                                               String remark, Boolean allowAcrossCategory) {
        return new Product(null, productNo, name, price, categoryId, productStatus, remark, allowAcrossCategory);
    }
}
  1. 啟動項目
    啟動的時候會報如下錯誤
    Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured
    因為我們還沒配置過數據庫連接。現在來配置它。application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/product_center?\ 
  useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=<username>
spring.datasource.password=<password>
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=create
# note that "spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect" was deprecated
spring.hibernate.dialect.storage_engine=innodb

在mysql里創建一個名為product_center的庫,再次啟動項目。hibernate自動為我們生成了一個product表:

  1. 復寫equals和hashCode方法(重要)
    添加guava包:
<properties>
    <guava.version>29.0-jre</guava.version>
</properties>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>${guava.version}</version>
</dependency>

Alt+Insert,選擇 equals() and hashCode(),Template選擇Objects.equals and hashCode(Guava),點擊“下一步”,Member選擇productNo:String,下一步,Finish。
生成的equals和hashCode方法如下:

@Override
public boolean equals(Object o) {
	if (this == o) return true;
	if (o == null || getClass() != o.getClass()) return false;
	Product product = (Product) o;
	return Objects.equal(productNo, product.productNo);
}

@Override
public int hashCode() {
	return Objects.hashCode(productNo);
}

ProductproductNo是唯一的,兩個實體,只要這個字段相同,就認為是同一個實體。

源碼下載: productcenter2.zip

文章有點長了,下一篇我們繼續。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM