(《应当重视ORM》的姊妹篇) 谈谈ORM触发器设计 —— 特别献给那开发开源ORM并有创业梦想的朋友
前一阵子,我写了一个关于我所理解的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实现中看不到正常的触发器框架,甚至看到根本不是由客户去注册反而要求服务端去注册触发器这种可笑的现象,这不能不说是一个硬伤。