DDD该怎么去落地实现(5)继承关系(上)

继承关系的设计实现(上)

大家好,我是范钢老师。在这个系列文章的第一篇,我就开宗明义地提出,DDD落地实现的关键在于“关系”,即如何将领域模型中对象与对象之间的关系,原汁原味、原封不动地在程序代码中表达出来。接着,我通过一系列的文章给大家讲解了,一对一、多对一、一对多,以及多对多关系,在程序中的实现。总体来说,除了形成一系列对应的领域对象以外,还需要通过DSL来补充说明更多细节的信息,包括它们之间是什么类型的关系、通过什么字段关联、是否存在聚合,等等。除了这些信息以外,细心的同学可能注意到了,还有领域对象与数据库表之间的关系:

<do class="com.edev.emall.order.entity.Order" tableName="t_order">
    <property name="id" column="id" isPrimaryKey="true"/>
    <property name="customerId" column="customer_id"/>
    <property name="addressId" column="address_id"/>
    <property name="status" column="status"/>
    <property name="amount" column="amount"/>
    <property name="orderTime" column="order_time"/>
    <property name="modifyTime" column="modify_time"/>
    <join name="customer" joinKey="customerId" joinType="manyToOne"
          class="com.edev.emall.order.entity.Customer"/>
    <join name="address" joinKey="addressId" joinType="manyToOne"
          class="com.edev.emall.customer.entity.Address"/>
    <join name="payment" joinType="oneToOne"
          class="com.edev.emall.order.entity.Payment"/>
    <join name="orderItems" joinKey="orderId" joinType="oneToMany" isAggregation="true"
          class="com.edev.emall.order.entity.OrderItem"/>
</do>
<do class="com.edev.emall.order.entity.Customer" tableName="t_customer">
    <property name="id" column="id" isPrimaryKey="true"/>
    <property name="name" column="name"/>
    <property name="gender" column="gender"/>
    <property name="email" column="email"/>
    <property name="identification" column="identification"/>
    <property name="birthdate" column="birthdate"/>
    <property name="phoneNumber" column="phone_number"/>
    <property name="createTime" column="create_time"/>
    <property name="modifyTime" column="modify_time"/>
</do>
<do class="com.edev.emall.order.entity.OrderItem" tableName="t_order_item">
    <property name="id" column="id" isPrimaryKey="true"/>
    <property name="orderId" column="order_id"/>
    <property name="productId" column="product_id"/>
    <property name="quantity" column="quantity"/>
    <property name="price" column="price"/>
    <property name="amount" column="amount"/>
    <join name="product" joinKey="productId" joinType="manyToOne"
          class="com.edev.emall.product.entity.Product"/>
</do>

在这个完整案例中,订单对象通过tableName属性对应到t_order这个表。这就意味着,当订单Service在保存订单时,底层的仓库会将数据存储到t_order表中。同时,订单通过join标签对应到订单明细对象,它将关联到订单明细对象的DSL配置。在这个配置中,订单明细对象又对应到t_order_item表。这样,在保存订单时,通过聚合,底层仓库会同时保存t_order和t_order_item两个表,并将它们放在同一个事务中。

同样通过以上的配置,当查询订单时,仓库会通过join标签找到客户、地址、订单明细,并找到它们对应的DSL配置,进而找到相应的表进行查询,最后通过工厂进行拼装,就得到一个完整的订单对象,返回给订单Service。也就是说,前面的三种类型的关系,每个领域对象都对应到一个数据库的表。

但第四种类型的关系——多对多关系,稍微有些不同,它会在关系的中间形成一个关联类。这样,多对多关系与它们的关联类,每个都分别对应自己的表:

<do class="com.edev.emall.authority.entity.Role" tableName="t_role">
    <property name="id" column="id" isPrimaryKey="true"/>
    <property name="name" column="name"/>
    <property name="description" column="description"/>
    <join name="authorities" joinKey="roleId" joinType="manyToMany" joinClassKey="authorityId"
          joinClass="com.edev.emall.authority.entity.RoleGrantedAuthority"
          class="com.edev.emall.authority.entity.Authority"/>
</do>
<do class="com.edev.emall.authority.entity.Authority" tableName="t_authority">
    <property name="id" column="id" isPrimaryKey="true"/>
    <property name="name" column="name"/>
    <property name="description" column="description"/>
    <join name="roles" joinKey="authorityId" joinType="manyToMany" joinClassKey="roleId"
          joinClass="com.edev.emall.authority.entity.RoleGrantedAuthority"
          class="com.edev.emall.authority.entity.Role"/>
</do>
<do class="com.edev.emall.authority.entity.RoleGrantedAuthority" tableName="t_role_granted_authority">
    <property name="id" column="id" isPrimaryKey="true"/>
    <property name="available" column="available"/>
    <property name="roleId" column="role_id"/>
    <property name="authorityId" column="authority_id"/>
</do>

在以上案例中,角色与权限是多对多关系,它们分别进行DSL配置,对应到t_role和t_authority表。然后,它们的关联类RoleGrantedAuthority也需要进行DSL配置,对应到t_role_granted_authority这个表。有了这些配置,剩下增删改和查询的操作就全交给底层仓库去完成了。

然而,第五种类型的关系——继承关系,就比较复杂了。首先,领域对象可以有继承关系,但数据库是没有继承关系的。这就意味着,如果领域模型中出现了继承关系,在持久化时,是不可能让每个领域对象都分别对应到数据库的一个表。这时,继承关系的持久化就有三种不同的方案:

1)  将整个父类与子类的数据都存储到数据库的一张表中,称之为simple;

2)  将每个子类的数据各自存储到各自的表中,称之为union;

3)  将父类的数据存储到一张表中,然后将每个子类的数据再分别存储到各自的表中,称之为joined。

这三种设计方案,各有各的优势和缺点,各有各的适用场景。因此,在不同业务场景中到底使用哪个设计方案,需要我们自己去权衡与选型,然后在DSL进行描述。首先,我们来看看第一个方案:Simple。

Simple方案将整个父类与子类都写入到了一张表。譬如,vip会员通过继承分为金卡会员与银卡会员。在程序设计时,首先通过领域对象体现这种继承关系:

@EqualsAndHashCode(callSuper = true)
@Data
public class Vip extends Entity<Long> {
    private Long id;
    private Date createTime;
    private Date modifyTime;
    private String vipLevel; // 会员等级,如银卡、金卡、白金卡
    private Double points; // 会员当前的积分总数
    private Double accumulatedPoints; // 购买商品后累积的积分总数
    private Customer customer;
}
@EqualsAndHashCode(callSuper = true)
@Data
public class GoldenVip extends Vip {
    private String image;
    private Double creditLimit;
}
@EqualsAndHashCode(callSuper = true)
@Data
public class SilverVip extends Vip {
}

在这里可以看到,Vip是父类,它通过会员等级(vipLevel)来区分每个子类。在这样的基础上,它有金卡会员(GoldenVip)与银卡会员(SilverVip)两个子类。

金卡会员在继承会员的基础上,还增加了image和creditLimit两个个性化字段,而银卡会员则没有。有了这样的设计,会员Service在操作会员的方法中,操作的都是vip会员这个父类。然而,在设计期操作的是父类,在运行期却必须要落实到具体的子类。

譬如,现在用户通过前端注册了一个银卡会员,前端会提交一个银卡的Json对象:

{
  "id": 10001,
  "vipLevel": "silver",
  "points": 1000,
  "accumulatedPoints": 5000
}

这里可能大家比较疑惑,为什么没有传递customerId这个字段。因为会员与客户是一对一关系,因此它们共用主键,id的值实际上就是customerId。这里传递了vipLevel这个字段,说明要创建的是银卡会员,那么传递到后端以后,Controller要进行正确地识别,然后创建一个银卡会员的对象。然而,这个创建在没有DDD底层平台的支持下,是无法正确创建的。

前面《通用仓库和工厂》那期我们提到,为了降低DDD落地的难度,需要引入一个DDD的底层平台。在这个平台中,当前端要发起增删改操作时,会请求OrmController,将前端提交的Json对象自动地转换成后台的领域对象。这样,我们就不必麻烦去编写DTO对象,而直接编写Service与领域对象就可以了。那么,OrmController如何将Json对象转换成领域对象呢?

OrmController通过反射找到Service要调用的方法,比如register()方法,再通过反射找到这个方法要传递的参数。如果这些参数是普通参数,直接从Json对象中根据名称获取就可以了;如果是领域对象,即实现了Entity接口,那么就要通过工厂来查找并创建该领域对象。这里复习一下在DDD中工厂的作用,DDD的工厂就是用于拼装和创建领域对象的组件,包括创建该领域对象,以及与该领域对象关联的对象,并将它们拼装起来。那么,什么时候需要拼装呢?一个是通过前端的提交创建领域对象,一个是通过后台的查询创建领域对象。

现在,工厂要根据前端传递的Json创建领域对象,如果是普通的领域对象,它会先在DSL中去查找,通过反射创建该对象,然后从Json中获取数据并填入领域对象的相应属性中。但如果该领域对象存在继承关系,那么工厂会通过DSL去查找一个特殊字段,被称之为“标识字段”:

<do class="com.edev.emall.vip.entity.Vip" tableName="t_vip" subclassType="simple">
    <property name="id" column="id" isPrimaryKey="true"/>
    <property name="createTime" column="create_time"/>
    <property name="modifyTime" column="modify_time"/>
    <property name="points" column="points"/>
    <property name="accumulatedPoints" column="accumulated_points"/>
    <join name="customer" joinType="oneToOne"
          class="com.edev.emall.customer.entity.Customer"/>
    <property name="vipLevel" column="vip_level" isDiscriminator="true"/>
    <subclass class="com.edev.emall.vip.entity.GoldenVip" value="golden">
        <property name="image" column="image"/>
        <property name="creditLimit" column="credit_limit"/>
    </subclass>
    <subclass class="com.edev.emall.vip.entity.SilverVip" value="silver"/>
</do>

在以上配置中可以看到,vipLevel属性配置了isDiscriminator="true",它就是vip会员的标识字段。接着,subclassType="simple",说明该继承关系选择了Simple方案,即父类和子类的数据都存储在一张表中,tableName="t_vip"定义了这张表,最后通过subclass标签罗列出了所有的子类。如果vipLevel=”silver”就是银卡会员,如果是golden就是金卡会员,它们通过subclass中的value来定义。通过以上这些配置就把这个继承关系,以及它的持久化方案都定义好了。

这样,当工厂根据Json创建领域对象时,发现vip存在继承关系,它就会根据DSL去查找vipLevel这个标识字段的值,创建出银卡会员,然后去调用会员Service的register()方法。这时,传递给该方法的参数就不再是抽象的vip会员,而是具体的银卡会员。紧接着,会员Service会去完成所有注册会员的操作,最后调用底层仓库。仓库在存储数据时,会查找DSL的描述,最后将数据存储到t_vip表中。

@Override
public Long register(Vip vip) {
    valid(vip);
    vip.setCreateTime(DateUtils.getNow());
    if(vip.getVipLevel()==null) vip.setVipLevel("silver");
    if(vip.getPoints()==null) vip.setPoints(0D);
    if(vip.getAccumulatedPoints()==null) vip.setAccumulatedPoints(0D);
    return dao.insert(vip);
}

相关代码详见我的仓库

采用Simple方案的时候,正如它的名称那样,设计起来比较简单,所有增删改和查询操作都在t_vip这张表中。然而,不同的子类都有各自不同的个性化字段,在这一张表中该如何存储呢?很简单粗暴,将它们全部罗列在该表的后面。在t_vip表中,前面罗列的是所有父类的字段,后面依次罗列的是所有子类的个性化字段:

可以看到,如果是一个银卡会员,他的image和credit_limit字段将永远为空。这样的设计,如果这个继承关系的子类比较少、子类个性化字段比较少,问题还不大。然而,如果这个继承关系的子类比较多、子类个性化字段比较多,就会形成大量为空的字段,造成“表稀疏”。在关系型数据库中,所有为空的字段都是需要占用存储空间的,它不仅会浪费大量存储空间,还会影响查询性能,是我们不期望看到的。

然而,随着互联网的发展,过去关系型数据的性能越来越不能满足我们的需求了,这时,另外一种性能更好的数据库——NoSQL数据库就孕育而生。与关系型数据库不一样的是,NoSQL数据库存储的不再是二维表,而是以Json的形式存储。在这种情况下,为空的字段在NoSQL数据库将不再占用空间。也就是说,在NoSQL数据库中不存在“表稀疏”的问题,再多为空的字段都不影响性能。因此,将继承关系持久化存储到NoSQL数据库中,采用的必然是Simple方案。

{
  "Rowkey": 2021012100001, "Timestamp": 20210121141056001, 
  "personal": {
    "name": "John", "gender": "male", "mobile": "13300568889"
  },
  "family": {
    "type": "adult", "wife": "Ann", "children": ["Forst", "Mary"]
  }
},
{
  "Rowkey": 2021012100002, "Timestamp": 20210121141056002, 
  "personal": {
    "name": "Patric", "gender": "male", "country": "USA", 
  },
  "family": {
    "type": "teenager", "father": "Eric", "mother": "Jone"
  }
}

以上案例存储的是HBase,一种典型的NoSQL数据库。可以看到,在John用户的档案中,他的家庭信息是老婆和孩子;在Patric这个用户中,他的家庭信息是父亲和母亲。他的老婆和孩子这两个字段到哪里去了呢?因为为空,即不占用空间,也不必表示出来。因此,他们的档案信息通过Simple方案,就可以存储在这一个表中。

然而,当要设计的继承关系有较多的子类,或者子类的个性化字段比较多,并且采用关系型数据库存储时,就不适合采用Simple方案,那么又应当如何进行数据持久化的设计呢?下期我们继续讲解继承关系持久化的另外两个方案:union和joined,敬请期待。

如果对以上内容感觉有用,欢迎关注、点赞、转发!

(待续)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值