DbEntry 之 ORM

ORM 是 DbEntry 的核心,这一点在刚开始时就是方向,只是在执行 SQL 的代码已经比较稳定后才开始做。

  我不是一个迷信 Property 的人,所以,最早的 DbEntry 的 ORM 是针对 Field 的:

public class User : IDbObject
{
    [DbKey] 
public long Id;
    
public string Name;
}

  我习惯每一个表里都有一个 Id,不管其中是否有什么“唯一”的列,而且,它的名字就叫 Id,而不是什么 UserId,所以就不用在代码里写 user.UserId 这种代码,我也推荐其他人这样做,所以在 DbEntry 中,虽然也可以像上面的代码一样自己定义 Id,不过,鼓励的做法是继承自一个已经有 Id 的系统类:

public class User : DbObject
{
    
public string Name;
}

  一个小插曲,以前为电信做的一个项目,客户要求不能使用 Identity,我和同事都觉得不可理解,沟通过几次,比较困难,就用一些迂回的方式解决了。后来在网上查了,才发现,早期 Sybase 的版本 Identity 的实现有 Bug,有可能造成问题,当然现在 Sybase 应该已经解决这个问题,而且,其它厂商的数据库也都没有这个问题,那么是否应该因为某个数据库的某个 Bug 而禁用 Identity 呢?我的答案在 DbEntry 里已经很清楚了。

  在 NHibernate 中,有很多种主键生成方式可供选择,Identity 啊,序列啊,NHibernate 生成啊,如果你用 Sql Server,选择 Identity 方式,那么在 Oracle 下,你需要修改它为序列方式。在我看来,不需要这么多的选择,用户真正关心的,并不是主键的生成方式,用户真正需要的就是一个自增关键列,所以,在 DbEntry 中,定义主键的方式就是上面那样,如果用户配置使用 Sql Server,就会自动使用 Identity 方式,如果用户配置使用 Oracle,就自动使用序列方式。DbEntry 也支持 GUID 作为主键,DbEntry 会自动生成它,所以,调用代码和使用 int、long 型的主键没什么区别。

  在 NHibernate/Castle ActiveRecord 里,一个字符串,如果没有任何额外定义,其长度是一个比较小的值,大概是250左右吧,如果要定义一个 TEXT 类型的列,需要定义其长度为一个很大的整数,我记不得那个整数,每一次都是复制的;在 DbEntry 里,一个没有任何额外定义的字符串,就是 TEXT 类型的,除非用户定义了长度。这个原则对于二进制型的列也相同。另外,DbEntry 中的长度定义,包括了定义最小长度项,它和最大长度一起会被用于调用数据库前的数据校验(可选),在 Hibernate(Java版,比 NH 先进点儿) 里,字符串在数据库里的长度定义,和校验的长度定义,隶属于不同的组件,所以需要把最大长度写两遍,而在 DbEntry 里,避免重复代码是一个重要的原则。

public class User : DbObject
{
    
public string Text; // NTEXT

    [Length(
1050)]
    
public string Fixed; // min:10, max50
}

  在 Hibernate 里,一个列,如果没有标记 NotNull,则默认它是支持 Null 的,而我认为,数据库中,缺省不应该把列设置为 Null,除非你明确的想要它是可为 Null 的,所以,在 DbEntry 里,一个列,如果没有标记 AllowNull,则默认它是 Not Null 的。这里有个例外,就是对于 ValueType,使用可空类型来定义其是否可为 Null,而不需要,也不允许写 AllowNull:

public class User : DbObject
{
    [AllowNull]
    
public string Null;

    
public string NotNull;

    
public int NotNullInt;

    
public int? NullInt;
}

  和大多数其他的 ORM 比较大的不同点还有一个,就是,DbEntry 在每一次用户调用 Save 的时候,都立即生成 SQL 并提交给数据库,而不是最后来一个总的 Commit,这和我习惯的数据库访问方式相似 —— 我可以控制我希望调用 Save 时机 —— 希望这也和你的相似。另外,就像在上一次说的,源自 DbEntry 执行事务的方式,只要我们定义了 UsingTransaction,不管是 ORM 发出的数据库调用,还是直接调用 SQL 语句,只要在这个 Scope 内,都将在一个事务内,这是符合对于事务的基本预期的。

  Ruby On Rails 的 ORM 组件很有特色,Castle ActiveRecord 就是对它的一个模仿。DbEntry 里也模仿了它的一些特性,比如 SpecialName:

public class User : DbObject
{
    [SpecialName] 
public DateTime CreatedOn;
    [SpecialName] 
public DateTime? UpdatedOn;
}

  这里的 CreatedOn 会在调用 Insert 时被自动赋予当前时间,而 UpdatedOn 会在调用 Update 时被自动赋予当前时间。不同于通常的 u.CreatedOn = DateTime.Now 的方式,DbEntry 总是调用数据库的时间函数来进行赋值,这样可以避免因为 Web 服务器和数据库服务器之间的时间不统一造成的统计误差。另外,SpecialName 还包含 SavedOn,Count,以及用于版本控制的 LockVersion,如果定义了被标记为 SpecialName 的 LockVersion 列,则 Save 的时候会先比较 LockVersion 的数值,如果不同,则不会更新,并引发异常,也就是通常所说的乐观锁方式。

  虽然我并不迷信 Property,但最终还是倒向了使用 Property 的一边。首先是我发现,ASP.NET 的控件,都比较迷信 Property,类似上面的 Model 定义,都不会被自动绑定,而必须明确写出绑定代码才行。另外,在开始关联对象的设计后,越发严重的一个问题是,不应该为没有修改任何属性值的对象调用 SQL,而关联对象的 Save 却需要遍历其子节点并调用其 Save。于是“部分更新”这个概念开始出现在 DbEntry 的构想中。具体是这样,如果一个对象修改了 A 属性,则生成的 Update 指令只含有更新 A 的部分,如果没有修改任何属性,则不生成任何 SQL,对于如何判断对象属性是否修改,我使用的是在属性中调用一个函数告知的方式,而不是保存副本比较的方式。当然,我是很反感写重复性非常高的属性代码的,既不利于书写,更不利于阅读和修改,当时是 .net 2.0 时代,没有自动属性这回事,DbEntry 采用了抽象属性的方式来解决:

public abstract class User : DbObjectModel<User>
{
    
public abstract string Name { getset; }
}

  虽然现在 .net 3.5 中,有了自动属性,不过,因为“部分更新”需要对于属性中插入一个函数调用,所以,仍然需要使用抽象属性的方式。我最近在考虑,是否使用 PostSharp 来进行这些代码的织入,这样,就可以使用自动属性了。另外,虽然鼓励使用抽象属性,DbEntry 现在仍然支持 Field 的方式。

  这样的抽象类,自然是 new 不出来的,所以 DbEntry 提供了一个 New 属性,用于创建新的对象,另外,提供一个 Init 函数,进行数据的初始化,同时,DbObjectModel 还提供了一些泛型函数供其使用:

public abstract class User : DbObjectModel<User>
{
    
public abstract string Name { getset; }

    
public abstract User Init(string name);
}

static void Main()
{
    var u 
= User.New.Init("tom");
    u.Save();
    var u1 
= User.FindById(u.Id);
}

  在 Linq 之前,DbEntry 提供一个有点儿类似的方式进行查询编写:

DbEntry.From<User>().Where(CK.K["Name"== "tom" && CK.K["Age"> 15).Select();

  如果移除 CK.K 以及列名上的引号,则 Where 子句中的语法就和正常的 SQL 几乎一模一样了。这也正是我希望的。另外,这个连贯 API 也支持按页查询,它会自动使用数据库支持的方式来查询请求页,所以 DbEntry 的数据源控件用在分页控件上时,使用数据库分页的方式来查询,而不是缺省的数据源控件那种查询出所有的项,只显示 10 条的方式:

DbEntry.From<User>().Where(CK.K["Name"== "tom").OrderBy("Age").Range(1020).Select();

  其中的 Where 子句,也可以自行拼接,这一点用在复杂查询页面很方便,比 Linq 的拼接要简单的多:

WhereCondition condition = CK.K["Age"> inputAge;
if(needName)
{
    condition 
&= CK.K["Name"== inputName;
}
DbEntry.From
<User>().Where(condition).Select();

  在 .Net 3.5 中,DbEntry 也可以使用 Linq 的方式进行查询,使用的也是类似 .net fx 那种分离的方式,所以可以同时支持 .net 2.0 和 .net 3.5,如果要使用 Linq,多引用一个 Lephone.Linq.dll 就可以了,这里有一点儿小小的不同在于,使用 Linq 的 Model,建议继承自 LinqObjectModel,这个是 .net 3.5 的扩展方法没办法提供 static 的扩展方法造成的,使用上,既可以使用经典 Linq 的方式,也可以使用连贯 API 的方式,推荐后者:

from p in User.Table where p.Name == "tom" && p.Age > 15 select p;
DbEntry.From
<User>().Where(p => p.Name == "tom" && p.Age > 15).Select();
User.Find(p 
=> p.Name == "tom" && p.Age > 15);

  其中,经典 Linq 方式和 Linq to Sql 不太一样的地方在于,在 DbEntry 里,它是直接返回结果集,并不是返回一个中间结果。因为几乎每一篇介绍 Linq 的文章都在重复的教导我们,及时调用 ToList,否则,Linq 返回的结果可能不是我们期望的。我认为这就是错的。打个比方,一个人修一条路,路中间有个坑,现在有两个解决方案,一个是立个牌子,写着“有坑,请跳过去”,另一个是把坑填掉,让行人能够放心的走。我选择后者。这里也许有人要说,那个坑是因为某某原因而必须存在的,我要说,我还是选择把它填掉,然后开一条小路,在那个小路上放这个坑,而让 95% 走大路的人能够放心的走。

  这里介绍的 ORM 只是一个笼统的介绍,很多细节没有讨论,而且也只是 DbEntry ORM 中的一部分,很多功能也没有提及,其它的功能以及其它细节问题,请参阅 DbEntry 的主页文档、单元测试以及源代码。
posted @ 2009-11-22 22:13  梁利锋  阅读(3579)  评论(29编辑  收藏  举报