1. 程式人生 > 實用技巧 >Spring Boot+JPA實現DDD(三)

Spring Boot+JPA實現DDD(三)

上一篇我們有了Product這個聚合根。前面已經分析過,一個商品可以包含一個或多個課程明細。課程明細可以單獨編輯,有自己的生命週期,課程明細也是一個聚合根。

  1. domain.model包下建立 courseitem.CourseItem類,內容如下:
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class CourseItem implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "item_no", length = 32, nullable = false, unique = true)
    private String itemNo;
    @Column(name = "name", length = 64, nullable = false)
    private String name;
    @Column(name = "category_id", nullable = false)
    private Integer categoryId;
    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    @Column(name = "remark", length = 256)
    private String remark;
    @Column(name = "study_type", nullable = false)
    private Integer studyType;
    @Column(name = "period")
    private Integer period;
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "deadline")
    private Date deadline;

    public static CourseItem of(String itemNo, String name, Integer categoryId, BigDecimal price, String remark, Integer studyType,
                                               Integer period, Date deadline) {
        return new CourseItem(null, itemNo, name, categoryId, price, remark, studyType, period, deadline);
    }

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

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

跟產品類似,課程明細也有名稱,價格,唯一的明細編碼,課程明細有2種有效學習期,按截止日期或者按下單後xx月。

產品跟課程明細是多對多的關係,這個關係怎麼處理?是不是要配置 @ManyToMany啊?
不要,因為模型裡的程式碼應該是框架無關的@ManyToMany是hibernate的註解,我們應該避免使用JPA具體實現的註解,而應該多用JPA通用的註解。
也許你會反駁我說,既然這樣,Entity類就應該保持純潔性,為什麼我還在Entity類裡使用JPA相關的註解?JPA雖然不是框架,但是在實體類裡寫@Column這種DB相關的東西真的好嗎?

這是個好問題。用JPA的原因是不給自己找麻煩。既然使用了Spring這個框架,框架提供了Spring Data JPA這麼成熟好用的工具我們為什麼不用呢。
沒必要自己再寫一套東西,把非常純潔的實體物件轉成持久化物件後再持久化它。 有種重複造輪子的感覺不說,還容易出錯。
個人覺得實體里加一些JPA的註解是可以忍受的,不是什麼很嚴重的問題。油管上看到的視訊,有人問過大神這個問題,大神就是這麼回答的。

我們知道要描述多對多的關係需要維護一張中間表。@Entity註解的類可以直接生成表,那麼商品-明細這個中間表怎麼生成呢?

需要使用JPA的2個註解。@Embeddable@ElementCollection

  1. 在product包下新建ProductCourseItem類,內容如下:
@Embeddable
@Getter
@EqualsAndHashCode
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ProductCourseItem implements Serializable {
    @Column(name = "course_item_no", length = 32, nullable = false)
    private String courseItemNo;
    @Column(name = "new_price", precision = 10, scale = 2)
    private BigDecimal newPrice;

    public static ProductCourseItem of(String courseItemNo, BigDecimal retakePrice) {
        return new ProductCourseItem(courseItemNo, retakePrice);
    }

}

注意,ProductCourseItem是一個值物件,值物件是不能被修改的。所以這個類只提供了getter,並沒有提供setter。

Product類新增如下:

@ElementCollection(targetClass = ProductCourseItem.class)
@CollectionTable(
        name = "product_course_item",
        uniqueConstraints = @UniqueConstraint(columnNames = {"product_no", "course_item_no"}),
        joinColumns = {@JoinColumn(name = "product_no", referencedColumnName = "product_no")}
    )
private Set<ProductCourseItem> productCourseItems = new HashSet<>();

並且修改of工廠方法(這裡也可以看到使用Lombok的好處之一,不用頻繁地重新生成有參建構函式和getter了):

public static Product of(String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, String remark, Boolean allowAcrossCategory, Set<ProductCourseItem> productCourseItems) {
    return new Product(null, productNo, name, price, categoryId, productStatus, remark, allowAcrossCategory, productCourseItems);
}

商品的課程明細不能重複,所以我們使用Set集合。
中間表的名稱是product_course_item,並且給中間表加一個唯一複合索引——商品的product_no和明細的course_item_no組成一個唯一索引。

到這裡也許你會奇怪,中間表product_course_item裡並沒有宣告product_no這個欄位啊。 別擔心,因為Product類裡有一個@ElementCollection。這個註解會幫我們在中間表裡生成product_no這個欄位。

  1. 啟動專案,hibernate會刪除之前的表,重新生成新的表結構:
    courseitem表:

中間表:

中間表有了一個唯一複合索引,這樣可以在db層面上保證不會重複。

中間表為什麼會有一個new_price欄位?
因為不同的課程明細在不同的商品下價格不同。