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这个字段。

为什么不在Product里直接引用CourseItem呢?
聚合根可以直接引用实体,值对象。 不能直接引用其它聚合根,要通过唯一标识来关联。

就算用唯一标识来关联,为什么不用物理主键而用业务主键关联呢?
哈哈,能问出这个问题,说明你真的在认真看我的文章了。通常我们都使用物理主键来做关联。 但其实db规范里并没有强制要求我们使用物理主键来做关联。
正如我在上一篇文章里说的,使用业务主键有很多好处,用业务主键做关联除了多占了一些空间外,我实在想不通有什么不好?

  1. 启动项目,hibernate会删除之前的表,重新生成新的表结构:
    courseitem表:
    Spring Boot+JPA实现DDD(三)

中间表:
Spring Boot+JPA实现DDD(三)

中间表有了一个唯一复合索引,这样可以在db层面上保证不会重复。

  1. 问题解答

①中间表为什么会有一个new_price字段?

因为同一个课程明细在不同的商品下价格不同。

ProductCourseItem类的equals方法是由@EqualsAndHashCode注解实现的。ProductCourseItem类只有2个字段,那么注解自动生成的equals方法里只会比较这2个字段。 为什么没有算上productNo?

好问题。 不需要算上productNo,因为ProductCourseItem不会单独使用,它只会存在于某个Product里,这天然地保证了它们的productNo都是一样的,所以equals方法也就没必要算上productNo了。

源码下载:productcenter3.zip