大家好,我是范钢老师。通过前面两期的讲解,我们深入探讨了在领域模型中的继承关系如何落地到软件开发。在落地的过程中,有三种方案进行领域对象的持久化:Simple方案、Union方案与Joined方案。这三种方案都各自有各自的优缺点,因此需要我们在具体的业务场景中进行决策,到底选用哪个方案更合适。
前面我们也已经探讨了,通过这三个方案,该如何创建领域对象、编写DSL配置、完成增删改的相应编码,来完成这些有继承关系的领域对象的基本操作。但是,对于有继承关系的领域对象来说,除了这些基本操作以外,其设计远比我们想象的要复杂。今天我们就接着前面的话题,继续探讨继承关系的落地实现吧。
前面我们谈到,当这些有继承关系的领域对象要进行增删改时,OrmController会根据前端Json传递过来的数据,通过数据中的标识字段去识别,现在传递过来的是哪个子类,然后将子类作为参数,传递给Service中要操作的方法。譬如,前端传递的Json中,标识字段vipType=”silver”。那么,OrmController就会创建一个银卡会员的对象,将其作为参数去调用VipService中register()方法。在该方法中,会去完成会员相关的业务校验、业务规则、业务操作,最后通过通用仓库来实现持久化,最终把数据存储到数据库里。然而,通用仓库是如何持久化存储有继承关系的领域对象的呢?
通用仓库首先会通过DSL去判断,该继承关系是采用哪种持久化方案来进行存储。其中,执行插入操作会相对比较简单,如果是Simple方案,不论是哪个子类都是存储那一张表;如果是Union方案,则根据标识字段去识别,到底存储到哪个子类的表;如果是Joined方案,先将父类的字段存储到父类的表,然后根据标识字段分别存储到各子类对应的表。相反,如果是删除操作,则按照以上思路分别完成各自的数据库删除。
但是,如果是更新操作,则相对要复杂一些。首先,通用仓库要识别,提交的数据是否变更了数据。它会通过ID查询数据库中这条数据的内容,然后和提交的数据进行比对。那么,这种查询是不是会影响系统性能呢?我的设计思路是认为,用户在更新之前肯定会先进行查询操作,因此会先对其进行缓存。然后,在进行更新的时候,查询数据库的操作就变为查询缓存的操作,从而保证了操作的性能。
接着,如果现在采用的是Simple方案,它操作的永远是一张表,那就直接做更新操作了。但如果采用的是Union方案,则需要将提交的数据与数据库中的进行比较,如果更新的是标识字段,也就意味着数据由一个子类变为了另一个子类,那么要存储的表就不一样了。因此,需要将之前的数据先进行删除,然后再执行插入操作;但如果更新的不是标识字段,也就是说子类没有变,那么该更新哪个表就更新哪个表了。如果采用的是Joined方案,就会先去更新父类的表。但在更新子类的表时,同样需要将提交的数据与数据库中的进行比较,进行判断。如果更新了标识字段,则先删除后插入,否则就直接执行更新操作。以上这是继承关系的增删改,落地实现的设计要点。
接着,再来看一看查询操作。在DDD中,要对领域对象进行查询,其实有两种方式:用load()方法,是通过主键ID去查询某一条记录;用query()方法,通过一个SQL语句和查询条件去查询一批数据。我们先来看一看load()方法的设计。
如果需要通过load()方法去查询某条记录,那么你需要告诉load()方法,你要查询的这条记录的主键ID,以及这是哪种领域对象,进行如下的调用:
Order order = dao.load(orderId, Order.class);
这时,通用仓库会根据领域对象的类名去查找DSL,然后在对应的表中进行查询,最后返回相应的领域对象。然而,如果这个领域对象有继承关系,那么查询就要复杂一些。如果第二个参数传递的类名是父类的类名,那么通用仓库在查询到这条记录以后,应当根据标识字段进行判断,返回不同类型的子类。譬如:
Vip vip = dao.load(vipId, Vip.class);
以上案例,通用仓库会先读取DSL确认采用的是哪种方案,然后到数据库中进行相应的查询。如果是Simple方案就查询那一张表;如果是Joined方案,不仅要查询父类的表,还要查询子类的表。再通过通用工厂来识别查询的记录是哪种子类,创建该子类,写入数据,通过通用仓库返回查询结果。
然而,如果采用的是Union方案,当输入一个主键ID时,通用仓库是不知道这条记录到底是哪种类型的子类,因此就不得不到所有子类的表中进行查询,然后再汇总,就会极大影响查询效率。因此,采用Union方案应当尽量避免用父类的方式进行查询,而是采用子类。
所以,如果第二个参数传入的类名是一个子类,那么最后返回的结果就只能是这个子类的数据。也就是说,即使这个主键ID有记录,如果不是该子类的数据,依然不能返回。譬如:
Vip vip = dao.load(vipId, SilverVip.class);
以上案例中,第二个参数是银卡会员的类名,因此该方法只能返回银卡会员。即使传入的vipId有记录,如果不是银卡会员,依然只能返回null。
除外,如果现在要通过一个SQL语句来批量查询数据,在前面《通用仓库与工厂》那一期谈到,需要首先编写一个MyBatis的Mapper,将这个SQL语句配置在Mapper中,然后配置AutofillQueryServiceImpl来完成对查询结果的数据补填,这是对通常的领域对象的查询。然而,如果有继承关系,那么对该领域对象的查询就要复杂一些了。
首先,如果该继承关系采用的是Simple方案,就比较简单,只需要查询那一张表。譬如,查询会员信息,则在Mapper中编写SQL语句,完成对t_vip的查询。
SELECT * FROM t_vip WHERE 1 = 1
and id in (${id})
and vip_type = ${vipType}
limit #{size} offset #{firstRow}
接着,进行Spring的装配:
@Bean
public QueryDao vipQryDao() {
return new QueryDaoMybastisImplForDdd(
"com.edev.emall.vip.entity.Vip",
"com.edev.emall.query.dao.VipMapper");
}
@Bean
public QueryService vipQry() {
return new AutofillQueryServiceImpl(
vipQryDao(), basicDaoWithCache);
}
最后就会通过分页,查询这一页的会员信息,根据会员类型返回不同类型的会员对象。
接着,如果继承关系采用的是Union方案,那么就需要通过SQL语句在所有子类的表中进行查询,然后汇总,就需要这样编写Mapper:
select id, `name`, begin_time, end_time, discount, discount_type, product_id,
null vip_type from t_product_discount where 1 = 1
select id, `name`, begin_time, end_time, discount, discount_type,
null product_id, vip_type from t_vip_discount where 1 = 1
and id in (${id})
limit #{size} offset #{firstRow}
可以看到,以上案例需要通过Union语句,将多个表的查询结果合并,这样肯定影响查询性能,特别是子类比较多的情况。因此,采用Union方案应当尽量避免对所有子类的查询,而是分成好几个Mapper,每个子类就是一个独立的查询功能。但如果业务需求就要求同时查询多个子类,就应当果断采用Joined方案。
但如果业务需求既可能每个子类单独查询,又可能所有子类一起查询,那应该怎么办呢?就将Union方案和Joined方案结合起来啦。首先,在DSL中配置的是Joined方案,并设计父类对应的表。与此同时,每个子类对应的表,通过冗余,将每个表的前面添加父类的字段。这样的设计,既可以设计一个父类的查询功能,也可以为每个子类设计查询功能。父类的查询配置父类的表,各子类的查询配置子类对应的表。
最后,如果采用的是Joined方案,那么在MyBatis的Mapper中,只需要配置对父类对应表进行的查询就可以了,AutofillQueryServiceImpl会自动完成对相应子类的数据补填。因此,Mapper的配置如下:
SELECT * FROM t_user WHERE 1 = 1
and id in (${id})
and (id like '%${value}' or name like '%${value}')
limit #{size} offset #{firstRow}
在这个案例中,用户通过继承分为“客户”、“员工”、“系统管理员”和“游客”。“客户”和“员工”有各自的表,但“系统管理员”和“游客”没有。但它们都有t_user这个父类的表,因此通过Mapper先完成对该表的查询,然后在Spring中进行如下的装配:
@Bean
public QueryDao userQryDao() {
return new QueryDaoMybastisImplForDdd(
"com.edev.emall.authority.entity.User",
"com.edev.emall.query.dao.UserMapper");
}
@Bean
public QueryService userQry() {
return new AutofillQueryServiceImpl(
userQryDao(), repositoryWithCache);
}
与此同时,我同样也可以单独对客户或者员工进行查询:
@Bean
public QueryDao staffQryDao() {
return new QueryDaoMybastisImplForDdd(
"com.edev.emall.supplier.entity.Staff",
"com.edev.emall.query.dao.StaffMapper");
}
@Bean
public QueryService staffQry() {
return new AutofillQueryServiceImpl(
staffQryDao(), basicDaoWithCache);
}
到此为止,我已经完成了对所有继承关系的讲解,同时也完成了对DDD中所有关系的讲解:常见的4种关系、继承关系的3种方案,以及聚合关系,落地开发的讲解。还是那句话,DDD落地开发的关键就是“关系”。如何将领域模型中对象之间的关系,以及这些关系背后的业务逻辑,准确地在程序设计中体现出来,这就是关键。下一期我们将站在更高的层面讲解支持DDD的架构设计。
如果对以上内容感觉有用,欢迎关注、点赞、转发!
相关的文档:
DDD该怎么去落地实现(1)关键是“关系”
DDD该怎么去落地实现(5)继承关系(上)
DDD该怎么去落地实现(6)继承关系(中)
(待续)