构建多对多关系
上一篇我们有了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