ORM末日到了

这是来自Pere Villega的一篇博文,主要从EventSourcing角度谈其对ORM的可替代性。

阻抗不匹配性
大部分信息系统都是持久化存储信息然后查询获取,这大部分是通过RDBMS完成的,不久NOSQL运动促使其成为一个关系数据库的替代者,总得来说,我们需要一个存储区域来保存数据。

但是,这不代表没有问题,流行的开发模型是基于面向对象编程语言配以关系数据库,这种组合有致命的问题:对象和关系阻抗不匹配性,换句话说,他们并不能在一起工作得很好,这其中有许多原因。

这不是一个新问题,人们也在艰难努力提供新的解决方案以消除痛苦,从一个开发者角度看,ORM可能缓解一下这种不匹配问题,即使只是稍微低,像Hibernate这样的工具负责与关系数据库层打交道,这样开发者不必花费时间在这上面,但是这并不完美,由于抽象泄漏定律使得问题变得棘手,任何使用过Hibernate的人都发现通过其框架实现的查询都不够完美,那就意味着开发者必须钻研得很深以便知晓如何告诉框架让它按自己的意图行事,这也通常意味着你进入了底层低级别,包括数据库层,阻抗不匹配问题又冒出来了。

这样,ORM作为一个完整的解决方案失败了,但是这是一个严重的问题,因为我们开发者已经为之付出了很多努力,在Scala的关系数据库列表中,我发现有6个不同的工具提供Scala和持久层之间的通讯联系,我们只是讲一种语言,还没有谈到Python或Java等类似名单,也不包括很多自己开发的内部ORM系统,他们是好的工具,是可以产品化规模使用的。

现在我们每年有多少人时投入其中呢?

即使我们开发者全部迁移到函数范式,Java 8的发布已经确认,即使NoSQL成为一个可行的候选方案,我们可以消灭了阻抗不匹配,但是换还要使用转换层来与关系数据库通讯,还需要转换我们的数据对象格式以便它们适合存储,你认为作为一个文档型的数据库MongoDB可以解决这个问题,如下:


query = new BasicDBObject("i",
new BasicDBObject("$gt", 20).
append("$lte", 30));

cursor = coll.find(query);

错误的方向?
如果我们花费这么多人时在这个问题上,它还会引起头痛,也许是我们的方向错了?

在应用中每个新组件都会增加系统的复杂性,这是一个好的复杂性,比如微服务,给你一个清晰的优势,即使增加了部件的数目,也有坏的复杂性,得学习像Hibernate这样一个完整的框架,这对于只是想将我的数据保存起来来说是过分了点。一个服务管理应用状态的存储是正确方向,但是当你在应用中有领域对象,其包含的数据和需要保存的数据有着完全不同的模型时,这就浪费时间了。

诚实讲,我们很多人这么做,这是很自然,以至于未觉察没有什么不对,直至正确的方式出现,Akka Persistence就是这样在正确方向出现的一个工具。

当然,我们不能欺骗自己,它也是一个必须学习的框架,它很可能一旦投入苛刻的生产环境,原先所有的承诺将会失败,但是请看看下面论述:

Akka持久框架提供了一个Event Sourcing模式的实现,可以忘记存储层阻抗不匹配,你的源码就是你的领域对象,你将变化存储到这些领域对象中,你能复制系统的任何时刻的状态,通过关注新的状态,不会有事务性问题出现(不修改原来的数据,永远只是新增数据,就没有多线程争夺,也就不需要全局事务JTA或数据库锁等),因为在数据层不再有对旧数据的修改发生,你能够任意截获随时间变化的状态序列,而关系数据库只能保存最后时刻的最后状态。

为什么要浪费时间在ORM或其他的选择吗?持久性一下子就变成了支撑系统,而不是你应用程序的主要组成部分之一。数据都驻留在你的领域中。你的领域对象是业务真相的源泉,而只有在确保它们的状态不会丢失的情况下,你才会将它们从内存中移除。这是很简单的。

为什么它会成功?
正如我所说的,有可能是我还没有考虑到技术问题。我可能无论是在用例或性能上存在盲目乐观的问题。也可能是因为它仅使用该存储系统来构建一个解决方案是不可行的。我可能是错误的。Akka持久框架有可能比目前的ORM在性能上轻微的差些,但是我不怀疑Typesafe有一批聪明的工程师会持续解决这些问题。

这坚信必定会成功的理由是:简单是王牌,看看这几年流行的技术:MySQL PHP Ruby MongoDb,它们都不是性能最优的,但是有短处,但是它们还是流行了,有很多开发应用,为什么?简单,如果这些技术有一个共同特点就是易于使用,任何开发者只要花几个小时学习基础就能使用它们。

下面是持久层了,请忘记学习SQL, 事务上下文 或ORM等复杂的技术,你的领域对象就是你的数据,这将成为大规模主流,增加软件的产品性,能够去除因为使用ORM导致的大量BUG和带来的扩展性问题,同时,你有一个简单且性能不错的框架。

我但愿我是错的...

翻译完:
相关参考:
Reactive宣言的思考
使用Akka实现Reactive DDD
ORM真的不适合DDD

标题太惊悚了,世界上就没有绝对的,
在能像hibernate有大量成熟的大规模应用之前是不会消失了。

持久化优雅的目标应该是能“像访问内存集合一样访问数据库”,ORM、EventSourcing手段而已
关系数据库或非关系型数据库都最终归于对象集合!
描述领域实体关系就剩Collection等基本数据类型了,也就谈不上“抽象泄露”

.net 代码部分片断



public ICollection<T> CreateCollection<T>()
{
return CreateCollection<T>(id => { });
}

public ICollection<T> CreateCollection<T>(Action<object> callback)
{
var items = new ObservableCollection<T>();

var changes = Observable.FromEventPattern(
(EventHandler<NotifyCollectionChangedEventArgs> ev)
=> new NotifyCollectionChangedEventHandler(ev),
ev => items.CollectionChanged += ev,
ev => items.CollectionChanged -= ev);

var adds =
from c in changes
where c.EventArgs.Action == NotifyCollectionChangedAction.Add
from item in c.EventArgs.NewItems.Cast<T>().ToObservable()
select item;

var removes =
from c in changes
where c.EventArgs.Action == NotifyCollectionChangedAction.Remove
from item in c.EventArgs.OldItems.Cast<T>().ToObservable()
select item;

adds.Subscribe(item =>
{
object id = Session.Save(item);
if (id != null)
{
callback(id);
}
});

removes.Subscribe(item =>
{
try
{
Session.Delete(item);
callback(true);
}
catch
{
callback(false);
}
});

return items;
}

查询接口(你懂的),剩下就是linq去解决问题了。


public interface IQuery<TResult> where TResult : class
{
IQueryable<TResult> Execute();
IQueryable<TResult> Execute(int timeout);
IQueryable<TResult> Execute(IDictionary parameters);
IQueryable<TResult> Execute(IDictionary parameters, int timeout);
}

自己动手丰衣足食,简单封装以下,其实思想都是一致的
[该贴被clonalman于2014-04-19 23:11修改过]

这篇文章主要是讲如何存储数据,而不是如何查询数据,查询数据方面使用LinQ或Java8的Stream已经做得不错:使用Java 8 stream处理数据

过去我们存储数据到关系数据库或NoSQL,存储的是应用程序的当前状态,至于如何导致状态变化到当前这个值的原因没有存储。根据状态模式总结,导致状态变化的一般是事件,存储事件是EventSourcing的主要特点。

存储事件总是存储的是一个个新的事件,比如你如果要修改删除一个已经存在的数据,如果按照存储状态的思路,那就先从数据库找到要删除的记录,然后修改它或删除,而按照EventSourcing思维,你必须发出一个新的修改或删除事件,Akka持久框架应该能存储这个修改或删除事件,这个新事件发出后,导致内存中领域模型实体内状态发生变化,比如一个论坛Forum实体内部状态原来是20个帖子的状态,你发出删除一个帖子新事件后,其状态变成19个帖子。至于数据库存储的是删除事件,而不是19这个状态。

如果机器重启,系统会根据存储的历史事件流进行重新播放,像放电影一样,一直放到关机前的状态,也就是19个帖子这个状态,但是19这个数据永远不会存储到数据库中了。

2014-04-20 07:36 "@banq"的内容
一个论坛Forum实体内部状态原来是20个帖子的状态,你发出删除一个帖子新事件后,其状态变成19个帖子。至于数据库存储的是删除事件,而不是19这个状态。 ...

知道是关于如何存储数据,是不是可以换一种描述方式:

Forum实体内部状态原来是20个帖子,是一个Collection,从Collection中删掉一个帖子剩下19个帖子。(幼儿园题目,这样表述是不是更加清爽)

存储的是删除事件还是对象实体本身,对于对象集合来说透明的,
上面的例子ICollection<Entity>是hibernate的session的save与delete直接影射到Collection的Add与Remove方法上,EventSourcing同理只要添加事件、删除事件影射到集合操作方法即可。

一台服务器的内存是有限的,不可能把整个业务系统的操作都进行重放,即使可以重放也不能模拟事件发生时的硬件、网络和第三方系统的状态。
我们把领域对象序列化成XML,然后整个存进MSSQL也是没问题的,XPATH查询也很方便。NOSQL号称能存放无模式数据,传统的关系型数据库也能做到。

2014-04-20 08:20 "@clonalman"的内容
存储的是删除事件还是对象实体本身,对于对象集合来说透明的 ...

是的,因为对象集合本身是一种状态范畴,是一种数据结构范畴,这就如同Java 8中集合Collection和Stream区别一样,见:http://www.jdon.com/idea/java/java8-stream.html

Collection是关于静止的数据结构,而Stream是有关动词算法和计算的。前者是主要面向内存,存储在内存中,后者主要是面向CPU,通过CPU实现计算的。

举例将一个影片存储在DVD盘上,这是一个集合,因为它包含整个电影的字节数据结构,而这个影片被放在互联网上,我们通过视频软件去观看它时,它实际是被流化了,它变成了一个字节流,流是与时间有关的概念,而数据结构是与时间无关,不会随着时间变化变化,流正好相反,随着时间不断地动态变化,如同水流一样潺潺不断。

传统ORM存储的是状态,数据结构;而EventSourcing如同Stream一样,是侧重计算算法的,是一种事件流。

[该贴被banq于2014-04-20 09:38修改过]

2014-04-20 09:37 "@banq"的内容
Collection是关于静止的数据结构,而Stream是有关动词算法和计算的。前者是主要面向内存,存储在内存中,后者主要是面向CPU,通过CPU实现计算的。 ...

动态过程与静态结果的关系,二者都很重要,如果只存事件而不存实体,统计分析也将是一个麻烦事

不要太理想了哈!

event sourcing可以解决CQRS架构的command side的聚合的状态存储和还原。

但是query side呢?不用orm吗?不用关系型db吗?如果不用,那如何实现各种复杂的查询?event sourcing是无法支持复杂条件的列表查询的。

难道你的系统中的所有只用户UI显示的查询都是直接查询内存中的聚合根的?很难想象这样如何支撑各种复杂的查询。传统关系型数据库,虽然和oo有天然的阻抗,但如果是CQRS架构的query side,完全可以使用ORM框架,将数据放在关系型db中,通过SQL实现各种查询,不是很好吗?数据库中有高效成熟的索引设计,如果抛弃数据库,那难道每个分页查询都要到内存中,遍历每个对象的某个属性,然后自己实现分页逻辑。就算能高性能实现,但是你内存够大吗?放得下这么多的聚合跟对象吗?
[该贴被tangxuehua于2014-04-22 22:19修改过]
[该贴被tangxuehua于2014-04-22 22:22修改过]

查询用linq就行了,内存是个大问题,总不能都靠nosql来支撑吧

就算内存中放的下所有的聚合根,但问题是有必要吗?值得吗?

一个系统中很多聚合根都是不活跃的或者生命周期已经结束了的;内存中应该只需要放活跃的,最有可能被用到的聚合根,基本不会使用或者生命周期已经结束了的聚合根,完全没必要放在内存中;他们只需要放在关系型数据库中,需要在UI上显示的时候通过SQL查询出来即可。

如果command side需要操作某个不太活跃的不在内存的聚合根,那可以通过event sourcing技术从eventstore还原这个聚合根,然后放回到内存,然后做操作即可。因为大部分活跃的聚合根都在内存,所以只是偶尔会需要用到event sourcing技术还原聚合根,所以不会影响command side的处理性能。这样就既能让内存存放有价值的数据,也能做到让command side快速处理活跃的生命周期还没结束的数据。实际上对于生命周期已经结束的聚合根,完全没必要留在内存了。因为我们不会再对他们做处理了,不是吗?

对于query side的数据库查询,因为有索引的支持,可以实现快速查询。如果单个数据库的查询有性能瓶颈,那可以使用多个备份的副本,将查询请求分摊到不同的副本,这样就能解决查询的问题。这就是一主多备的设计,mysql很容易支持。

所以,我完全不知道为何orm会末日到了?难道大家的db查询都是手写sql的?

大家讨论都挺好,我下面进行纯粹分析一下,今天脑子比较好些。

对象和数据库都是一种静态的数据结构,而SQL与LinQ或Lambda表达式或Stream都属于一种动态算法过程。两个分别对应内存和CPU,如同哼哈二将,阴阳一体,一个系统由这个两个组成比较和谐。但是有时会组合错误。

和谐:
数据结构+算法过程

不匹配阻抗:
数据结构+数据结构
算法过程+算法过程

使用ORM相当于对象和数据库两个数据结构,因此存在阻抗矛盾,相反,因为SQL属于算法过程,SQL+数据库反而比较协调,但是做些CRUD还可以,应对复杂的业务,需要翻看各种数据表设计才能明白业务,数据库表又混杂了各种技术性能优化等技术方言,相当于方言夹带普通话,只通普通话(代表业务语言)的人不容易听通,增加沟通的难度。

后来有了Java等对象语言,就有了SQL+数据库+DTO数据Java对象,这就是大部分系统的特点,两个数据结构发生阻抗,改了数据库表还要改Java对象。

Hibernate试图协调对象和数据库两种数据结构矛盾,其实是清官难断家务事,捣浆糊而已,因为从逻辑上对象和数据表两者本来就没有必要搞到一起,搞个映射对应。

Hibernate出来很讨人喜欢,但是做些简单CRUD没有问题,随着项目深入,加上程序员数据库思维影响,使用Hibernate觉得隔着靴子瘙痒,其实我一直认为,如果程序员有阅读一个框架的源码冲动时,说明抽象泄漏已经发生,泄漏出来的坏味道已经让使用者心神不宁了。最终现实是:使用Hibernate的人是SQL与Hibernate两个要一起掌握,其实是增加学习成本。

EventSourcing是因为DDD这个用对象分析复杂业务的方法出现以后才出来,ES属于算法,领域对象聚合根实体属于数据结构,两种阴阳搭配协调,如同SQL+数据库一样干净。

当然,LinQ+数据库或Stream+Collection都属于和谐干净的阴阳合体。OO+函数也是。

总之:只要一个系统只有一个数据结构+一个算法过程,这个系统本身就干净和谐,只有系统架构干净简单,才不至于因为架构复杂而导致项目失败或导致项目维护困难。聪明的人不能因为自己发明解决问题的方法干扰了自己解决问题的目的。

赞同banq上面的说法,哈哈