構建多對多關系
上一篇我們有了Product這個聚合根。前面已經分析過,一個商品可以包含一個或多個課程明細。課程明細可以單獨編輯,有自己的生命周期,課程明細也是一個聚合根。
- 在
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
。
- 在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
這個字段。
為什么不在Product里直接引用CourseItem呢?
聚合根可以直接引用實體,值對象。 不能直接引用其它聚合根,要通過唯一標識來關聯。
就算用唯一標識來關聯,為什么不用物理主鍵而用業務主鍵關聯呢?
哈哈,能問出這個問題,說明你真的在認真看我的文章了。通常我們都使用物理主鍵來做關聯。 但其實db規范里並沒有強制要求我們使用物理主鍵來做關聯。
正如我在上一篇文章里說的,使用業務主鍵有很多好處,用業務主鍵做關聯除了多占了一些空間外,我實在想不通有什么不好?
- 啟動項目,hibernate會刪除之前的表,重新生成新的表結構:
courseitem表:
中間表:
中間表有了一個唯一復合索引,這樣可以在db層面上保證不會重復。
- 問題解答
①中間表為什么會有一個new_price
字段?
因為同一個課程明細在不同的商品下價格不同。
②ProductCourseItem
類的equals方法是由@EqualsAndHashCode
注解實現的。ProductCourseItem
類只有2個字段,那么注解自動生成的equals方法里只會比較這2個字段。 為什么沒有算上productNo
?
好問題。 不需要算上productNo
,因為ProductCourseItem不會單獨使用,它只會存在於某個Product里,這天然地保證了它們的productNo都是一樣的,所以equals方法也就沒必要算上productNo了。
源碼下載:productcenter3.zip