# 前言:数据库交互之路
还记得以前在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列表
Product[] products = CSPAbstractRecord.BatchLoadRows<Product>(productids);
上面的,都是基于SQL的查询方案. Where方法还可以指定Predicate<T>来选择符合要求的记录:
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).Where(delegate(Product p) { return p.Details.Length>10 });
这种查询,先优先执行SQL,然后把所有符合SQL的数据返回,在本地进行CLR的筛选. 其实内部只是简单地使用Array.FindAll<Product>()方法去筛选而已.
因为要加载所有数据, 所以只适合小记录的表. 或者是肯定SQL语句能排除大部分记录的情况下使用.
查询时的排序:
上面的方式使用了 .OrderBySql("ProductName", false); 来进行SQL的排序.
CSPAbstractRecord还支持使用 Comparison<T> , Comparer<T> 进行排序:
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderBy(delegate(Product d1, Product d2) { return d1.Supplier.CompanyName.CompareTo(d2.Supplier.CompanyName); });
这种方案会加载所有相关的Supplier. 如果不想加载相关的对象, 还可以使用OrderByKey方法:
IDictionary dict = SqlScope.ExecuteDictionary("SELECT ProductID,CategoryName FROM Products INNER JOIN Categories ON Products.CategoryID=Categories.CategoryID");
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderByKey(delegate(object k1, object k2)
{
string n1 = dict[k1] as string;
string n2 = dict[k2] as string;
return string.Compare(n1, n2);
}, false);
上面的例子使用ProductID作为Key,找出相关的数据,然后用相关数据进行排序.
这个方法看上去和
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderBy(delegate(Product p1, Product p2)
{
string n1 = dict[p1.ProductID] as string;
string n2 = dict[p2.ProductID] as string;
return string.Compare(n1, n2);
}, false);
没两样.
但实际上OrderBy(delegate(Product p1, Product p2)的方案会加载所有符合Where的Product, 而OrderByKey则不需要. 在使用GetRange分页的情况下, OrderByKey会先对主键进行分页, 然后才加载符合分页的 Product, 明显性能会高很多.
# 性能问题
CSPAbstractRecord内部实现了2级的缓存. 这个会对多对一,一对多,多对多的关系起到非常重要的作用. 不过因为这个实现是透明的, 程序员完全不用去理解其内部实现机制. 这里就省略了.
而对数据库的读写操作, 是完全的强类型. 可以断定的是, AbstractRecord 比 DataSet 要快!
总结: