Jekyll2022-03-21T07:16:19+00:00https://www.mutuduxf.com/feed.xmlMutuduxf’s BlogSomething about...
Mutuduxfaeondxf@live.com浴室沉思:聊聊DAL和Repository2018-01-20T00:00:00+00:002018-01-20T00:00:00+00:00https://www.mutuduxf.com/2018/01/20/%E6%B5%B4%E5%AE%A4%E6%B2%89%E6%80%9D:%E8%81%8A%E8%81%8ADAL%E5%92%8CRepository<h1 id="这是一个由ddd群引发的随笔">这是一个由DDD群引发的随笔</h1>
<hr />
<p>在写了上一篇随笔<a href="https://www.mutuduxf.com/2018/01/05/%E6%B5%B4%E5%AE%A4%E6%B2%89%E6%80%9D-%E8%81%8A%E8%81%8AORM.html">《浴室沉思:聊聊ORM》</a>后一些朋友私聊我,很多刚接触DDD的朋友会对Repository(仓储层)这东西有点疑惑,为什么要叫仓储层?是不是三层的DAL换个名字而已?毕竟大家都是对数据库的操作嘛,而且我用EF还有必要用仓储么?是不是可以省掉这一层?ABP框架的持久化究竟有什么问题?</p>
<p>在回答这些问题之前,我们要先了解一些模型分层的概念。</p>
<h2 id="dodomain-object">DO(Domain Object)</h2>
<hr />
<p>几乎每位程序员在刚入门时都以三层架构(3-tier architecture)为基础,界面层(User Interface layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data access layer)首次让我们在架构级别了解高内聚低耦合的概念。其中对数据访问层也即是DAL的定义是——屏蔽对“数据文件”的操作,这里的数据文件指的是数据库以及其它持久化实现。</p>
<blockquote>
<p>扩展阅读:持久化不一定是数据库,直接写个excel甚至txt也算持久化。例如rabbitmq或者redis就是通过对文件的读写来实现。追根溯源,其实数据库也是如此,例如SQL server的mdf文件。</p>
</blockquote>
<p>在最简单的三层中,我们一般有一个models层或者entities层贯通三层用于数据的传递(为了便于描述后文统一叫entities层)。在界面层,entity用于显示数据或者作为view object的转换源;在业务逻辑层,和业务代码组合起来完成业务逻辑的实现;在数据访问层则作为被持久化的对象保存到数据库里。</p>
<p>不管怎么分,即便是后来的MVC,在很长一段时间的绝大部分项目里其实都以这种逻辑来分层,但到了DDD中就遇到了问题,因为DDD的核心模型是DO,而且DO是充血模型。</p>
<blockquote>
<p>扩展阅读:所谓充血模型,实际上就是包含了业务逻辑的对象。回想一下面向对象的定义,对象是包含“状态”(字段)以及“行为”(方法/函数)的,其实这很容易理解——贫血模型实际上就是数据的容器,它不是“活的”对象,在任何地方都可以对其进行修改,这实际上违反了高内聚的原则。打个比方:我们现在有个程序员的对象,他要减肥,那么我们要通过调用“健身”这个方法将其Weight属性逐渐降下来,而不是直接在外面set他的体重。
贫血模型大行其道有其历史原因,最早的EJB就是将业务对象分为属性和业务方法两部分,然后spring延续了下来,但spring他爹也说:这实际上是一种面向过程的做法。在这里我们暂时不展开充血模型和贫血模型的讨论。</p>
</blockquote>
<p>在DDD的模型划分中,最核心的部分就是DO(Domain Object领域对象)。DO是依据不变性划分出来的业务对象,其属性是public get protected set的,只能通过DO本身的方法来进行操作。举个简单的不严谨的例子,例如520网购,你在帮女盆友清空购物车的时候一般生成一个订单对象,其包含了子订单(例如口红、神仙水),我们大概地将DO设计如下:</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pubic</span> <span class="k">class</span> <span class="nc">Order</span>
<span class="p">{</span>
<span class="k">public</span> <span class="kt">string</span> <span class="n">OrderId</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="n">Status</span> <span class="n">Status</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="n">DateTime</span> <span class="n">CreateTime</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span> <span class="p">=</span> <span class="n">Datetime</span><span class="p">.</span><span class="n">Now</span><span class="p">;</span>
<span class="k">public</span> <span class="n">DateTime</span><span class="p">?</span> <span class="n">PayTime</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="n">DateTime</span><span class="p">?</span> <span class="n">CommitTime</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="n">List</span><span class="p"><</span><span class="n">OrderItem</span><span class="p">></span> <span class="n">Items</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Pay</span><span class="p">(</span><span class="kt">decimal</span> <span class="n">money</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="n">Items</span><span class="p">?.</span><span class="n">Count</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">exception</span> <span class="p">(</span><span class="s">"请选择商品。"</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">totalPrice</span> <span class="p">=</span> <span class="n">Items</span><span class="p">.</span><span class="nf">Sum</span><span class="p">(</span><span class="n">item</span> <span class="p">=></span> <span class="n">item</span><span class="p">.</span><span class="n">UnitPrice</span> <span class="p">*</span> <span class="n">item</span> <span class="n">Quantity</span><span class="p">);</span>
<span class="k">if</span><span class="p">(</span><span class="n">money</span> <span class="p"><</span> <span class="n">totalPrice</span><span class="p">)</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">exception</span> <span class="p">(</span><span class="s">"余额不足。"</span><span class="p">)</span>
<span class="n">PayTime</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">Now</span><span class="p">;</span>
<span class="n">Status</span> <span class="p">=</span> <span class="n">OrderStatus</span><span class="p">.</span><span class="err">已支付</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Commit</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="n">Status</span> <span class="p">!=</span> <span class="n">OrderStatus</span><span class="p">.</span><span class="err">已支付</span><span class="p">)</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">exception</span> <span class="p">(</span><span class="s">"请先支付。"</span><span class="p">);</span>
<span class="n">CommitTime</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">Now</span><span class="p">;</span>
<span class="n">Status</span> <span class="p">=</span> <span class="n">OrderStatus</span><span class="p">.</span><span class="err">已确认</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">OrderItem</span>
<span class="p">{</span>
<span class="k">public</span> <span class="n">Guid</span> <span class="n">Id</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="kt">string</span> <span class="n">Sku</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="kt">decimal</span> <span class="n">UnitPrice</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="kt">int</span> <span class="n">Quantity</span> <span class="p">{</span><span class="k">public</span> <span class="k">get</span><span class="p">;</span> <span class="k">protected</span> <span class="k">set</span><span class="p">;}</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">enum</span> <span class="n">OrderStatus</span>
<span class="p">{</span>
<span class="err">待支付</span><span class="p">,</span>
<span class="err">已支付</span><span class="p">,</span>
<span class="err">已确认</span><span class="p">,</span>
<span class="p">.....</span>
<span class="p">}</span>
</code></pre></div></div>
<p>在这个例子中我们可以条件反射般地想到,RDB中有两张表Order和OrderItem,这两张表是一对多的关系。假如我们用的是DAL,那么可能有个OrderDal和OrderItemDal来将数据分别持久化到RDB中,更严谨些的话还会将它们放到一个事务里执行。当然也可以有个泛型的DBHelper将Order和OrderItem分别持久化。</p>
<p>在这里DAL接收的参数是Order和OrderItem,它封装了对RDB的操作,让我们可以用更友好的方式来使用RDB。但这里我们要思考一个问题:所谓持久化,我们要持久化的是什么?</p>
<h2 id="popersistent-object持久化对象">PO(Persistent Object/持久化对象)</h2>
<hr />
<p>实际上我们持久化的并不是DO,而是DO的“状态”。例如你吃饭时被人偷拍,照片会将你吃饭时的样子记录下来,但吃饭这种“行为”本身无法持久化,如上面的Pay和Commit两个方法。另外DO的一个原则是“原子性”,也就是说拿就整个拿,存就整个存,如果是用仓储层来持久化,则只会有一个OrderRepository,将整个Order作为参数丢进去,仓储层内则将Order和OrderItem的状态部分转为OrderPo和OrderItemPo两种贫血对象,并用之持久化。</p>
<blockquote>
<p>重点:只有聚合才有仓储</p>
</blockquote>
<p>到了这里我们有了一个初步的概念:DAL是对数据文件操作的封装,不管是否实现了泛型与表的转换,它都是以数据文件为中心,在使用的时候其实我们是以一种面向数据库编程的思维来进行操作;仓储层则是反过来为领域层服务,领域层需要什么它才提供什么,屏蔽掉底层持久化的具体实现。</p>
<h2 id="我们已经用了ormef了还有必要用仓储层么">我们已经用了ORM(EF)了,还有必要用仓储层么?</h2>
<hr />
<p>其实还是要的,上一篇随笔<a href="https://www.mutuduxf.com/2018/01/05/%E5%85%B3%E4%BA%8EORM%E7%9A%84%E6%B5%B4%E5%AE%A4%E6%B2%89%E6%80%9D.html">《关于ORM的浴室沉思》</a>说明了ORM的概念,但遗憾的是现在所有ORM实际上都没有彻底解决阻抗失配的问题。</p>
<p>另外DO是原子性的,因此只有聚合才有仓储,上述例子OrderItem不属于聚合,它是没有仓储的。假如直接用EF的话代码是可以直接访问到DbContext<OrderItem>,这就对代码造成了隐患。</p>
<p>而且直接使用EF的话,所有的业务代码将会对EF造成高度耦合,这会造成潜在的技术风险。假如我们使用的是仓储层,到时候只需要在仓储层捣鼓就行。实际上DDD项目中,ORM反而不是必要的东西。</p>
<h2 id="所以abp的问题是">所以ABP的问题是?</h2>
<hr />
<p>ABP是一个很好的框架,土牛的技术水平也很高,但不能说ABP就是DDD的标准答案,例如其仓储层实际上并不是服务于DO而是PO。因此有种说法ABP不是DDD而是DDD LITE,这种说法有其原因。另外仓储层严谨地说不应该提供IQueryable,而应该是Command端需要什么的获取方法才提供,否则会无法进行单元测试。</p>
<p>至于Query端,怎么快怎么来,管它呢~</p>杜小非这是一个由DDD群引发的随笔 在写了上一篇随笔《浴室沉思:聊聊ORM》后一些朋友私聊我,很多刚接触DDD的朋友会对Repository(仓储层)这东西有点疑惑,为什么要叫仓储层?是不是三层的DAL换个名字而已?毕竟大家都是对数据库的操作嘛,而且我用EF还有必要用仓储么?是不是可以省掉这一层?ABP框架的持久化究竟有什么问题? 在回答这些问题之前,我们要先了解一些模型分层的概念。 DO(Domain Object) 几乎每位程序员在刚入门时都以三层架构(3-tier architecture)为基础,界面层(User Interface layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data access layer)首次让我们在架构级别了解高内聚低耦合的概念。其中对数据访问层也即是DAL的定义是——屏蔽对“数据文件”的操作,这里的数据文件指的是数据库以及其它持久化实现。 扩展阅读:持久化不一定是数据库,直接写个excel甚至txt也算持久化。例如rabbitmq或者redis就是通过对文件的读写来实现。追根溯源,其实数据库也是如此,例如SQL server的mdf文件。 在最简单的三层中,我们一般有一个models层或者entities层贯通三层用于数据的传递(为了便于描述后文统一叫entities层)。在界面层,entity用于显示数据或者作为view object的转换源;在业务逻辑层,和业务代码组合起来完成业务逻辑的实现;在数据访问层则作为被持久化的对象保存到数据库里。 不管怎么分,即便是后来的MVC,在很长一段时间的绝大部分项目里其实都以这种逻辑来分层,但到了DDD中就遇到了问题,因为DDD的核心模型是DO,而且DO是充血模型。 扩展阅读:所谓充血模型,实际上就是包含了业务逻辑的对象。回想一下面向对象的定义,对象是包含“状态”(字段)以及“行为”(方法/函数)的,其实这很容易理解——贫血模型实际上就是数据的容器,它不是“活的”对象,在任何地方都可以对其进行修改,这实际上违反了高内聚的原则。打个比方:我们现在有个程序员的对象,他要减肥,那么我们要通过调用“健身”这个方法将其Weight属性逐渐降下来,而不是直接在外面set他的体重。 贫血模型大行其道有其历史原因,最早的EJB就是将业务对象分为属性和业务方法两部分,然后spring延续了下来,但spring他爹也说:这实际上是一种面向过程的做法。在这里我们暂时不展开充血模型和贫血模型的讨论。 在DDD的模型划分中,最核心的部分就是DO(Domain Object领域对象)。DO是依据不变性划分出来的业务对象,其属性是public get protected set的,只能通过DO本身的方法来进行操作。举个简单的不严谨的例子,例如520网购,你在帮女盆友清空购物车的时候一般生成一个订单对象,其包含了子订单(例如口红、神仙水),我们大概地将DO设计如下: pubic class Order { public string OrderId {public get; protected set;} public Status Status {public get; protected set;} public DateTime CreateTime {public get; protected set;} = Datetime.Now; public DateTime? PayTime {public get; protected set;} public DateTime? CommitTime {public get; protected set;} public List<OrderItem> Items {public get; protected set;} public void Pay(decimal money) { if(Items?.Count == 0) throw new exception ("请选择商品。"); var totalPrice = Items.Sum(item => item.UnitPrice * item Quantity); if(money < totalPrice) throw new exception ("余额不足。") PayTime = DateTime.Now; Status = OrderStatus.已支付; } public void Commit() { if(Status != OrderStatus.已支付) throw new exception ("请先支付。"); CommitTime = DateTime.Now; Status = OrderStatus.已确认; } } public class OrderItem { public Guid Id {public get; protected set;} public string Sku {public get; protected set;} public decimal UnitPrice {public get; protected set;} public int Quantity {public get; protected set;} } public enum OrderStatus { 待支付, 已支付, 已确认, ..... } 在这个例子中我们可以条件反射般地想到,RDB中有两张表Order和OrderItem,这两张表是一对多的关系。假如我们用的是DAL,那么可能有个OrderDal和OrderItemDal来将数据分别持久化到RDB中,更严谨些的话还会将它们放到一个事务里执行。当然也可以有个泛型的DBHelper将Order和OrderItem分别持久化。 在这里DAL接收的参数是Order和OrderItem,它封装了对RDB的操作,让我们可以用更友好的方式来使用RDB。但这里我们要思考一个问题:所谓持久化,我们要持久化的是什么? PO(Persistent Object/持久化对象) 实际上我们持久化的并不是DO,而是DO的“状态”。例如你吃饭时被人偷拍,照片会将你吃饭时的样子记录下来,但吃饭这种“行为”本身无法持久化,如上面的Pay和Commit两个方法。另外DO的一个原则是“原子性”,也就是说拿就整个拿,存就整个存,如果是用仓储层来持久化,则只会有一个OrderRepository,将整个Order作为参数丢进去,仓储层内则将Order和OrderItem的状态部分转为OrderPo和OrderItemPo两种贫血对象,并用之持久化。 重点:只有聚合才有仓储 到了这里我们有了一个初步的概念:DAL是对数据文件操作的封装,不管是否实现了泛型与表的转换,它都是以数据文件为中心,在使用的时候其实我们是以一种面向数据库编程的思维来进行操作;仓储层则是反过来为领域层服务,领域层需要什么它才提供什么,屏蔽掉底层持久化的具体实现。 我们已经用了ORM(EF)了,还有必要用仓储层么? 其实还是要的,上一篇随笔《关于ORM的浴室沉思》说明了ORM的概念,但遗憾的是现在所有ORM实际上都没有彻底解决阻抗失配的问题。 另外DO是原子性的,因此只有聚合才有仓储,上述例子OrderItem不属于聚合,它是没有仓储的。假如直接用EF的话代码是可以直接访问到DbContext<OrderItem>,这就对代码造成了隐患。 而且直接使用EF的话,所有的业务代码将会对EF造成高度耦合,这会造成潜在的技术风险。假如我们使用的是仓储层,到时候只需要在仓储层捣鼓就行。实际上DDD项目中,ORM反而不是必要的东西。 所以ABP的问题是? ABP是一个很好的框架,土牛的技术水平也很高,但不能说ABP就是DDD的标准答案,例如其仓储层实际上并不是服务于DO而是PO。因此有种说法ABP不是DDD而是DDD LITE,这种说法有其原因。另外仓储层严谨地说不应该提供IQueryable,而应该是Command端需要什么的获取方法才提供,否则会无法进行单元测试。 至于Query端,怎么快怎么来,管它呢~浴室沉思:聊聊ORM2018-01-05T00:00:00+00:002018-01-05T00:00:00+00:00https://www.mutuduxf.com/2018/01/05/%E6%B5%B4%E5%AE%A4%E6%B2%89%E6%80%9D:%E8%81%8A%E8%81%8AORM<h1 id="这是一个由ef群引发的随笔">这是一个由EF群引发的随笔</h1>
<hr />
<p>平时在一个EF群摸鱼,日常问题可以归纳为以下几种:</p>
<blockquote>
<p>这条sql用linq怎么写?</p>
<p>EF可以调用我写的存储过程么?</p>
<p>EF好慢啊一些复杂查询写起来好麻烦……</p>
</blockquote>
<h2 id="为什么会有这些问题">为什么会有这些问题?</h2>
<hr />
<p>因为EF是一个“ORM”。基本上这些问题都有一个共同点:将EF当作data mapping tool来使用,而不是ORM。</p>
<h2 id="什么是orm">什么是ORM?</h2>
<hr />
<p>ORM是随着面向对象(OOP)而来的。很早的时候RDB一统天下,大家也习惯了面向数据的开发习惯(其实现在也是)。OOP出来后业界就发现了问题:RDB是基于数学理论的,而面向对象(OOP)是从软件工程的基本原则发展出来的,两套理论存在着阻抗,例如现在我们定义一个简单的博客对象:</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">Blog</span>
<span class="p">{</span>
<span class="k">public</span> <span class="n">Guid</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="kt">string</span> <span class="n">Title</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="kt">string</span> <span class="n">Content</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;}</span>
<span class="k">public</span> <span class="n">List</span><span class="p"><</span><span class="kt">string</span><span class="p">></span> <span class="n">Tags</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>在这个博客对象中有个字符串泛型集合的标签属性,如果要持久化在RDB中一般用两种方法:1、标签单独一张表,Blog表与Tag表一对多关系;2、直接将Tags序列化(新一代的RDB提供的Json功能,所以一般是Json序列化)保存到Blog表中,用DDD的概念来说就是一种值对象(Value object)。</p>
<p>我们可以看到,第一种做法如果是直接将表映射到entity,那么我们最终得到的Blog类型可能并不是根据业务设计出来的样子,也就是说业务对象为RDB持久化的技术而妥协设计了。第二种方法看起来不错,但已经属于newsql的范畴,和RDB无关。</p>
<p>因此我们要明确的概念是:ORM是为了解决阻抗失配的,没有解决阻抗失配的都不是ORM,包括dapper、Mybatis等等等等(所谓micro orm其实并不是ORM),后者更适合的叫法应该是DMT(data mapping tool),只提供了表和entity的映射或者表达式树的处理。在DotNet这块,真正的ORM只有EF和NH两者(天国的linq to sql也不算,因为其只提供了DB First)。</p>
<blockquote>
<p>扩展阅读:阻抗失配不仅仅是上述问题那么简单,例如OOP三要素——封装、继承、多态,假如你的业务对象存在继关系,那么在RDB中该如何描述?EF中提供了TPH (Table Per Hierarchy,父子类在同一张表,EF自动添加Discriminator字段用于标识属于哪一类型)、TPT (Table per Type,父子类在不同的表,子类表只包含子类属性,通过相同的Id来关联父类表上相同的entity父类数据)以及TPC(Table Per Concrete Type,没用过,也没见人用过,父子类在不同的表,父类的属性在子类表中也会存在,估计是为了优化query)三种方式,大家可以找下资料,在这里不做展开。</p>
</blockquote>
<h2 id="如何优雅地使用orm">如何优雅地使用ORM</h2>
<hr />
<p>正确地使用ORM第一个前提是,项目必须是OOP设计,解析业务后先进行业务对象的建模,然后再通过ORM持久化业务对象的状态。以EF为例,基本排除了DB First以及Model First的做法,因为后两者属于面向数据库设计,所以EF Core只保留Code First除了更为精简外,其实也更符合ORM的实践。</p>
<p>然而:</p>
<blockquote>
<p>这没有解决搜索(query)问题啊。</p>
</blockquote>
<p>在这里我们要了解另一个概念——CQS(命令查询分离)。</p>
<p>CQS最早提出于1988年Bertrand Meyer的《面向对象软件架构》,可以归纳为“原则上一个方法不应该对数据造成影响(增删改)的同时又返回数据”。以是否对数据造成影响我们可以将操作分成两类:</p>
<blockquote>
<p>查询(Query):返回数据,不修改数据,不会产生副作用。</p>
<p>命令(Command):修改数据,不返回数据,遵守单一职责原则。</p>
</blockquote>
<p>在具体落地的项目中,查询往往千变万化,复杂的查询甚至要多表链接(大于3)还要进行聚合处理。其实我们可以看到,这里的查询几乎可以当作是弱报表——而几乎所有的这些查询,都不是OOP的功能。</p>
<p>因此虽然可以实现复杂查询,但ORM并不适用于CQS中的查询(Query)端。更好的做法是将项目的功能分成命令和查询两块,然后只在命令端使用ORM,Query端怎么快怎么来——当然具体实现也可以两边都用EF,但C端要当作ORM用,Q端直接执行sql语句。</p>
<blockquote>
<p>扩展阅读:CQS并不是死规矩,例如stack的pop操作,有返回结果的同时也会改变stack本身。CQS落实到实际项目中并不是真的将操作简单地分成两类,比较简单的分法是:页面展示的一般是Q端。一些专业的项目C端甚至可以只通过Id来获取业务对象,这有助于仓储层的服务化以及事件溯源(Event Sourcing)的实现,以及在分布式系统中处理幂等。</p>
</blockquote>
<p>最终总结,正确的使用ORM并没有想象中那么简单也没有那么难,其实也就是两条经验之谈:</p>
<blockquote>
<p>1、必须是OOP设计,先使用Code First建好业务模型后再考虑如何持久化。</p>
<p>2、不能为Query而妥协设计,如果真的出现相对复杂的查询,直接CQS,Q端可以使用Dapper甚至Ado.net实现。</p>
</blockquote>
<p>到了最后聊聊一些题外话,现在已经有各种DDD框架,但用了DDD框架并不代表你的项目就是DDD。同时有些DDD框架的实现就有待商榷,例如ABP其仓储层的设计就存在问题,因为它持久化的并不是DO(Domain object)的状态而是PO,这导致ABP的项目更类似于DDD Lite,这个问题我们以后有时间再说。</p>杜小非这是一个由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,这个问题我们以后有时间再说。