浴室沉思:聊聊ORM

 

这是一个由EF群引发的随笔


平时在一个EF群摸鱼,日常问题可以归纳为以下几种:

这条sql用linq怎么写?

EF可以调用我写的存储过程么?

EF好慢啊一些复杂查询写起来好麻烦……

为什么会有这些问题?


因为EF是一个“ORM”。基本上这些问题都有一个共同点:将EF当作data mapping tool来使用,而不是ORM。

什么是ORM?


ORM是随着面向对象(OOP)而来的。很早的时候RDB一统天下,大家也习惯了面向数据的开发习惯(其实现在也是)。OOP出来后业界就发现了问题:RDB是基于数学理论的,而面向对象(OOP)是从软件工程的基本原则发展出来的,两套理论存在着阻抗,例如现在我们定义一个简单的博客对象:

public class Blog
{
    public Guid Id { get; set;}
    public string Title  { get; set;}
    public string Content { get; set;}
    public List<string> Tags { get; set;}
}

在这个博客对象中有个字符串泛型集合的标签属性,如果要持久化在RDB中一般用两种方法:1、标签单独一张表,Blog表与Tag表一对多关系;2、直接将Tags序列化(新一代的RDB提供的Json功能,所以一般是Json序列化)保存到Blog表中,用DDD的概念来说就是一种值对象(Value object)。

我们可以看到,第一种做法如果是直接将表映射到entity,那么我们最终得到的Blog类型可能并不是根据业务设计出来的样子,也就是说业务对象为RDB持久化的技术而妥协设计了。第二种方法看起来不错,但已经属于newsql的范畴,和RDB无关。

因此我们要明确的概念是:ORM是为了解决阻抗失配的,没有解决阻抗失配的都不是ORM,包括dapper、Mybatis等等等等(所谓micro orm其实并不是ORM),后者更适合的叫法应该是DMT(data mapping tool),只提供了表和entity的映射或者表达式树的处理。在DotNet这块,真正的ORM只有EF和NH两者(天国的linq to sql也不算,因为其只提供了DB First)。

扩展阅读:阻抗失配不仅仅是上述问题那么简单,例如OOP三要素——封装、继承、多态,假如你的业务对象存在继关系,那么在RDB中该如何描述?EF中提供了TPH (Table Per Hierarchy,父子类在同一张表,EF自动添加Discriminator字段用于标识属于哪一类型)、TPT (Table per Type,父子类在不同的表,子类表只包含子类属性,通过相同的Id来关联父类表上相同的entity父类数据)以及TPC(Table Per Concrete Type,没用过,也没见人用过,父子类在不同的表,父类的属性在子类表中也会存在,估计是为了优化query)三种方式,大家可以找下资料,在这里不做展开。

如何优雅地使用ORM


正确地使用ORM第一个前提是,项目必须是OOP设计,解析业务后先进行业务对象的建模,然后再通过ORM持久化业务对象的状态。以EF为例,基本排除了DB First以及Model First的做法,因为后两者属于面向数据库设计,所以EF Core只保留Code First除了更为精简外,其实也更符合ORM的实践。

然而:

这没有解决搜索(query)问题啊。

在这里我们要了解另一个概念——CQS(命令查询分离)。

CQS最早提出于1988年Bertrand Meyer的《面向对象软件架构》,可以归纳为“原则上一个方法不应该对数据造成影响(增删改)的同时又返回数据”。以是否对数据造成影响我们可以将操作分成两类:

查询(Query):返回数据,不修改数据,不会产生副作用。

命令(Command):修改数据,不返回数据,遵守单一职责原则。

在具体落地的项目中,查询往往千变万化,复杂的查询甚至要多表链接(大于3)还要进行聚合处理。其实我们可以看到,这里的查询几乎可以当作是弱报表——而几乎所有的这些查询,都不是OOP的功能。

因此虽然可以实现复杂查询,但ORM并不适用于CQS中的查询(Query)端。更好的做法是将项目的功能分成命令和查询两块,然后只在命令端使用ORM,Query端怎么快怎么来——当然具体实现也可以两边都用EF,但C端要当作ORM用,Q端直接执行sql语句。

扩展阅读:CQS并不是死规矩,例如stack的pop操作,有返回结果的同时也会改变stack本身。CQS落实到实际项目中并不是真的将操作简单地分成两类,比较简单的分法是:页面展示的一般是Q端。一些专业的项目C端甚至可以只通过Id来获取业务对象,这有助于仓储层的服务化以及事件溯源(Event Sourcing)的实现,以及在分布式系统中处理幂等。

最终总结,正确的使用ORM并没有想象中那么简单也没有那么难,其实也就是两条经验之谈:

1、必须是OOP设计,先使用Code First建好业务模型后再考虑如何持久化。

2、不能为Query而妥协设计,如果真的出现相对复杂的查询,直接CQS,Q端可以使用Dapper甚至Ado.net实现。

到了最后聊聊一些题外话,现在已经有各种DDD框架,但用了DDD框架并不代表你的项目就是DDD。同时有些DDD框架的实现就有待商榷,例如ABP其仓储层的设计就存在问题,因为它持久化的并不是DO(Domain object)的状态而是PO,这导致ABP的项目更类似于DDD Lite,这个问题我们以后有时间再说。