1 引言
目前大多数项目或产品都使用关系型数据库实现业务数据的存储,这样在开发过程中,常常有一些业务逻辑需要直接用写SQL语句实现,但这样开发的结果是:遍地布满SQL语句。这些藕合较高的SQL语句给系统的改造和升级带来很多无法预计的障碍。为了提高项目的灵活性,特别是快速开发,ORM是一个不错的选择。举个简单的例子:在使用ORM的系统中,当数据库模型改变时,不再需要理会逻辑代码和SQL语句中涉及到该模型的所有改动,只需要将该模型映射的对象稍作改动,甚至不做改动就可以满足要求。
ORM的全称是Object Relational Mapping,即对象关系映射。它的实质就是将关系数据(库)中的业务数据用对象的形式表示出来,并通过面向对象(Object-Oriented)的方式将这些对象组织起来,实现系统业务逻辑的过程。在ORM过程中最重要的概念是映射(Mapping),通过这种映射可以使业务对象与数据库分离。从面向对象来说,数据库不应该和业务逻辑绑定到一起,ORM则起到这样的分离作用,使数据库层透明,开发人员真正的面向对象。下图简单说明了ORM在多层系统架构中的这个作用。
图1 ORM在多层系统架构中的作用
当然ORM本身并不是全能的,当遇到特别复杂的数据处理及海量数据处理和弥补拙劣的设计不足时还得归结到SQL或存储过程来实现。所以快速开发、面向对象和性能优化灵活必须兼顾才行,这些该工具可以很好的做到,下文分别一一介绍。
2 内容
2.1 ORM的工具实现:C#.Net实体代码生成工具(EntitysCodeGenerate)
优秀的ORM工具不仅可以帮助我们很好的理解对象及对象的关系,而且工具本身会帮助我们维护这些关系,并且帮助我们记住字段属性业务含义及提供辅助的应用等。基于这个理念,我于多年的项目实践和业余时间设计开发了一个基于.NET的ORM工具——C#.Net实体代码生成工具(EntitysCodeGenerate),该工具运行于dotnetframework2.0框架下,希望多多交流并指正。
C#.Net实体代码生成工具(EntitysCodeGenerate) 为ORM提供对象持久、简单对象查询、事务处理等功能。数据持久包括一些对象的Insert、Update、Save、Delete、Select等功能,简单对象查询则提供一些基于对象的简单对象查询GetEntity等。该工具是基于VS.NET 2005的开发的应用程序,职责是从数据库中提取生成实体类代码并帮助开发人员快速映射关系数据库中的业务模型的映射实体类,暂时只提供从Oracle、SqlServer、Access数据库生成C#代码的支持,可以生成实体及实体集的相关文件,并自动提取数据库表和字段的注释说明和对应的数据类型等。
另外所生成的代码文件只需修改数据库连接,即可用于目前市场上ado.net支持的各种类型的数据库,如Oracle、Access、SqlServer、MySQL、Excel等。
在ORM实现的前期工作中,为了实现屏蔽各种数据库之间的操作差异,我们需要定义数据操作公有接口,封装基本的数据库增、删、改、查等操作。下面以工具当前3.1版以数据库Oracle的为例介绍。
3.1版实体层代码有个基类,基类里其实也就是你项目的数据连接及类型的配置,该工具的实际应用时配置其实也就在这里,默认为生成代码时所填写的数据库连接信息,并可手工扩展修改:
public class BaseEntity
{
public static string GetConnectionString()
{
return "User ID=scott;Password=tiger;Data Source=85";//数据库连接设置可修改从别处读取
}
public static DatabaseType GetDatabaseType()
{
return DatabaseType.Oracle; //数据库连接类型设置也可修改从别处读取
}
……
}
这里可设置该命名空间当前实体下的数据库连接类型及数据库连接字符串,当然也可以修改成从其它配置文件中读取。以前的版本是放在全局设置DbConnectString这个类里面,这样的缺陷是当一个项目有多个数据库时将不好处理,而现在通过使用基类及命名空间则很容易解决。
此外,我们还需要定义下层各种数据库操作的公共组件:
namespace System.Database
{
public class DbCore : IDisposable
{
……
public int ExecuteNonQuery(DBCommandWrapper command){…}
public DataSet ExecuteDataSet(DBCommandWrapper command){…}
……
}
}
再定义数据库操作类及数据库的基类和数据库连接的工厂类,实现各种不同类型的数据。
public abstract class DBCommandWrapper {……}
internal abstract class Database{……}
internal sealed class DatabaseFactory{……}
……
然后实现各种数据库的操作类,以Oracle为例
internal class OracleDatabase : Database{…}
public class OracleCommandWrapper : DBCommandWrapper{…}
最后在生成的实体类及基类文件中生成相应的数据库Insert,Update,Delete,GetEntity,Save等操作和类型映射代码如下所示:
public int Insert()
{
ORMap<BaseEntity> ormap = new ORMap<BaseEntity>(this);
DEPT entity = new DEPT();
return ormap.Insert(entity);
}
……
public static System.Data.DbType GetDBTypeByFullName(string strTypeFullName)
{
switch (strTypeFullName)
{
case "System.Byte":
return System.Data.DbType.Byte;
case "System.Boolean":
return System.Data.DbType.Boolean;
case "System.Char":
return System.Data.DbType.Byte;
case "System.DateTime":
return System.Data.DbType.DateTime;
case "System.Decimal":
return System.Data.DbType.Decimal;
……
default:
return System.Data.DbType.Object;
}
}
在生成代码的同时,工具自动添加实体类的属性标示及字段注释说明等如下所示:
[Serializable(),Description("Primary:DEPTNO")]
public class DEPT:BaseEntity
{
……
/// <summary>
/// 主键
/// </summary>
[DataObjectField(true)]
public int DEPTNO
{
set{ _deptno=value;}
get{return _deptno;}
}
/// <summary>
///
/// </summary>
[DataObjectField(false)]
public string DNAME
{
set{ _dname=value;}
get{return _dname;}
}
……
}
实体中的summary中的注释自动将数据中表及字段的注释说明提取到这里,方便程序员在代码编写及维护中交流使用,理解其所对应的业务含义。示例是以Oracle数据库自带的示例库为例介绍的,数据库自带的实例库scott/tiger表注释为空,所以这里实体字段提取出注释也是为空。
实体定义完成后,我们需要根据实体类中绑定的属性构造出运行期需要的SQL语句,收集实体类定义中的数据结构描述,再定义一个类来说明实体在运行期所引用到的所有关于数据持久的信息,包括关键字字段,一般字段等。同时需要一个字段的元数据字段在数据库中的名称,大小,是否可为空,列类型等信息。这些条件具备后,再定义解析类,负责转换数据的程序类型到数据库字段类型,并且构造出Insert,Update,Delete,Select,Save等操作所需要的SQL语句,再去调用数据操作公有接口DbCore,即可实现。同时数据操作公有接口System.Database.DbCore结合实体类可将简单或复杂及事务的操作更为方便的实现,下文着重介绍在实际中的使用。数据库操作默认以实体对应表的主键为准,当然也可以指定条件,引入增加和更新合并为一个保存操作,由实体对象本身自己判断是增加还是更新操作。
2.2 在开发中的实际应用
C#.Net实体代码生成工具(EntitysCodeGenerate)安装后,在生成对应的实体代码后都会有个“相关配置”文件夹,里面有“配置说明”文档和相关文件,实际使用时只须按“配置说明”文档操作即可。使用该工具开发的项目,可以做到很好的切换到异构数据库,且只需变动数据库连接接口即可(即只须修改GetDatabaseType()/GetConnectionString())。同时提供了ORMaping.dll(1.0版本引用),System.Database.dll(2.0/3.0/3.1版本引用)安装目录下有相应的chm格式帮助文档,是对这两个dll单独使用的说明,支持自定义的数据库访问,并可结合生成的实体操作数据库,提供良好的事务处理等。其中ORMaping.dll支持Oracle;System.Database.dll默认支持Oracle,并可用于各种类型的数据库访问,如SqlServer、MySQL等。工具所生成的代码和提供的文件,都是可选配的,可单独使用也可配置使用,比如:你可以只生成实体代码,而不生成对应的数据库操作,这样就不需要相应的配置文件,只须将代码文件拷贝或生成到对应目录下即可。但是选择带数据库操作的实体文件的功能更强大些,否则只能发挥实体及实体集的基本通用功能。实体代码的生成界面比较简单如下所示:
这里,只须选择数据库类型输入正确的数据库连接字符串、代码文件的输出目录和代码命名空间即可。实体数据类型以”数据类型映射文件”为准,工具提供了系统默认的类型映射,若有自定义的类型,可在”数据类型映射文件”里修改,可配置到数据类型的精确刻度。若修改过程中想恢复默认的配置,只须点击”生成”按钮即可。准备工作做好后,单击”生成代码”按钮,生成成功之后按提示生成的配置说明文档,将实体及实体集代码文件和基类文件拷贝到指定目录并添加ORMap.dll、System.Database.dll的引用即可。操作简单这里就不在此赘述,下面开始介绍实体对象的数据库操作的工作。
先介绍单个实体类的数据库操作是如何工作的:
2.2.1 单个实体对象数据库操作
这里还是以Oracle附带库DEPT为例来做说明,首先做对象的声明
DEPT entity = new DEPT();
下面以该对象来做阐述。
1、获取一个实体对象信息GetEntity
实体的操作默认以主键为准,对单个实体信息的获取可简单如下实现:
entity.DEPTNO = 50;
entity = entity.GetEntity();
返回主键字段DEPTNO=50的对象,若数据库中没有对应的记录则返回null。DEPT表的主键字段是DEPTNO,同时对联合主键也是支持的,下文同此;GetEntity同时提供其它指定条件的重载,也可以使用GetEntityByEntityCondition,当指定条件有多个记录符合时只返回首条记录;返回多条记录可使用GetDataTable方法。
2、新增一个实体对象
新增一个对象代码可如下所示:
entity.DEPTNO = 51;
entity.DNAME = “DNAME1”;
entity.LOC = “LOC1”;
entity.Insert();
同时Insert提供多种相应的重载和InsertAll方法;Insert和InsertAll区别是:Insert在插入记录时会比较对象初始的字段值,将与初始值不同的值插入,其余的以表字段的默认方式处理;InsertAll则是插入全部字段,即使是对象的初始值没有改变也会插入。
3、更新一个实体对象
更新一个对象代码可如下所示:
entity.DEPTNO = 51;
entity.DNAME = “DNAME2”;
entity.LOC = “LOC2”;
entity.Update();
Update也提供多种相应的重载和UpdateAll方法;Update和UpdateAll区别是:Update在更新记录时会比较对象初始的字段值,将与初始值不同的值进行更新,其余的表字段不更新;UpdateAll则是更新全部字段,即使是对象的初始值没有改变也会将对象的初始值更新到表里。可根据情况选择使用。
4、保存一个实体对象
保存是该工具新增的功能,即将新增和更新合并到一个保存功能里,ORM会自动解析是新增还是更新,保存一个对象的代码可如下所示:
entity.DEPTNO = 51;
entity.DNAME = “DNAME3”;
entity.LOC = “LOC3”;
entity.Save();
保存操作也是默认以主键为准,对多个联合主键也是支持的,不带参数的保存默认是按主键判断有对应的记录就更新,没有就插入新记录。同时Save提供多种重载和SaveAll、SaveByEntityCondition方法,Save和SaveAll区别同新增和更新。
5、删除一个对象
删除可如下代码所示:
entity.DEPTNO = 51;
entity.Delete();
删除操作也是默认以主键为准,对多个联合主键也支持,同时也提供多种指定条件的重载方法。最后附加说明实体对象的增删改保存操作都会返回一个int值,该值返回表中记录受影响的行数。
从这些代码可以明显的看到,这里常用的数据增、删、改、查操作只需很简单几句即可实现,少写了很多代码,是不是很好?
6、取得实体映射表数值字段的最大ID+1
代码可如下:
int intID = entity.GetInt32MaxID();
这里获取实体对象对应表字段默认第一个主键的最大值ID+1,类型为整型,同时提供GetInt?MaxID多种重载,即有整型和长整型及指定字段等。
本节介绍的都是单表无事务的操作,下节开始介绍多表及事务处理的操作。
2.2.2 多个实体对象及事务处理并结合System.Database 的工作
这里简略介绍实体对象结合System.Database.DbCore及事务处理是如何工作的,先看以下代码(可参见安装示例代码System.Database.Demo):
Entitys.Common.LC_WORKTYPE entity = new Entitys.Common.LC_WORKTYPE();
entity.ID = 1;
entity.TYPENAME = "TYPENAME";
string strConnection = "Password=newaqfx;User ID= newaqfx;Data Source=85";
DbCore dbCore = new DbCore(DatabaseType.Oracle, strConnection);
dbCore.Open();
dbCore.BeginTransaction();
dbCore.Save(new Entitys.Common.LC_WORKTYPE(), entity);
entity.DESCRIPTION = "类型描述";
dbCore.Save(new Entitys.Common.LC_WORKTYPE(), entity);
entity.TYPENAME = "作业类型";
dbCore.Save(new Entitys.Common.LC_WORKTYPE(), entity);
DataSet ds = dbCore.ExecuteDataSet("select * from lc_worktype");
entity.ID = 1;
DataTable dt = dbCore.GetDataTableByEntityKey(entity);
int intRecord = dbCore.Delete(entity);
dt = dbCore.GetDataTableByEntityKey(entity);
dbCore.CommitTransaction();
dbCore.Close();
这里使用另外一个实体LC_WORKTYPE(映射安全风险系统的"作业类型"表),BeginTransaction()为开始事务的标志,CommitTransaction()为提交当前事务,还一个是RollbackTransaction()表示回滚当前事务,放弃当前事务下所有数据的更改。这样在事务开始和提交或回滚之间可以进行多个实体的操作,并将结果最终一起提交或回滚撤销。这里Save有两个参数第一个是实体对象的初始类用于比较实体的初始值,第二个是要保存的对象,该方法依据主键自动判断表数据是更新还是插入;同时与Save类似的方法有SaveAll保存全部字段,同时也有Insert、InsertAll、Update、UpdaAll、Delete、IsExitByEntityKey、Exists、Get?MaxId等等方法,均可相互结合使用,方法都有详尽的说明及示例代码。该方法执行过程中可单步跟踪,查看该事务下每步命令执行后对应的数据集信息。
下面再看以Oracle自带的scott库为例一段代码Delete、Insert、Update并结合事务使用的代码:
DbCore dbCore = null;
try
{
EMP entity1 = new EMP();
DataSet ds = new DataSet();
entity1.EMPNO = 7369; //设置主键EMPNO为
entity1 = entity1.GetEntity(); //取得主键EMPNO为实体对象信息
//return "User ID=scott;Password=tiger;Data Source=85";
dbCore = new DbCore(Entitys.Common.BaseEntity.GetConnectionString());
dbCore.Open();
dbCore.BeginTransaction();
//选择当前事务下的所有雇员EMP的信息
ds = dbCore.SelectAll().From(entity1).ExecuteDataSet();
dbCore.Delete(entity1);//删除主键EMPNO为7369的记录
ds = dbCore.SelectAll().From(entity1).ExecuteDataSet();//查看当前事务下记录,当前删除记录将不在此显示
dbCore.Insert(new EMP(), entity1);//插入刚才删除主键EMPNO为7369的记录
ds = dbCore.SelectAll().From(entity1).ExecuteDataSet();//查看当前事务下记录,可见刚刚插入的新记录
entity1.SAL = entity1.SAL + 100;//薪水加100
dbCore.Update(new EMP(), entity1);//更新
ds = dbCore.SelectAll().From(entity1).ExecuteDataSet();//查看当前事务下记录,对应薪水SAL已更新
entity1.SAL = entity1.SAL - 100;// 薪水减100
dbCore.Update(new EMP(), entity1);//更新
ds = dbCore.SelectAll().From(entity1).ExecuteDataSet();//查看当前事务下记录,对应薪水SAL已更新
dbCore.CommitTransaction();
dbCore.Close();
}
catch (Exception ex)
{
if (dbCore != null)
{
dbCore.Close();
}
}
上面的Insert、Update方法都可以Save方法来取代,Save方法会自动判断是Update还是Insert,这里只是用来展示之用。
诚然ORM本身并不是全能,当遇到特别复杂的数据处理及海量数据处理、性能优化和弥补拙劣的设计时,还得归结到SQL或存储过程来实现才是好的选择。实际项目中仍然会有海量复杂的数据处理及复杂查询和不同类型数据库、SQL语句优化和存储过程等等,使用System.Database.DbCore都可很好的解决,该组件支持目前市场上ado.net支持的各种类型的数据库,可执行自定义编写的SQL语句和存储过程等,这样可针对复杂功能特殊处理及性能优化等。
System.Database.DbCore可直接用于Oracle、SqlServer、MySql和支持OleDb、Odbc类型的数据库,代码分别如下:
DbCore dbCore = new DbCore(DatabaseType.Oracle, “OracleConnectionString”);
DbCore dbCore = new DbCore(DatabaseType.SqlServer, “SqlServerConnectionString”);
DbCore dbCore = new DbCore(DatabaseType.MySql, “MySqlConnectionString”);
DbCore dbCore = new DbCore(DatabaseType.OleDb, “OleDbConnectionString”);
DbCore dbCore = new DbCore(DatabaseType.Odbc, “OdbcConnectionString”);
再来看一段适合Oracle和SqlServer访问的通用代码:
DbCore dbCore = PublicClass.GetNewDbCore();
string strParaToken = dbCore.GetCurrentParameterToken;
string strSql = "INSERT INTO dept (deptno, dname, loc) VALUES (" + strParaToken + "deptno, " + strParaToken + "dname, " + strParaToken + "loc)";
dbCore.Open(); //打开数据库连接
dbCore.BeginTransaction(); //开始事务
DBCommandWrapper cmd = dbCore.GetSqlStringCommandWrapper(strSql);
//cmd.AddParameter(..);//为命令增加一个参数实例
cmd.AddInParameter(strParaToken + "deptno", DbType.Int32, 99);
cmd.AddInParameter(strParaToken + "dname", DbType.String, "部门名称");
cmd.AddInParameter(strParaToken + "loc", DbType.String, "locTest");
int intMaxDeptId = dbCore.GetInt32MaxId("dept", "deptno");//当前表的deptno最大值
dbCore.ExecuteNonQuery(cmd);
intMaxDeptId = dbCore.GetInt32MaxId("dept", "deptno");//插入数据deptno=99之后当前表的deptno最大值
strSql = "DELETE dept WHERE deptno = " + strParaToken + "deptno";
DBCommandWrapper cmd1 = dbCore.GetSqlStringCommandWrapper(strSql);
cmd1.AddInParameter(strParaToken + "deptno", DbType.Int32, 99);
dbCore.ExecuteNonQuery(cmd1);
intMaxDeptId = dbCore.GetInt32MaxId("dept", "deptno");//删除数据deptno=99之后当前表的deptno最大值
dbCore.RollbackTransaction();//回滚撤销事务。等于该方法什么都没做,只是演示作用
intMaxDeptId = dbCore.GetInt32MaxId("dept", "deptno");
dbCore.Close();//关闭数据库连接
其中第一句的PublicClass.GetNewDbCore()方法体代码可以是new DbCore(DatabaseType.Oracle, “OracleConnectionString”)也可以是new DbCore(DatabaseType.SqlServer, “SqlServerConnectionString”),当new的是Oracle时即表示操作访问的是Oracle数据库,当new的是SqlServer即表示操作访问的是SqlServer数据库。dbCore.GetCurrentParameterToken即是获取对应数据库连接参数的前导符(如:Oracle是“:”,SQL Server是“@”等),这里也可以结合使用dbCore.StandardSqlWithParameters方法对当前带参数的SQL语句进行标准化通用处理,即所写SQL可以用于如MySql/Access数据库等。这里的数据库操作同样也是可以同实体对象一块协同工作。dbCore.GetSqlStringCommandWrapper(…)创建一个SQL语句的命令,dbCore. GetStoredProcCommandWrapper(…)创建一个执行存储过程的命令,可根据项目自身实际需要选择使用。
对专有数据库命令也可以转化为指定数据库命令来使用,这样可针对该数据库特性使用更多的方法,如Oracle、SqlServer的命令转化可像下列代码来转化:
OracleCommandWrapper cmd = dbCore.GetSqlStringCommandWrapper(strSql) as OracleCommandWrapper;
SqlCommandWrapper cmd = dbCore.GetSqlStringCommandWrapper(strSql) as SqlCommandWrapper;
……
这里顺便说明一下当程序执行出现错误时可使用dbCore.Close()来关闭当前打开的数据库连接,如下代码所示:
catch (Exception ex)
{
if (dbCore != null)
{
dbCore.Close();
}
MessageBox.Show(ex.Message);
}
最后再说一个System.Database.DbCoreConnectLimit.AllDBMaxConnectionCount,可以设置数据库可打开的最大连接数目,默认不受限制。
下载:
http://download.enet.com.cn/html/030212009031901.html
http://www.skycn.com/soft/53715.html