# 前言:数据库交互之路

 

还记得以前在ASP时代,虽然VB/JS是基于对象的, 但是那时做网页的编程, 基本不会去声明对应数据库的格式的类. 当时的编程方式就是, 用ADO的RecordSet去读取数据. RecordSet可以说是一个集合类. 提供了最简单的, 基于字段名读取数据的Indexer. 例如 <%=rs("CustomerName")%> 这样的语句,也许大家都不会觉得陌生. 如果要进行INSERT/UPDATE/DELETE, 那么就需要手动写SQL了. 那时纯粹是传递一条SQL语句. 这个语句是需要程序员自己构造的. 如果一个表有几十个字段, 那条语句写下来, 单单是双引号和单引号就能让人觉得头晕. 好在,我在ASP上, 只工作了8个月. 就转去ASP.NET了.

 

ASP.NET下,我们有全新的ADO.NET. 那时基本都是从DataSet开始的. 因为它实在和RecordSet很像. 当时只是觉得语法有点不同而已. 重要的一点是, SqlDataAdapter能够执行INSERT/UPDATE. 也就是说, 我可以把一条记录Load下来, 然后修改其中的某些属性, 然后再更新回去. 这个过程并不需要写很长的SQL语句. 出错的机会很少.

 

相比ASP时代. ADO.NET已经是救世主了. 当时的我, 并不觉得DataSet有什么不好的.

 

后来ORM在网上热起来. 我突然发觉row["CustomerName"]是个很笨的方法. 在我的桌子旁边, 总有一张数据表结构的印刷版. 当我要操作某张表, 我就把那张纸拿出来对照. 那实在是很麻烦. 我也慢慢体会到那种强类型的对象编程的好处. 不是虚拟继承的好处, 而是当我在vs.net上打一'.'的时候,属性就会被列出来了.

 

是的,我并不关心面向对象有什么好处. 我只希望我的编程工作能简单点,尽量少出错. 可惜那时候的ORM框架并没有解决一些问题. 有些要我先设计对象,然后自动生成数据库. 有些则需要写一大堆的xml文件. 有些生成工具, 要不断地执行用于更新代码. 我自己后来也实现了自己的生成工具, 不过效果也不太理想.

 

后来的一段时间, 我没有去弄那些东西了. 我采用POCO的方案. 老老实实地把数据从SqlDataReader复制到POCO去. 插入和更新的时候, 老老实实地写那种很长的语句. 这个方案其实已经比DataSet好很多了. 起码数据是强类型的对象, 在VS.NET下有提示. 而SqlScope也帮我省了很多代码.

 

后来我渐渐过渡到一种简单的DomainModel+DomainService的方案.我把对象的属性都弄成ReadOnly,把数据都改为internal. 这样就能保护我的数据的逻辑. 这个模型一直用到现在.

 

 

世界在发展,编程的模式不会一成不变. . 在我离开CSDN后,我曾经有一段时间脱离社区. 当我回头时, 才发现自己已经脱离编程世界好久似的. 我充满恐惧, 恐怕被变化所抛弃. 于是我重新返回社区. 先是潜水, 去读博客, 了解这个世界这今年来技术有什么更新. 博客园帮了我很多. 像博客园里很多人都自己做了一套ORM的实现, 正是对以往的数据库操作模型的的不满. 而我也深受他们的影响. 我想起了以前写的文章 <<用 System.Reflection.Emit 自动实现调用存储过程的接口>> http://blog.joycode.com/lostinet/archive/2004/11/19/39238.aspx . 我决定沿用那个方式, 去实现一个全新的ORM.

 

 

# AbstractRecord的基本概念

 

一开始是这样的, 我想用interface来表示一个数据库的记录. 例如

[Table("Employees")]

public interface Employee

{

     string EmployeeName { get;set;}

}

 

后来经过思考, 根本没必要做成interface, 而换成abstract class, 能添加用户自定义的代码, 那样会更好:

[Table("Employees")]

public abstract partial class Employee

{

     public abstract string EmployeeName{get;set;}

}

后面会有一些篇幅去描述作为abstract class的好处.

 

这个编写类型的方式, 不是普通的POCO或者DomainModel. 它也不是Active Record,因为它不需要集成某个基类. 后来我发现它连 ORM 都不是. 因为不存在Mapping这个东西. 它负责的就是读写数据库而已. 也就是说, 它针对的是数据库方面, 而不是对象逻辑方面.

 

我自己给了它一个新的名字 : "Abstract Record" .

 

 

下面直接给出一个例子, 描述AbstractRecord框架下编程的第一印象:

 

拿Northwind数据来说 , 定义方式:

[CSPAR("Categories")]

public abstract partial class Category

{

     public abstract int CategoryID { get;}

 

     public abstract string CategoryName { get;set;}

     public abstract string Description { get;set;}

 

}

 

是的, 就这样, 就足够了. 无需编写配置文件, 也无需定义太多的Attribute, 也不需要定义字段, 然后傻傻地get和set.

 

既然是abstract, 不需要写实现代码了? 不需要. 这就是AbstractRecord的核心思想. 由框架去帮你实现.

 

那么,这样的对象,是无法new Category()的, 怎样实现CRUD操作?? 下面是说明的代码(ASP.NET):

 

//下面是CreateCategory.Aspx的内容

protected void ButtonCreate_Click(object sender, EventArgs args)

{

     Category cate = CSPAbstractRecord.NewRow<Category>();   //实例化一个抽象类!

     cate.CategoryName = textBoxName.Text;

     cate.Description = textBoxDescription.Text;

     CSPAbstractRecord.Save(cate);    //INSERT

     Response.Redirect("EditCategory.Aspx?CategoryID=" + cate.CategoryID); //自动得到自增的id.

}

 

上面的代码,描述了如何创建一个abstract class的实例. CSPAbstractRecord正是负责控制CRUD的类.

CSPAbstractRecord是AbstractRecord在我的那个系统上的实现. 开发人员只需要学习2个类, 就能够完成绝大部分的事情了!

 

//下面是EditCategory.Aspx的内容

protected Category _category;

protected void EnsureCategory()

{

     if (_category != null) return;

     int id = int.Parse(Request.QueryString["CategoryID"]);

     _category = CSPAbstractRecord.LoadRow<Category>(id);    //SELECT

     if (_category == null) throw (new Exception("没有该数据或者数据已经被删除!"));

}

protected override void OnLoad(EventArgs args)

{

     base.OnLoad(args);

     if (IsPostBack) return;

     EnsureCategory();

     //初始化界面

     textBoxName.Text = _category.CategoryName;

     textBoxDescription.Text = _category.Description;

}

protected void ButtonUpdate_Click(object sender, EventArgs args)

{

     EnsureCategory();

     _category.CategoryName = textBoxName.Text;

     _category.Description = textBoxDescription.Text;

     CSPAbstractRecord.Save(_category);   //UPDATE

}

protected void ButtonDelete_Click(object sender, EventArgs args)

{

     EnsureCategory();

     CSPAbstractRecord.Delete(_category); //DELETE

     Response.Redirect("CategoryList.Aspx");

}

 

上面的代码,使用了CSPAbstractRecord.LoadRow和CSPAbstractRecord.Delete去进行SELECT和DELETE的操作.

 

由上面的代码可以看出, AbstractRecord是非常容易使用, 而且, 代码量非常少. 短短几行, 就已经完成CRUD的操作界面. 我实在是想不出能比这种方式更节省代码的方式了.

 

使用AbstractRecord编程的重点:

1. 用极少的代码去定义类型化数据的抽象类.

2. 使用CSPAbstractRecord.NewRow/LoadRow/Save/Delete进行CRUD的操作.

 

 

# 封装数据

 

传统的POCO或贫血的DomainModel都有一个共同点, 就是所有的属性, 都是 get;set 的. 能得到那些对象, 就能随意更改属性, 甚至会破坏应用程序的逻辑. 如果是小型的应用, 业务逻辑简单, 那无所谓. 但是如果是一个复杂的系统, 那么保护数据不被滥用 , 是非常重要的事情. 这个,是很多ORM或相关框架无法做到的.

 

AbstractRecord允许程序员把数据成员定义为protected或者是protected internal. 这就是封装的根本实现方案.

 

就上面那个Category的例子, 里面有一个Picture字段. 通过数据的封装, 可以实现类型的转换:

public abstract partial class Category

{

     protected abstract byte[] InternalPicture { get;set;}

     public System.Drawing.Image Picture

     {

         get

         {

              if (InternalPicture == null)

                   return null;

              return System.Drawing.Image.FromStream(new System.IO.MemoryStream(InternalPicture));

         }

         set

         {

              if (value == null)

              {

                   InternalPicture = null;

                   return;

              }

              System.IO.MemoryStream ms = new System.IO.MemoryStream();

              value.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);

              InternalPicture = ms.ToArray();

         }

     }

}

上面的代码 , 一个是 abstract 的 InternalPicture. 这个属性被读写时, 反映的是数据库字段的读写. 而 public System.Drawing.Image Picture 并不是abstract的, 它通过InternalPicture,实现外部数据类型和数据库类型的转换. 这是程序员自定义的代码, 也就是为什么我使用abstract class, 而不是interface的原因.

 

(TIPS: 我们还可以定义这样的属性: public abstract string MyField { get;internal protected set;} 这种属性能被外部读取, 但只能在同assembly的范围内修改.)

 

基于这种形式的数据封装, 程序员甚至可以把所有的数据属性都定义为protected或protected internal. 然后提供一个公共的方法, 或者是使用另外的一个DomainService去修改那些数据. 业务逻辑就这样被保护起来了.

 

 

# 关系处理

 

关系处理一直是ORM的难题. 因为基于POCO的ORM, 它要帮你填充所有相关的数据, 不能实现LAZY LOAD. 而有些能实现的呢? 则需要很多定义. 或者要继承某个基类, 然后调用基类的方法去取得相关数据.

 

AbstractRecord使用一种基于数据库定义的关系去生成相关对象的实现. 在关系的处理上, 很简单:

[CSPAR("Order Details")]

public abstract partial class OrderDetail

{

     public abstract Order Order { get;}

     public abstract Product Product { get;}

}

 

就是这样,当程序员定义一个abstract的,返回其他相关类型的属性, 就已经完成了多对一关系的定义了. 无需写更多的代码. 无需定义Attribute或者是xml文件.

 

那么一对多呢?

[CSPAR("Orders")]

public abstract partial class Order

{

     public abstract OrderDetail[] Details { get;}

}

 

当返回值改为相关对象类型的数组是, 就是多对一的情况. 一对多,和多对一的定义, 是独立的.少了哪个都可以 .

 

剩下的就是 多对多的"难题"了. 也许你看到下面的代码, 都会惊叹原来多对多是那么地简单:

 

[CSPAR("Orders")]

public abstract partial class Order

{

     public abstract OrderDetail[] Details { get;}

     public Product[] Products

     {

         get

         {

              return Array.ConvertAll<OrderDetail, Product>(Details, delegate(OrderDetail d) { return d.Product; });

         }

     }

}

[CSPAR("Products")]

public abstract partial class Product

{

     public abstract OrderDetail[] Details { get;}

     public Order[] Orders

     {

         get

         {

              return Array.ConvertAll<OrderDetail, Order>(Details, delegate(OrderDetail d) { return d.Order; });

         }

     }

}

 

上面的代码, 使用一对多和多对一的属性, 再使用一次 Array.ConvertAll , 就实现了多对多. (其实数据库的多对多实现也是通过一对多组合而成的. )

 

# 数据库查询

 

很多人不喜欢ORM, 是因为大多的ORM框架, 都企图让数据库变得透明. 甚至是弄出HQL之类的语法去统一数据库查询方案. 其实这是软件发展的一个方向. 但不见得是程序员所希望的.

 

CSPAbstractRecord设计的时候, 就和数据库紧密相连, 并且能与传统的数据库查询相结合.

 

先看看CSPAbstractRecord是如何进行简单查询的:

 

查询存货不足的产品:

Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderBySql("ProductName", false);

实际上UnitsInStock<{0}就是一条SQL语句. 而CSPAbstractRecord会把{0}换成@p1_1这样的形式去查询SqlServer.

 

根据Category查询Product:

Product[] products = CSPAbstractRecord.All<Product>().Where("CategoryID IN (SELECT CategoryID FROM Categories WHERE CategoryName={0})", category).OrderBySql("ProductName", false);

 

或者是组合起来:

Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).Where("CategoryID IN (SELECT CategoryID FROM Categories WHERE CategoryName={0})", category).OrderBySql("ProductName", false);

 

实际上, 使用IN/EXISTS就能实现INNER JOIN的功能了. 甚至在子查询内使用聚合:

Order[] orders=CSPAbstractRecord.All<Order>().Where("OrderID IN (SELECT OrderID FROM [Order Details] GROUP BY OrderID HAVING SUM(UnitPrice*Quantity)>{0} )", 10000);

 

如果你曾经研究过INNER JOIN,IN,EXISTS的区别, 你就会明白数据库会对这些查询进行优化. 三种方案的性能是一致的.

 

数据分页: AbstractRecord还带一个高性能的分页方式:

Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderBySql("ProductName", false).GetRange(start, pagesize, out allcount);

输入开始位置(例如pageindex*pagesize),要取回的记录数(pagesize),就能返回符合要求的记录,并且返回所有记录数.

就这样的一个GetRange方法, 就能满足目前ASP.NET开发的分页需求了.

 

如果上面的查询依然不能满足你的需要, 还可以使用CSPAbstractRecord.BatchLoadRows方法:

object[] productid=......//用自定义的方法去取得ProductID列表