ORM的全称是Object Relational Mapping,即对象关系映射。它的实质就是将关系数据(库)中的业务数据用对象的形式表示出来,并通过面向对象(Object-Oriented)的方式将这些对象组织起来,实现系统业务逻辑的过程。在ORM过程中最重要的概念是映射(Mapping),通过这种映射可以使业务对象与数据库分离。从面向对象来说,数据库不应该和业务逻辑绑定到一起,ORM则起到这样的分离作用,使数据库层透明,开发人员真正的面向对象。图 1简单说明了ORM在多层系统架构中的这个作用。
图1:ORM在多层系统架构中的作用
目前大多数项目或产品都使用关系型数据库实现业务数据的存储,这样在开发过程中,常常有一些业务逻辑需要直接用写SQL语句实现,但这样开发的结果是:遍地布满SQL语句。这些高藕合的SQL语句给系统的改造和升级带来很多无法预计的障碍。为了提高项目的灵活性,特别是快速开发,ORM是一个不错的选择。举个简单的例子:在使用ORM的系统中,当数据库模型改变时,不再需要理会逻辑代码和SQL语句中涉及到该模型的所有改动,只需要将该模型映射的对象稍作改动,甚至不做改动就可以满足要求。
NFramework开源AOP框架ORM的实现原理与应用
1. 基本概念
对象关系映射(Object Relational Mapping,简称ORM)是一种为了解决面向对象与关系数据库存在的互不匹配现象的技术。简单的说,ORM是通过使用描述语言来描述数据库与对象之间的映射关系,并将程序中的对象自动持久化到关系数据库中,本质上就是将数据从一种形式转换到另外一种形式。
2. NHibernate
NHibernate目前可谓如日中天,许多人都在谈论它,并且也得到了广泛的应用,但在我的项目经历中,即使应用NHibernate这样的好工具也有痛苦的时候,大量的xml文件让我们眼花缭乱,简单易学的HQL语言却又常常不能满足我们的要求。特别是随着系统的复杂性越来越高,再加上人员的流动,最终xml堆积如山,若干事件以后不再有人记得那个xml文件是用来做什么的了。NHibernate本身对SQL的封装做的很好,但或许这也成为了我们的束缚,对于非常复杂的业务问题,我们的调试变得更困难,要在代码与数个xml文件之间不停的查阅以期快速定位问题的所在,这增加了我们的调试难度。
3. IBatis.NET
IBatis.NET考虑到了这种情况,因此它将SQL释放出来,让我们可以一目了然。但同样也存在xml数据量过多的情况。随着系统的不断升级,我们不得不考虑膨胀的xml文件对内存的占用问题。当然,IBatis.NET可以快速的溶入到现有的项目中,在不改变既有方案的前提下为我们提供一种新的思路来解决实际的问题,这是一个不错的优点。但纵观NHibernate也好、IBatis.NET也好,其应用代码中都有一些严重的重复问题,比如说OpenSession、CloseSession、GetFactory类似这样的代码,这对只关注业务的开发人员来说也是一个不小的工作量。
4. NFramework
NFramework在设计ORM时,充分考虑了目前流行了ORM框架,如NHibernate、IBatis.NET这两个流行的产品。当然NFramework也不可能没有缺点,但它为开发人员考虑的更多。首先从部署的角度来讲,NFramework没有采用xml文件作为ORM的映射描述,而是采用了扩展元数据的方式,这在很大程度上减轻了维护xml文件的负担,在部署时也只是一个简单的DLL文件。
NFramework中的实体(Entity)本身没有包括任何CRUD相关的方法,因此可以说是一个“轻量级的实体”,你不必担心由于大量的对实体对象的new操作导致系统占用资源过多而性能有所下降。NFramework中的实体只包括映射的元数据与对象属性,因此它是轻量级的,在我们的实际测试中,在用户大量的并发操作时,创建新实体对象占用的资源微乎其微。在做到轻量级的同时,NFramework并没有以降低映射的灵活性和效率性为前提,通过自定义的元数据,不但可以映射出数据表、视图、字段、字段类型、字段长度的映射关系,还可以轻松映射出多表之间的关联信息。下面我们来比较一下NFramework与以NHibernate为代表的ORM框架之间的不同。
比较项目
|
NFramework
|
NHibernate
|
速度
|
直接使用DLL做为载体,因此速度较快(当然,这里使用了反射,速度主要取决于反射的速度)。
|
视配置文件的大小,另外有文件的IO操作,另外大量的xml文件,对内存也提出了比较大的要求。
|
易用性
|
简单,和操作正常的类一样。
|
相对复杂,需要对了解每一个xml文件配置节属性,有一定的学习难度。
|
可维护性
|
简单,可以直接用代码跟踪调试。
|
相对复杂,每个XML文件都需仔细校验,无法在编译期间检查错误。
|
可部署性
|
方便部署,只需复制实体的DLL,如果实体有变化在编译期就可发现错误
|
相对麻烦,每个XML文件都要被部署到服务器,对于遗漏的文件将产生运行时错误,无法被早期发现,并且设置xml文件的路径都是比较繁琐的事情。
|
映射效率性
|
晚期绑定,因此对于数据表字段的类型、大小等更改不影响代码,映射效率高。
|
早期绑定,对于数据表的更改要将所有相关的XML配置文件也重新修改,对于配置文件较多的情况无疑会产生巨大的工作量
|
灵活性
|
由于实体要被编译成DLL的形式,因此灵活性相对较差
|
非常灵活,这归功于XML文件的灵活性
|
5. 开发人员参考
NFramework中的ORM使用对开发人员来说十分简单,可以使用NFramework提供的代码生成器,从实际的数据表或视图中快速的生成实体类代码,下面以用户实体为例来说明NFramework中ORM的使用。
5.1. 生成实体类
首先,使用代码生成器根据实际的数据表生成了如下内容的用户实体类代码:
/*
版权信息:版权所有(C) 2006,IntelligenceSoft Corporation
作 者:Moneystar
完成日期:2006-02-22
内容摘要:用户实体类
*/
using System;
using System.Data;
using System.Runtime.Serialization;
using IntelligenceSoft.Framework.Common;
using IntelligenceSoft.Framework.Common.Entity;
using IntelligenceSoft.Framework.Common.Enum;
namespace IntelligenceSoft.Framework.Extension.Organization
{
/// <summary>
/// 用户实体类
/// </summary>
[Serializable]
[ORMapping(TableName="t_system_user")] // 映射的实际数据表名称
public class UserEntity : BaseEntity // 必须继承实体基类
{
// 用户ID
private string m_user_id;
// 用户名称
private string m_user_name;
// 登录名称
private string m_login_name;
// 密码
private string m_login_password;
/// <summary>
/// 用户ID
/// (FieldName=user_id,IsPK=true,IsSelected=true,IsInserted=true,IsUpdated=true)
/// </summary>
[ORMapping(FieldName="user_id",IsPK=true,IsSelected=true,IsInserted=true,IsUpdated=true)]
public string UserID
{
get {return m_user_id;}
set {m_user_id = value;}
}
/// <summary>
/// 用户名称
/// (FieldName=user_name,IsPK=false,IsSelected=true,IsInserted=true,IsUpdated=true)
/// </summary>
[ORMapping(FieldName="user_name",IsPK=false,IsSelected=true,IsInserted=true,IsUpdated=true)]
public string UserName
{
get {return m_user_name;}
set {m_user_name = value;}
}
/// <summary>
/// 登录名称
/// (FieldName=login_name,IsPK=false,IsSelected=true,IsInserted=true,IsUpdated=true)
/// </summary>
[ORMapping(FieldName="login_name",IsPK=false,IsSelected=true,IsInserted=true,IsUpdated=true)]
public string LoginName
{
get {return m_login_name;}
set {m_login_name = value;}
}
/// <summary>
/// 密码
/// (FieldName=login_password,IsPK=false,IsSelected=true,IsInserted=true,IsUpdated=true)
/// </summary>
[ORMapping(FieldName="login_password",IsPK=false,IsSelected=true,IsInserted=true,IsUpdated=true)]
public string LoginPassword
{
get {return m_login_password;}
set {m_login_password = value;}
}
}
}
5.2. 实体类编码说明
ORMapping类用于设置对象与数据表的映射信息,通过ORMapping我们很容易的定义出种映射关系。另外,实体类需要从BaseEntity基类继承,BaseEntity类是NFramework框架的重要类之一。利用BaseEntity类中提供的方法可以方便的获取实体与数据表的影射关系,比如说映射的表名、某个属性所对应的字段名等等。不过,BaseEntity最重要以及最常用的功能则是实体与DataTable之间的转换。只需调用一个方法就可以将实体转换成DataTable,或者从DataTable转换成实体,而这一切不需要再编写额外的其它代码,从而大大减少了编码的工作量。
根据以上的实体类代码,我们就可以使用如下方法方便的实现实体与DataTable之间的转换。举例来说:
l 将实体转换成DataTable
UserEntity user = new UserEntity();
DataTable dt = user.ConvertTo(); // 一个方法就可以转换了
l 将DataTable转换成实体
DataTable dt = new DataTable();
UserEntity user = new UserEntity();
user.ConvertFrom(dt); // 一个方法就可以转换了,也可以使用user.Parse(dt)方法
这里要说明的是BaseEntity类提供了两个方法从DataTable转换成实体,即ConvertFrom和Parse。这两个方法都可以实现转换的功能,但它们之间有什么区别呢?
我们知道,利用SQL语句返回查询结果时通常是一个DataTable形式,而DataTable中的列名是根据SQL语句中的查询列名来定义的,因此这就产生了DataTable中的列如何与实体属性值匹配的问题。ConvertFrom方法通过获取实体属性映射的字段名,进而在DataTable中查找同名的列,一旦找到同名列,则会使用此列值作为实体属性值的数据源。Parse方法和ConvertFrom相同,只不过Parse方法使用实体属性名的字符串形式在DataTable中查找,然后再进行匹配。
另外要说明的就是ConvertTo方法,在使用这个方法将实体转换成DataTable时,会使用实体属性名称的字符串形式来创建DataTable列,这样在将DataTable绑定到DataGrid时就可以直接使用属性名称了,而不用再关心数据库中的表结构,即使用表的字段名改变了也只需要改动实体属性名的映射值就可以了,对现有代码无任何影响,这充分体现了ORM的思想,因此ConvertTo方法是非常重要方法,尽管它会带来一些性能上的开销,但在屏蔽数据库方面却有着非常重要的作用。
5.3. 实体类持久化操作
由于实体本身没有提供任何CRUD等相关方法,因此对实体的任何操作都是通过NFramework数据访问功能提供的方法。
还是用户实体为例,下面的代码为我们展示CRUD的编码方式:
/// <summary>
/// 获取方法
/// 同时这个方法也演示了不使用事务进行数据的访问,由NFramework负责打开连接
/// 及自动关闭连接
/// </summary>
/// <param name="userID">ID</param>
/// <returns>实体</returns>
public UserEntity GetEntity(string userID)
{
UserDAL dal = new UserDAL();
UserEntity user = new UserEntity();
try
{
// 没有使用事务进行数据访问,NFramework负责打开连接以及自动关闭连接
user.UserID = userID;
user.ConvertFrom(dal.GetEntity(user));
}
catch (Exception ex)
{
// NFramework错误处理机制
ErrorHandler.HandleError(ex);
}
return user;
}
/// <summary>
/// 新增方法
/// 这个方法同时演示了使用AOP自动事务的处理方式
/// </summary>
/// <param name="user">实体</param>
/// <param name="deptID">部门ID</param>
/// <returns>主键ID</returns>
[Transaction] // 使用AOP自动事务元数据标识
public string Insert(UserEntity user)
{
UserDAL dal = new UserDAL();
// 新增(自动处理不同数据库的序列问题)
dal.InsertEntity(user);
// 返回新增的用户ID
return user.UserID;
}
/// <summary>
/// 更新方法
/// 这个方法演示了不使用AOP时的事务处理方式
/// </summary>
/// <param name="user">实体</param>
public void Update(UserEntity user)
{
Dao dao = Dao.GetInstance();
dao.BeginTransaction();
try
{
dal.UpdateEntity(user);
// 递交事务
dao.Commit();
}
catch (Exception ex)
{
// 回滚事务
dao.Rollback();
ErrorHandler.HandleError(ex);
}
}
}
6. 设计人员参考
无。
7. 关于NFrmework设计的一些考虑
7.1. 为何实体本身没有任何CRUD方法
在基于数据访问的业务系统中,可能存在大量的实体对象,因此就要求在创建这些实体对象时对系统的资源占用最少,所以“实体”必须是轻量级的。有的朋友建议在BaseEntity基类中增加CRUD方法,这样可以给开发人员提供更多的灵活性,但这无疑会增加实体对象的负担(更多的成员变量),占用过多的系统资源,在大量用户并发的时候可能会产生不小的瓶颈。
另一方面,实体类继承了过多的方法,也会使我们的分层架构变得模糊。通常实体会存在于表示层、业务层、数据访问层,试想一下,如果在表示层new一个实体,根据实体本身提供的数据访问方法,那我们在表示层就完成了数据访问层应该做的事情,在业务层也同样可以这样做,那么这时可能就产生的混乱,数据访问逻辑与表示层逻辑、业务逻辑混杂在了一块。
NFramework在设计时考虑了上面的这些情况,因此实体被设计成一个单纯的,用来描述映射关系的对象,并且它是轻量级的,对实体的任何持久化操作必须借助于NFramework的为数据访问而提供的功能。NFramework控制了与数据库打交道的地方只能在数据访问层进行,对表示层、业务逻辑层彻底隔离了访问接口,这使我们的分层逻辑更加清晰,结构也变得一目了然。