(《应当重视ORM》的姊妹篇) 谈谈ORM触发器设计 —— 特别献给那开发开源ORM并有创业梦想的朋友

以专业开发人员为伍 2008-10-02 01:33:00
加精
前一阵子,我写了一个关于我所理解的ORM基本功能设计规格的帖子,那基本上是我所理解的ORM设计的最重要初步。这里我再简要讨论一下另一个重要部分,既看似比较简单的“触发器”功能。


例如我们需要开发一个“注册用户”工程,这个工程显然要给将来各个应用系统共享,也就是说将来的各个系统依赖于它。这个系统中定义了用户类,保存了用户对象。那么当用户被删除,或者其某些属性修改了,我们要通知其它子系统,怎么办呢?调用其它子系统的功能吗?有设计知识的架构师不会这样轻易回答,因为前面已经说了各个系统依赖于它,而不是它依赖于在它之后才开发的子系统。这就需要各个子系统能够注册事件处理程序到用户对象类中。但是这看起来有点麻烦,就是以往在使用事件时我们通常都是将事件处理程序注册到一个对象上,而这里我们是开发ORM,我们希望处理程序针对“一类”对象均自动注册,这两者的编程方法有区别。


我们的ORM是一个完整的面向对象风格的数据库工具,它不可能依赖于底层关系数据库来实现一个纯粹工作在应用程序中的触发器,必须依靠自身力量解决。


不要告诉我“这就是AOP”就完了(当您并不能直接拿出一个成熟的AOP代码给我直接使用在此系统上时),因为我接下来要描述处理这个过程的设计规格,所以我其实并不管我的做法是不是AOP,我在此只是这样实现这个系统的:

public static class RegisterCallbacks
{
public static void RegisterCallbackAfterCreate(Type type, CallbackHandler handler);
public static void RegisterCallbackAfterDelete(Type type, CallbackHandler handler);
public static void RegisterCallbackAfterUpdate(Type type, CallbackHandler handler);

public static void Created(IDomainClient sender, object obj);
public static void Deleted(IDomainClient sender, object obj);
public static void Updated(IDomainClient sender, object obj);
}


这个类是一个static类,它是ORM的一个独立的信息管理类,它记录了各个类型的数据当在数据库中更新(Create、Update、Delete)时应该触发哪些方法(handlers)。显然,同一类型同一更新方法可以注册多个handler。而handler的类型定义如下:

public delegate void CallbackHandler(IDomainClient sender, CallbackArgs args);

public class CallbackArgs : EventArgs
{
public object Obj;
}


IDomainClient 是我定义的数据库打开之后的操作接口(提供了事务功能),其设计规格解释可以参考我开头提到的那篇帖子。

假设一个“发送欢迎短信任务安排”的类需要被“用户”这个类所触发,也就是说一旦一个用户被新增入系统数据库就要触发给他发送一个欢迎短信的任务记录,那么我们(通常在发送欢迎短信任务安排这个类中,当然也可以写在其它代码文档中)应该可以见到这样的代码:

[RegisterCallbacks]
public static void Register()
{
RegisterCallbacks.RegisterCallbackAfterCreate(typeof(用户), 准备发送欢迎短信);
}

private static void 准备发送欢迎短信(IDomainClient db, CallbackArgs args)
{
var obj = args.Obj as 用户;
db.Save(new 发送欢迎短信任务安排{ 用户=obj, 开始时间=DateTime.Now, 过期时间=DateTime.Now.AddHours(1) });
}

这个代码告诉 RegisterCallbacks 这个管理类,当“用户”类型的对象被数据库创建时要触发“准备发送欢迎短信”这个方法。


在 RegisterCallbacks 中并不关联具体的数据库。一旦通过某一个数据库对象实例新增了一个对象,这个数据库对象就会去调用静态方法

RegisterCallbacks.Created(thisDb,theObject)

这样 RegisterCallbacks 就会帮助数据库调用 theObject 的所有类型(包括各层父类、接口)在 Create 操作上所需要的触发方法。这样,各个在ORM之后开发的领域对象工程,以及各种不同的数据库实现,可以在ORM帮助下实现触发器功能。


对于Update、Delete操作,其机制完全与Create操作一致。




我们使用关系数据库的经验可以告诉我们,触发器所修改的数据是可以回滚的。因此,这个“准备发送欢迎短信”中无需写
    
    db.Commit()

因为如果

Save(用户);

触发它,紧接下来会被 Rollback 命令回滚,那么触发器中的更新当然也应该回滚。同样,如果调用方 Commit,这时候这个新增发送欢迎短信安排记录才算真正保存到数据库文件中。在这个触发器程序执行过程中,可以从 db 查询到所有调用方的代码已经更新到数据库但是还没有 Commit 的数据,尽管这些数据可能随后被 Rollback。说白了,触发器是在一个事务中对象被更新到数据库但是还没有进行 Commit 或者 Rollback 时触发的。




但是,我们什么时候去调用 Register 方法呢?是在领域类型的.cctor中吗?显然,这时候有点晚,因为.cctor只有在类型第一次被真正用来实例化对象时才执行。如果我们启动一个应用系统,新增了一个用户,此时还没有使用过发送欢迎短信任务安排对象,我们就无法让这个触发器被注册到 RegisterCallbacks 中。


为了解决这个问题,我首先定义了一个标签

[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=false)]
public class RegisterCallbacksAttribute : Attribute
{
}

这个标签用于注明类型中上述Register静态方法。当数据库对象第一次实例化时,它应该去遍历当前应用程序域中所有的引用了这个ORM工程的assembly中的每一个类型,从类型中找出具有 RegisterCallbacksAttribute 这个标签的静态方法(为了方便,我设计为public或者private均可,这实际上由各个实现了 IDomainClient 接口的数据库对象类来决定),并且反射执行它。当然,仅仅执行这个静态方法一次就足够了。




我最后总结一下主要的设计初衷:虽然我们使用OOP工具进行数据持久化方面的编程,但是往往在许多人那里与面向实际应用领域的OOAD的结果并不一致。例如对于一些没有经验的软件工程师,他可能认为成百上千类领域对象的各个实例的更新是调用那些理应依赖于这个对象影响的对象的方法来进行级联更新的。运行时当然是这样的,但这是排好队干活的“机器”所做的事,而不是设计师所关心的大事。设计师负责研究把机器放进工位使其依照一定关系行事的策略,即那个“you don't call me, we’ll call you”的好莱坞原则所表现的灵活性。然而,我们在不少ORM实现中看不到正常的触发器框架,甚至看到根本不是由客户去注册反而要求服务端去注册触发器这种可笑的现象,这不能不说是一个硬伤。
...全文
2614 82 打赏 收藏 转发到动态 举报
写回复
用AI写文章
82 条回复
切换为时间正序
请发表友善的回复…
发表回复
glan_ou 2010-03-18
  • 打赏
  • 举报
回复
mark!!!
bluedoctor 2010-02-11
  • 打赏
  • 举报
回复
这里都是高手在过招阿,楼主用的也是基于反射的ORM方案,这里看看我的简单方案,完全不用反射
PDF.NET数据开发框架

另外对于第50楼,楼主并没有给出解决方案,我建议这样的复杂查询可以将查询结果再重新映射成一个新的实体对象。当然如果这样的查询很多都手工去写那太夸张了,但是可以借助工具,我的框架提供这样的功能,那就是SQLMAP。
usr33322 2009-12-09
  • 打赏
  • 举报
回复
软件架构QQ群 群号76395176

软件架构,UML,开发语言不限,架构是主题,软件一般问题也鼓励讨论,情感交流


工作经验5年以上,年令30以上
  • 打赏
  • 举报
回复
例如我们写一个业务逻辑:

using(var db=OpenMyDB())
{
var users=from User u in db
inner join Group g on u.推荐人 equals g.负责人
where g.Name=="无极集团"
select u;
foreach(User u in users)
{
u.工资 += 1000;
db.Save(u);
};
db.Commit();
}

这里给某个集团负责人推荐的所有用户的工资都增加1000元。设计User和Group类型时,我们不要求设计者依赖于什么特定的接口,它就是普通的通用对象就可以了。
  • 打赏
  • 举报
回复
虽然我将查询接口设计为

IEnumerable<T> Cast<T>();

但是我实际上现在都是返回 IQueryable<T>的,其实也就是利用了那些底层数据库的Linq实现。设计为这个接口,那么如果哪一天我把“某个网站、当前的屏幕像素”等当作一个数据库源的时候,那些东西不一定有Linq实现给我一用,我也可以方便地自己先用不太高效的笨办法实现这个接口从而支持高层的Linq查询调用。我不知道你所选择的 ActiveRecord 有没有一个通用的查询接口,ORM中的查询接口的实现也是很重要的,但是很多ORM都使用“字符串”来声明,这看似万能实际上而实际上则不够好用。
  • 打赏
  • 举报
回复
其实我的 IDomainClient 接口设计很简单,这里可以贴出来:

using System;
using System.Collections.Generic;
using System.IO;

namespace Domain
{
public interface IDomainClient : IDisposable
{
IEnumerable<T> Cast<T>();
void Save<T>(T obj);
void Delete<T>(T obj);
void Commit();
void Rollback();
FileInfo File { get; }
}
}


这就是一个完整的面向对象数据库需要面对业务层实现的接口。我用3种数据库实现了这个接口,但是目前只有一种经过了成千上万次的在多个工程中的自动化测试(我在.net中利用System.Data.SqlClient命名空间下的SQL Server方法与之进行了对比,有不太多的超过5万数据,证明在ORM最不擅长的最坏的情况下也并不比SQL Server的查询速度慢),另两个因为Cast有BUG而几乎没有测试过(我也没有什么动力去实现它)。

我的接口中,obj是“任意”类型的对象。在早期我也考虑过让对象一定要从一个ORM规定的类型上继承,后来发现这有些多余,许多.net framework中已经有的或者别人开发的dll中的对象就不能被装入数据库了,于是改过来了。我看到你说你是从DDD出发进行设计的,我不知道是不是DDD也比较僵化,至少我在开发中从来不用去再在真实的业务对象之外再去考虑“实体”这种概念。
  • 打赏
  • 举报
回复
今天刚刚又看了这个帖子,看到了后边73楼的回复。我看了回复中所引的ActiveRecord的开发例子,确实,ActiveRecord已经将Attribute减到很少了,但是它是基于NHibernate实现的所以继承了人们对NHibernate的批评。他看上去只有这个“底层”鸡肋,而本身的简洁的架构是比大多数ORM都好用的。
Tomato77 2008-11-04
  • 打赏
  • 举报
回复
学习
fsy123456accp 2008-10-31
  • 打赏
  • 举报
回复
楼主很强悍啊 我来学习,顶一个了
acqy 2008-10-31
  • 打赏
  • 举报
回复
[Quote=引用 2 楼 sp1234 的回复:]
hibernate基本上是基于“配置”的思路去开发的,它假设你首先使用关系数据库系统设计数据库结构,而后创建配置文件......

但是,这只是ORM的一种流派。

我的两篇短文中所讨论的,是自动反射对象结构(当然,反射的结果仅需进行一次就保存起来复用),然后自动地创立、更新数据库,无需去操作关系数据库。例如你在开发中使用c#的class语句定义了一个“用户”类,它有20个关于人员的属性,紧接着你就可以直接在程序中用一条…
[/Quote]

非常支持

目前的NHibernate也好,Castle ActiveRecord也好,甚至是似像非像的LINQ to SQL,都有这个问题,就是在业务对象发生更改的时候,md数据库还得跟着手动滴一起修改,这就使得ORM中的这个M很难维护,尤其是那种类似ERP之类的信息系统,客户变更是随时随地的,这时候维护M的代价就变得非常庞大。我始终坚持一个原则,就是数据库仅仅是保存业务对象“冻结(持久化)”状态的一种外部存储,应该是数据库按照业务的变更而随需应变,而不是业务被数据库牵着鼻子走,这样就把因果关系搞反了。

从DDD的角度考虑,Castle ActiveRecord的表现比Hibernate/NHibernate更差,当然,我说的是从DDD的角度考虑,事实上要具体情况具体分析。比如我早断时间用DDD的思想自己随便实现了一个非常简单的购物系统(地址:http://www.codeplex.com/StoreDDD),因为在DDD的基础上用了ActiveRecord而被N多人批评。此后我深感问题的严重性,但从这个应用本身来讲,或许ActiveRecord比NHibernate更合适。

为什么我被N多人批评?因为我用了Castle ActiveRecord(简称AR,下同)框架(也就是MF所说的ActiveRecord模式的实现框架),它可以看成是基于NHibernate上的ORM框架,只不过将所有的配置信息完全以属性Attribute的形式来代替,这样大大简化了mapping file的维护工作。那为什么我不能用AR?请看下面的代码:


[ActiveRecord(Table="Students")]
public class Student : ActiveRecordBase<Student>
{
[PrimaryKey(Generator=PrimaryKeyType.Identity)]
public int Id {get; set;}
[Property(Nullable=false, Unique=False)]
public string Name {get; set;}
// ... 以下省略xxx行
}


事实上Student是一个业务实体,但上面却穿插着N多跟数据库相关的东西,比如PrimaryKey等,而这些信息本身不应该由业务实体维护的(也就是说,业务实体不应该知道这些信息)。如果有一天我的外部存储从关系型数据库转换为其它的plaintext,那这些信息又有什么用呢?

回过头来看我的那个StoreDDD,一堆的ActiveRecord。难怪被人批评。但是仔细想想,我系统本身并不大,充其量就是个能够实现基本功能的购物网站,我开发的资源也很有限,既然ActiveRecord能够解决我的问题,又能够在开发上提高效率,为何不去使用呢?

写了这么多,这也是我最近所获得的一些信息,有不对的地方还请大家批评指正。
acqy 2008-10-31
  • 打赏
  • 举报
回复
楼主是强人,帮顶!
很早以前我看过一本书,刚刚查了一下,书名是“Applying UML and Patterns”,讲UML以及GRASP的。第38章“Frameworks, Patterns and Persistence”专门就是讲ORM的实现思路的。有兴趣的可以买本来看。
cnapc 2008-10-30
  • 打赏
  • 举报
回复
其实处理这样的问题应该归结于另外的领域"项目变更".
这种情况的出现,不是要求代码开发人员改两行代码就可以的,也不应该让ORM来作这样的工作啊.
如果要这样的功能,那也简单啊,作个新的表结构,复制数据过去就好得,体现在ORM中应该不需要超过50行代码,
性能也取决于SQL服务器和ORM没什么关系,因为不管怎样处理,这样的时间是不可避免的.


[Quote=引用 47 楼 sp1234 的回复:]
引用 35 楼 mengweilil 的回复:
2、除非你打算自己开发一个数据库(OO数据库),否则,ORM最后总要映射到数据库上,所以,随心所欲修改表结构是要付出代价的——试过几万条记录修改表结构吗?


哦,我把这句话理解为数据库中:有几万条记录,此时要修改一次表结构其时间也是不可能接受的。

这个时间方面的估计并没有考虑到所谓修改表结构是用在什么时候。实际上,主要是开发的时候。

关系数据库允许修改表结构,并…
[/Quote]
garylijia 2008-10-29
  • 打赏
  • 举报
回复
学习了。。。虽然看得不是非常懂,大体还是有点感觉了!
qinhl99 2008-10-28
  • 打赏
  • 举报
回复
很有深度,学习了
andyhooo 2008-10-21
  • 打赏
  • 举报
回复
[Quote=引用 5 楼 jikun6666 的回复:]
路过学习了,谢谢
[/Quote]`
ojekleen 2008-10-16
  • 打赏
  • 举报
回复
收藏先,回头再看
老纪@ToB Dev 2008-10-15
  • 打赏
  • 举报
回复
ORM看起来不错,学习了
afeng_06 2008-10-15
  • 打赏
  • 举报
回复
写的不错啊,学习了。
andy1414 2008-10-13
  • 打赏
  • 举报
回复
学习学习
HellMoxi 2008-10-13
  • 打赏
  • 举报
回复
大师啊。。。
加载更多回复(60)

13,190

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 分析与设计
社区管理员
  • 分析与设计社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧