C#发现之旅第十一讲 使用反射和特性构造自己的ORM框架(下)

C#发现之旅第十一讲 使用反射和特性构造自己的ORM框架(下)

在InnerReadValue函数中遍历所有的属性绑定信息,调用IDataReader.GetValue函数从数据库中获得原始数据,然后调用FieldBindInfo.FromDataBase函数对这个原始数据进行一些处理,主要是进行格式化和数据类型转换,然后调用PropertyInfo.SetValue函数根据读取的数据设置对象实例的属性值。这样遍历了所有的绑定信息也就完整的填充了对象实例的属性值。

在ReadObjects函数中,遍历所有查询的数据,对每个记录创建一个对象实例,遍历数据库记录完毕后,我们就将所有创建的对象实例组成一个数组作为函数返回值,然后退出函数。

我们可以在这个ReadObjects函数上面派生出一系列的从数据库读取对象的函数。这个ReadObjects函数就实现了框架程序读取数据这个核心功能之一。

此外我们还定义了一个Contains函数用于判断一个应用程序对象实例对应的数据库记录是否存在。

新增数据

框架程序的InsertObjects函数就能将若干个对象插入的数据库表中。其主要代码为

/// <summary>
/// 将若干个对象插入到数据库中
/// </summary>
/// <param name="Objects">对象列表</param>
/// <param name="TableName">制定的数据表,若未指定则使用默认的数据表名</param>
/// <returns>插入的数据库记录的个数</returns>
public int InsertObjects( System.Collections.IEnumerable Objects , string TableName )
{
if( Objects == null )
{
throw new ArgumentNullException("Objects");
}
this.CheckBindInfo( Objects , false );
System.Collections.ArrayList list = new System.Collections.ArrayList();
foreach( object obj in Objects )
{
list.Add( obj );
}
if( list.Count == 0 ) {
return 0 ;
}
this.CheckConnetion();
// 上一次执行的SQL语句
string strLastSQL = null ;
int InsertCount = 0 ;
using( System.Data.IDbCommand cmd = myConnection.CreateCommand())
{
foreach( object obj in list )
{
TableBindInfo table = this.GetBindInfo( obj.GetType());
string TableName2 = TableName ;
if( TableName2 == null || TableName.Trim().Length == 0 )
{
TableName2 = table.TableName ;
}
System.Collections.ArrayList values = new System.Collections.ArrayList();
// 拼凑SQL语句
System.Text.StringBuilder myStr = new System.Text.StringBuilder();
System.Text.StringBuilder myFields = new System.Text.StringBuilder();
foreach( FieldBindInfo field in table.Fields )
{
if( field.Property.CanRead == false )
{
throw new Exception("属性 " + field.Property.Name + " 是不可写的");
}
object v = field.Property.GetValue( obj , null );
if( v == null || DBNull.Value.Equals( v ))
{
continue ;
}
values.Add( field.ToDataBase( v ));
if( myStr.Length > 0 )
{
myStr.Append(" , ");
myFields.Append( " , " );
}
myStr.Append(" ? " );
myFields.Append( FixFieldName( field.FieldName ));
}//foreach
myStr.Insert( 0 , "Insert Into " + FixTableName( TableName2 )
+ " ( " + myFields.ToString() + " ) Values ( " );
myStr.Append( " ) " );
string strSQL = myStr.ToString();
if( strSQL != strLastSQL )
{
// 重新设置SQL命令对象
strLastSQL = strSQL ;
cmd.Parameters.Clear();
cmd.CommandText = strSQL ;
for( int iCount = 0 ; iCount < values.Count ; iCount ++ )
{
cmd.Parameters.Add( cmd.CreateParameter());
}
}
// 填充SQL命令参数值
for( int iCount = 0 ; iCount < values.Count ; iCount ++ )
{
( ( System.Data.IDbDataParameter ) cmd.Parameters[ iCount ]).Value = values[ iCount ] ;
}
// 执行SQL命令向数据表新增记录
InsertCount += cmd.ExecuteNonQuery();
}//foreach
}//using
return InsertCount ;
}

在这个函数的参数是对象列表和要插入的数据库表名称。在函数中首先是遍历应用程序对象列表,对每一个对象调用GetBindInfo函数获得绑定信息,然后遍历所有对象类型属性绑定信息,拼凑出一个“Insert Into TableName ( 字段1,字段2 … ) Values ( 属性值1, 属性值2 … ) ”这样的SQL语句,这里使用了PropertyInfo.GetValue函数来从对象实例中获得指定的属性值,我们并没有将属性值直接放入到SQL语句中,而是采用了SQL参数的方式来存放属性值。

SQL语句拼凑完毕后我们就设置SQL命令对象,然后执行它,这样就向数据库插入一条数据库记录了。这里我们还使用了一个strLastSQL的变量来保存上次执行的SQL语句,这样可以减少设置SQL命令对象的次数,提高性能。

向数据库插入所有的对象后,我们就累计所有插入的数据库记录的个数为函数返回值,然后退出函数。

这个函数其过程也不复杂,我们在这个函数上面派生了一系列的向数据库插入记录的方法以丰富ORM框架的接口。

修改数据

框架定义了一个UpdateObjects函数用于根据应用程序对象来修改数据库记录,其主要代码为

/// <summary>
/// 更新多个对象
/// </summary>
/// <param name="Objects">对象列表</param>
/// <returns>更新修改的数据库记录个数</returns>
public int UpdateObjects( System.Collections.IEnumerable Objects )
{
if( Objects == null )
{
throw new ArgumentNullException("Objects");
}
this.CheckBindInfo( Objects , true );
this.CheckConnetion();
int RecordCount = 0 ;
using( System.Data.IDbCommand cmd = myConnection.CreateCommand())
{
foreach( object obj in Objects ) {
TableBindInfo table = this.GetBindInfo( obj.GetType());
// 拼凑生成SQL更新语句
System.Collections.ArrayList values = new System.Collections.ArrayList();
System.Text.StringBuilder myStr = new System.Text.StringBuilder();
foreach( FieldBindInfo field in table.Fields )
{
object v = field.Property.GetValue( obj , null );
if( myStr.Length > 0 )
{
myStr.Append(" , " + System.Environment.NewLine );
}
myStr.Append( FixFieldName( field.FieldName ) + " = ? " );
values.Add( field.ToDataBase( v ));
}
myStr.Insert( 0 , "Update " + FixTableName( table.TableName ) + " Set " );
string strSQL = BuildCondition( obj , values );
myStr.Append( " Where " + strSQL );
strSQL = myStr.ToString();
// 设置SQL命令对象,填充参数
cmd.Parameters.Clear();
cmd.CommandText = strSQL ;
foreach( object v in values )
{
System.Data.IDbDataParameter p = cmd.CreateParameter();
cmd.Parameters.Add( p );
p.Value = v ;
}
RecordCount += cmd.ExecuteNonQuery();
}//foreach
}//using
return RecordCount ;
}
/// <summary>
/// 根据对象数值创建查询条件子SQL语句
/// </summary>
/// <param name="obj">对象</param>
/// <param name="values">SQL参数值列表</param>
/// <returns>创建的SQL语句字符串</returns>
private string BuildCondition( object obj , System.Collections.ArrayList values )
{
TableBindInfo table = this.GetBindInfo( obj.GetType() );
// 拼凑查询条件SQL语句
System.Text.StringBuilder mySQL = new System.Text.StringBuilder();
foreach( FieldBindInfo field in table.Fields )
{
if( field.Attribute.Key )
{
object v = field.Property.GetValue( obj , null );
if( v == null || DBNull.Value.Equals( v ))
{
throw new Exception("关键字段属性 " + field.Property.Name + " 未指定值" ) ;
}
if( mySQL.Length > 0 )
{
mySQL.Append(" And " );
}
mySQL.Append( FixFieldName( field.FieldName ));
mySQL.Append( " = ? " );
values.Add( field.ToDataBase( v ));
}
}//foreach
if( mySQL.Length == 0 )
{
throw new Exception("类型 " + obj.GetType().FullName + " 未能生成查询条件");
}
return mySQL.ToString();

}

这个函数的参数是应用程序对象实例列表,在函数中遍历这个列表,对于每一个对象实例调用GetBindInfo函数获得绑定信息,然后遍历所有的对象属性的绑定信息,这样就可以拼凑出一个“Update Table Set 字段名1=属性值1 , 字段名2=属性值2 , 字段名3=属性值3…”这样的SQL语句。

我们还调用BindCondition函数来创建该SQL语句的Where子语句用于设置更新数据库记录的查询条件。在BindCondition函数中,遍历查找所有标记为关键字的属性绑定信息,然后拼凑出一个“字段名1=属性值1 and 字段名2=属性值2 …”这样的SQL语句,并调用PropertyInfo.GetValue函数来获得关键字段属性值。

这里和InsertObjects函数类似,我们并没有将对象实例的属性值嵌入在SQL语句中,而是使用SQL命令参数的方式来保存对象实例的属性值。

完整的用于更新数据库记录的SQL语句拼凑完毕后,我们就设置SQL命令对象,然后执行SQL语句,这样就能根据对象来修改数据库的记录,然后我们设置累计的修改数据库记录的个数作为返回值后退出函数。

我们还在UpdateObjects的基础上派生了一些其他函数用于丰富ORM框架的编程接口。

删除数据

ORM框架定义了一个DeleteObjects函数用于删除数据库记录,其主要代码为

/// <summary>
/// 删除若干条对象的数据
/// </summary>
/// <param name="Objects">对象列表</param>
/// <returns>删除的记录个数</returns>
public int DeleteObjects( System.Collections.IEnumerable Objects )
{
if( Objects == null )
{
throw new ArgumentNullException("Objects");
}
this.CheckBindInfo( Objects , true );
this.CheckConnetion();
int RecordCount = 0 ;
using( System.Data.IDbCommand cmd = myConnection.CreateCommand())
{
foreach( object obj in Objects )
{
TableBindInfo table = this.GetBindInfo( obj.GetType() );
// 拼凑SQL语句
System.Collections.ArrayList values = new System.Collections.ArrayList();
string strSQL = BuildCondition( obj , values );
strSQL = "Delete From " + FixTableName( table.TableName ) + " Where " + strSQL ;
// 设置SQL命令对象
cmd.Parameters.Clear();
cmd.CommandText = strSQL ;
foreach( object v in values )
{
System.Data.IDbDataParameter p = cmd.CreateParameter();
p.Value = v ;
cmd.Parameters.Add( p );
}
// 执行SQL,删除记录
RecordCount += cmd.ExecuteNonQuery();
}
}
return RecordCount ;
}

该函数的参数是应用系统对象实例的列表。在这个函数中遍历所有的对象实例,调用GetBindInfo函数获得映射信息,然后拼凑出一个”Delete From 映射的数据表名 Where 查询条件”的SQL语句,这里使用BindCondition函数来创建Where子语句,然后使用拼凑的SQL语句设置SQL命令对象并执行,这样就从数据库中删除了应用程序对象对应的数据库记录了。

我们在DeleteObjects的基础上派生了一些函数用于丰富框架的编程接口。

使用ORM框架

ORM框架开发完毕后,我们就来简单的测试这个框架,首先我们在一个Access2000的数据库中建立一个名为Employees的数据表,该数据表存放的是公司员工的信息,其字段有

C#发现之旅第十一讲 使用反射和特性构造自己的ORM框架(下)

对于这张表我们编写了对应的应用程序对象类型,其主要代码为

[System.Serializable()]
[BindTable("Employees")]
public class DB_Employees
{
/// <summary>
/// 人员全名
/// </summary>
public string FullName
{
get
{
return this.LastName + this.FirstName ;
}
}
#region 定义数据库字段变量及属性 //////////////////////////////////////////
///<summary>
/// 字段值 EmployeeID
///</summary>
private System.Int32 m_EmployeeID = 0 ;
///<summary>
/// 字段值 EmployeeID
///</summary>
[BindField("EmployeeID" , Key = true )]
public System.Int32 EmployeeID
{
get
{
return m_EmployeeID ;
}
set
{
m_EmployeeID = value;
}
}
///<summary>
/// 字段值 LastName
///</summary>
private System.String m_LastName = null ;
///<summary>
/// 字段值 LastName
///</summary>
[BindField("LastName")]
public System.String LastName
{
get
{
return m_LastName ;
}
set
{
m_LastName = value;
}
}
///<summary>
/// 字段值 FirstName
///</summary>
private System.String m_FirstName = null ;
///<summary>
/// 字段值 FirstName
///</summary>
[BindField("FirstName")]
public System.String FirstName
{
get
{
return m_FirstName ;
}
set
{
m_FirstName = value;
}
}
其他字段……………..
#endregion
}// 数据库操作类 DB_Employees 定义结束

这个类型的代码很简单,就是定义一个个和数据库字段对应的公开属性而已,因此很容易使用各种代码生成器来生成这样的代码。这个DB_Employees类型中使用了BindTable和BindField特性来标记对象及其属性绑定了数据库表和字段上,这里的EmployeeID属性标记为关键字段,因此框架程序修改和删除数据库记录是会依据EmployeeID来生成查询条件。

我们开发了一个简单的WinForm程序来测试我们建立的ORM框架。其用户界面为

C#发现之旅第十一讲 使用反射和特性构造自己的ORM框架(下)

查询数据

用户界面上的“刷新”按钮就是读取数据库,然后刷新员工名称列表,其主要代码为

private void cmdRefresh_Click(object sender, System.EventArgs e)
{
using( MyORMFramework myWork = this.CreateFramework())
{
RefreshList( myWork );
}
}
private void RefreshList( MyORMFramework myWork )
{
object[] objs = myWork.ReadAllObjects(typeof( DB_Employees ));
System.Collections.ArrayList list = new ArrayList();
list.AddRange( objs );
this.lstName.DataSource = list ;
this.lstName.DisplayMember = "FullName";
}
/// <summary>
/// 连接数据库,创建ORM框架对象
/// </summary>
/// <returns>ORM框架对象</returns>
private MyORMFramework CreateFramework()
{
System.Data.OleDb.OleDbConnection conn = new System.Data.OleDb.OleDbConnection();
conn.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + System.IO.Path.Combine( System.Windows.Forms.Application.StartupPath , "demomdb.mdb" );
conn.Open();
return new MyORMFramework( conn );
}

该按钮的点击事件处理中,首先调用CreateFramework函数连接数据库,创建一个ORM框架对象的实例,然后调用RefreshList函数来刷新列表。

RefreshList函数中,首先调用ORM框架的ReadAllObjects函数获得数据库中所有的类型为DB_Employees的对象,ReadAllObjects函数内部调用了ReadObjects函数。程序获得一个对象数组后放置到一个ArrayList中,然后设置列表框控件的数据源和显示字段名称,这样就刷新了员工名称列表框的内容了。

用户点击员工名称列表框的某个姓名后就会在用户界面的右边显示该员工的详细信息,其处理过程的代码为

private void lstName_SelectedIndexChanged(object sender, System.EventArgs e)
{
DB_Employees obj = lstName.SelectedItem as DB_Employees ;
if( obj != null )
{
this.txtID.Text = obj.EmployeeID.ToString() ;
this.txtName.Text = obj.FullName ;
this.txtTitleOfCourtesy.Text = obj.TitleOfCourtesy ;
this.txtAddress.Text = obj.Address ;
this.txtNotes.Text = obj.Notes ;
}
else
{
this.txtID.Text = "";
this.txtName.Text = "";
this.txtTitleOfCourtesy.Text = "";
this.txtNotes.Text = "" ;
this.txtAddress.Text = "";
}
}

这个过程也很简单,用户刷新员工列表框后,该列表框的列表内容都是DB_Employees类型,我们就获得当前的员工信息对象,然后一个个设置右边的文本框的内容为各个属性值就可以了。

新增数据

我们点击“新增”按钮就会向数据库新增一条记录,其主要代码为

private void cmdInsert_Click(object sender, System.EventArgs e)
{
try
{
using( dlgRecord dlg = new dlgRecord())
{
dlg.Employe = new DB_Employees();
if( dlg.ShowDialog( this ) == DialogResult.OK )
{
using( MyORMFramework myWork = this.CreateFramework())
{
if( myWork.InsertObject( dlg.Employe ) > 0 )
{
RefreshList( myWork );
}
}
}
}
}
catch( Exception ext )
{
MessageBox.Show( ext.ToString());
}
}

该函数中首先调用员工信息编辑对话框来输入新增员工的信息,该对话框的用户界面为

C#发现之旅第十一讲 使用反射和特性构造自己的ORM框架(下)

用户确 认输入新增员工的信息后,程序调用CreateFramework的函数创建一个ORM框架对象的实例, 然后调用它的InsertObject函数来向对象插入一个数据库记录,InsertObject函数内部会调 用上面介绍的InsertObjects函数。如果插入的数据库记录个数大于0则调用RefreshList函数 来刷新左边的员工列表。

修改数据

用户点击“修改”按钮后就能 修改当前员工数据并修改数据库记录。其主要代码为

private void  cmdEdit_Click(object sender, System.EventArgs e)
{
      DB_Employees obj = this.lstName.SelectedItem as DB_Employees ;
      if( obj == null )
          return ;
      using(  dlgRecord dlg = new dlgRecord())
      {
          dlg.txtID.ReadOnly = true ;
          dlg.Employe = obj ;
          if( dlg.ShowDialog( this ) == DialogResult.OK )
          {
               using( MyORMFramework myWork  = this.CreateFramework())
               {
                    if( myWork.UpdateObject( obj ) > 0 )
                    {
                        RefreshList( myWork );
                    }
              }
          }
      }
}   

在这个按钮点击事件处理中,首先调用员工信息编辑对话框来编辑当前员 工的信息,当用户修改并确认后,程序创建一个ORM框架对象实例,然后调用UpdateObject函 数来修改数据库记录,UpdateObject函数内部调用上面介绍的UpdateObjects函数。若成功的 修改数据库记录则调用RefreshList函数来更新列表。

删除数据

用户点击 “删除”按钮来删除数据库记录,其主要代码为

private void  cmdDelete_Click(object sender, System.EventArgs e)
{
      DB_Employees obj = this.lstName.SelectedItem as DB_Employees ;
      if( obj != null )
      {
          if(  MessageBox.Show(
               this ,
               "是否删除 " + obj.FullName + " 的纪录?",
               "系统提示" ,
               System.Windows.Forms.MessageBoxButtons.YesNo ) == DialogResult.Yes )
          {
               using( MyORMFramework myWork  = this.CreateFramework())
          {
                    myWork.DeleteObject( obj );
                    RefreshList( myWork );
               }
          }
      }
} 

在这个按钮点击事件处理中,程序 首先让用户确认删除操作,然后创建一个ORM框架对象,然后调用它的DeleteObject函数来删 除对象对应的数据库记录,然后调用RefreshList函数来刷新列表。

从这个例子可以 看出,使用ORM框架,对于最常见的查询,新增,修改和删除数据库记录的操作将变得比较简 单,而且和数据库表对应的对象类型的代码很简单,可以很容易的采用代码生成器来生成它 们的代码,以后若数据库结构发生改变,只需更新这些数据表对应的实体类的代码就可以了 。这些特性都能比较大的降低开发和维护成本,提高开发速度。

部署ORM框架

由于这个ORM框架是轻量级的,不依赖任何其他非标准组件,因此部署非常方便,我们可以将 修改这个演示程序工程样式为DLL样式,编译生成一个DLL即可投入使用,也可以将代码文件 MyORMFramework.cs或者其内容复制粘贴到你的C#工程中即可。

小结

在本课程 中,我们使用了.NET框架提供的反射和特性来构造了一个简单的ORM框架。反射就是.NET程序 的基因分析技术,功能强大,使用也不复杂。特性本身不影响程序的运行,但能对各种软件 编程单元进行标记,可以指引某些程序模块的运行。反射和特性都是C#的一种比较高级的编 程技巧,好好利用可以构造出非常灵活的程序框架。