ORM背景
在数据库界,主流的数据库都是关系型数据库,其采用的关系型数据结构模型,无论从数学上还是实践中都相当的成熟,得到非常广泛的应用。在关系型数据结构理 论中,所有的数据都组织成一个个相互独立的二维表格,一个数据表有若干行若干列。因此关系型数据库适合存储大量的结构简单的数据,不适合存储复杂的数据结 构。
在编程界,面向对象的编程思想及其派生思想占据主流。面向对象的编程思想具有封装,继承,重载等手段来方便的实现比较复杂的数据结构,这适应了现代信息系统包含大量复杂数据结构的特点。因此面向对象的编程思想得到广泛应用。
关系型数据模型和面向对象的编程思想之间存在比较大的差别,数据在两者之间的交换是不大通畅的,就像南京,江北和主城区之间是长江来阻断交通。因此开发人员迫切需要破解这种数据交通的阻断。
以前程序员需要编程,从数据库中读取一个个字段值并赋值到编程对象的一个个字段或属性上,这就像在长江上架一个独木桥,开发效率低下,维护困难。后来出现一种叫ORM的框架性的程序,它能根据某些配置信息将数据库中的字段和编程对象的字段或属性之间建立映射关系,从而能方便的从数据库读取字段值并赋值到对象属性中。这是一种半自动的机制,能比较大的提高开发效率,简化维护,这就像在长江上架设了一座高速公路大桥。
从本质上说,关系型数据库和面向对象的编程思想之间的隔阂非常大,就像长江是天涧,即使建五六座大桥也不够用。彻底的解决方法就是抛弃关系型数据库而使用 面向对象的数据库,不过这个过程就像南京江北整体搬迁到江南一样,工程浩大,时间漫长。在等待面向对象数据库的统治前,我们仍然得忍受关系型数据库和面向 对象编程思想之间的数据交通不畅的痛苦,并使用ORM框架来很有限的降低这种痛苦。从这个角度上说,我们痛恨关系型数据库,就像搞运载火箭的人痛恨地球引力一样。
反射和特性
反射是.NET框架提供的一种高级编程接口。学过VB的都知道VB中有一个CallByName函数,它能调用对象的指定名称的成员方法,比如有个窗体对象,我们可以调用“frm.Close()”来关闭窗体,也可以调用“CallByName( frm , “Close”)”来实现同样的功能。而反射就是CallByName的.NET版本,而且功能更加强大。使用反射,我们可以列出任何对象类型的所有的字段,属性,方法和事件的名称,包括公开的或私有的。我们还可以更深入的获得字段的数据类型,成员方法的参数的个数,类型及其返回值;事件使用的委托类型等等。可以说反射技术就是.NET框架提供的只读的程序基因分析技术。
.NET框架对反射这种程序基因分析技术提供了天然的支持。在.NET框架中,任何对象类型都是从object类型上面派生的,object类型有一个GetType函数,该函数返回一个System.Type类型的对象,该对象就是反射操作的入口点,这样任何.NET对象类型都能用反射技术进行分析。
特性也是.NET框架提供的一种高级编程手段。它是附加在类型,字段,属性,函数等编程单元上面的额外信息,就相当于Access数据库中的表和字段的说明文本。它不会影响所附着的编程单元的正常执行。但它是客观存在的,可以通过反射来获得其信息,一般的我们可以调用System.Attribute类型的GetCustomAttribute函数来获得指定编程单元附加的指定类型的特性对象。
从编程角度看,特性也是一种对象类型,它们都是从System.Attribute上面派生的。.NET类库中已经定义了大量的特性类型,我们也可以定义自己的特性。
使用特性也很简单,也就是在编程单元前面使用方括号包含特性的定义,比如对于WinForm程序其入口函数具有以下类似的定义。在这里,函数定义前头的“[Ssystem.STAThread]”就表示这个函数附加了一个类型名为“System.STAThreadAttribute”的特性,这里存在一个C#的语法,在附加特性时可以将类型名称后面的“Attribute”后缀去掉,当然也可以写全名。一个编程单元可以附加多个特性。
///<summary> |
使用特性,我们可以在对象属性上附加数据库字段映射信息,使用反射,我们可以动态的读取和设置对象的属性值。这样特性和反射可以成为我们实行ORM的技术基础。
关于反射和特性的详细信息可以参考MSND中的相关文档。
ORM框架设计
我们首先来设计一个ORM框架。说到框架大家一定联想到.NET框架,J2EE框架。这些框架都是大公司劳苦数年才能完成,结构复杂功能强大。其实我们都可以根据各自需要自己开发一些通用的支持性质的软件,并美其名曰框架。现在我们就来设计一个轻量级的ORM框架,并应用我们今天要学习的反射和特性的.NET编程技术。
既然是轻量级的,我们就不考虑所有的情况,只考虑经常遇到的简单情况,复杂情况不予考虑。很多时候我们的编程对象和数据库表之间是存在简单的影射关系的, 比如一个对象类型对应一个数据表,对象的一个属性对应数据表中的一个字段。此时我们可以定义两种特性,一个数据数据库表绑定特性,名为BindTableAttribute,用于将一个对象类型绑定到一个指定表名称的数据表上;还有一个数据库字段绑定特性,名为BindFieldAttribute,用于将一个对象属性绑定到一个指定名称的字段上面。
下图就是一个映射关系的例子,数据库中有个名为Employees的数据表,而开发者定义了DB_Employees类型。通过使用BindTableAttribute特性,将DB_Employess类型映射到数据表Employees,而是用BindFieldAttribute特性将DB_Employees的EmployeeID属性映射到数据库字段EmployeeID上面。类似的DB_Employees中的很多属性都映射到数据表Employees中的某个字段上,当然不是所有的对象类型的属性映射到数据库字段。通过在程序代码中,我们可以使用硬编码的方式将对象类型及其属性映射到数据库中的表和字段上面。
这种将映射信息保存在代码中的方式有利有弊,好处是程序代码比较集中,修改代码方便,坏处就是当数据库结构或者映射关系发生改变时,需要修改代码,这导致重新编译重新部署。一些ORM框架使用XML配 置文件来保存对象和数据库的映射关系,不过这会导致代码,数据库和映射配置文件的三者同步更新的操作,工作量大,会加大开发成本,当然好处是当数据库结构 或者映射关系发生改变时,只需要修改数据库和配置文件,程序代码不需要更新,从这方面看有利于系统的维护。不过在很多实践中,数据库或映射关系改变时,很 容易导致程序代码必须作相应的修改,此时会导致代码,数据库和映射配置文件的同步更新工作。因此映射配置信息采用何种保存模式需要开发者自己权衡,不过在 这里由于是要演示使用反射和特性的,因此映射配置信息是保存在代码中的。当然我们可以建立一个ORM框架,既支持使用特性存储映射关系,也可以使用映射配置文件,不过比较复杂,本框架程序是演示程序,不会实现该功能。
开发者在编制存储数据的类型后,使用BindTableAttribute和FieldBindAttribute特性建立了映射关系后,ORM框架程序就能根据这些类型来操作数据库了,目前的设计将提供以下几种功能。
查询数据库,返回对象
使用本功能,框架可以根据指定的SQL查询语句和对象类型查询数据库,并根据查询结果生成若干个对象,并设置刚刚创建的对象的属性值为字段值。在这个功能中,需要首先指定SQL查询语句和对象类型。
在这个功能中,框架程序首先获得对象类型的所有公开属性,获得其附加的BindFieldAttribute特性,获得这些属性绑定的数据库字段名。然后执行SQL查询,遍历所有查询的纪录,对每一个记录都创建一个数据对象,并根据字段和属性的映射关系将数据库字段值保存到对象的属性值中。如此就实现了查询数据库获得对象的功能。
这个功能中需要用户指定SQL查询语言,也可以根据对象类型绑定的数据表名称来自己拼凑SQL语句。
将对象插入到数据库
在本功能中,框架程序使用反射获得对象类型附加的BindTableAttribute特性,获得该对象映射的数据表名;然后遍历所有的公开实例属性,若属性附加了BindFieldAttribute特性,则获得该属性映射的字段名。然后收集所有的属性值和它们映射的字段名,使用字符串拼凑生成一个Insert的SQL语句。然后调用数据库连接对象执行这个SQL语句,以实现向数据库新增记录的功能。
根据对象修改数据库记录
在本功能中,框架程序使用指定的对象来修改数据库中的记录。此时对象类型中至少有一个属性附加了关键字段映射特性。框架程序使用反射获得对象类型附加的BindTableAttribute 特性,获得该对象映射的数据表名,然后遍历属性,获得对象属性和数据库字段之间的映射关系。然后收集属性值,使用字符串拼凑生成一个“Update 数据表名 Set 字段1=属性1的值,字段2=属性2的值”的SQL语句。然后还遍历属性,找到所有附加了关键字段特性的属性以及绑定的字段名,拼凑出“Where 关键字段1=属性1的值 and 关键字段2=属性2的值”,这样就能拼凑出一个完整的更新数据库用的Update的SQL语句,然后调用数据库连接对象执行这个SQL语句,就能实现更新数据库记录的功能。
根据对象删除数据库记录
在本功能中,框架程序获得对象类型绑定的数据表名,并遍历所有的附加了绑定关键字段的特性,然后拼凑出“Delete From 数据表名Where 关键字段1=属性1的值 and 关键字段2=属性2的值”的SQL语句,然后调用数据库连接对象来执行这个SQL更新语句,这样就实现了删除数据库记录的功能。
框架程序代码说明
根据程序设计,我已经初步的把框架程序开发出来,现在对其源代码进行说明。
数据表绑定信息 BindTableAttribute类型
框架程序中首先定义了BindTableAttribute类型,该类型就保存了对象类型映射的数据库表的名称。其源代码为
///<summary> |
BindTableAttribute演示了如何实现自己的特性。所有的特性都是从System.Attribute类型上面派生的,在定义类型时还使用System.AttributeUsage特性来表明这个自定义特性的使用范围,这里使用了Class样式,表示BindTableAttribute特性只能用在其它的Class类型前面,若放置在Interface或Struct类型前面,或者放在对象成员的前面则会出现编译错误。这里还是用语句 AllowMultiple=false语句来表明对于一个类型,该特性只能用一次,若一个Class类型前面出现多个BindTableAttriubte,则会出现编译错误。若设置AllowMultiple=true,则该特性可以多次定义,也就是一个Class类型前面可以出现多个相同类型的特性。不过这里我们假设一个对象只能映射到一个数据表上,没有多重映射,因此就指明对同一个类型该特性不能多次使用。
特性也是一个Class类型,可以有多个构造函数,就像C#的new语句一样,我们向类型附加特性时可以使用不同的初始化参数来指明使用特性的那个构造函数。我们附加特性时还可以使用“属性名=属性值”的方法来直接指明特性的属性值。这有点类似VB中调用函数时使用“参数名=参数值”的语法。
该特性中定义了一个Name属性,该属性就是被修饰的对象所映射的数据库表的名称。框架程序就读取BindTableAttribute特性的Name值来作为数据对象映射的数据表名,若Name值为空则认为对象类型的名称就是映射的数据表的名称。
若对象没有附加BindTableAttribute特性,则该对象没有映射到任何数据表上,因此不能让框架程序使用它来操作数据库。
数据字段绑定信息 BindFieldAttribute类型
框架程序中定义了BindFieldAttribute类型,该类型就保存了对象的属性映射的数据库字段的名称,转换格式和关键字段样式,其源代码为
[System.AttributeUsage( System.AttributeTargets.Property , AllowMultiple = false ) ] public class BindFieldAttribute : System.Attribute { ///<summary> /// 初始化对象 ///</summary> public BindFieldAttribute( ) { } ///<summary> /// 初始化对象 ///</summary> ///<param name="name">字段名</param> public BindFieldAttribute( string name ) { strName = name ; } private string strName = null; ///<summary> /// 数据字段名 ///</summary> public string Name { get { return strName ; } } private bool bolKey = false; ///<summary> /// 该字段为关键字段,可用作查询条件 ///</summary> public bool Key { get { return bolKey ; } set { bolKey = value; } } private string strReadFormat = null; ///<summary> /// 数据读取格式化字符串 ///</summary> public string ReadFormat { get { return strReadFormat ; } set { strReadFormat = value ; } } private string strWriteFormat = null; ///<summary> /// 数据存储格式化字符串 ///</summary> public string WriteFormat { get { return strWriteFormat ; } set { strWriteFormat = value; } } }//public class BindFieldAttribute : System.Attribute |
在BindFieldAttribute中,首先我们使用AttributeUsage特性来描述了这个特性的应用范围,这里使用了System.AttributeTargets.Property来表明该特性只能用于对象类型的属性而不能用于其它任何地方。
这里定义了Name属性,就是其所依附的数据对象的属性映射的数据库字段的名称,若Name值为空则认为属性名就是映射的数据库字段名。若数据对象的属性没有附加BindFieldAttribute特性,则该属下没有映射到任何数据库字段上,框架程序会忽略这个成员属性的存在。
这里还定义了Key属性,用于表明所映射的字段是不是关键字段。框架程序在修改和删除数据库记录时需要获得查询条件,而对象类型中所有的附加了BindFieldAttribute特性且Key值为true的属性就可构造出查询条件,若对象类型中没有任何一个Key值为true的成员属性,则框架程序不能根据其来修改和删除数据库记录。
ReadFormat属性用于指明从数据库读取的原始数据设置到对象属性值时的解析格式。比如数据库中保存了类似“20080603”的格式为“yyyyMMdd”的日期数据,而对象属性的数据类型是DateTime类型。此时我们可以设置ReadFormat值为“yyyyMMdd”则框架程序从数据库获得原始数据后试图用“yyyyMMdd”的格式解析原始数据并获得一个DateTime值,然后设置到对象属性值。
WriteFormat属性类似ReadFormat属性,用于指明将数据对象的属性值按照指定的格式化生成一个字符串并保存到数据库中。
主框架模块 MyORMFramework类型
类型 MyORMFramework是本ORM框架的主要程序模块,它根据类型BindTableAttribute和BindFieldAttribute提供的信息将应用程序对象和数据库的表和字段进行绑定,然后向数据库查询,新增,修改和删除数据库记录。应用程序使用ORM框架也基本上就是创建一个MyORMFramework的实例,然后调用它的成员。
获得对象-数据库绑定信息
框架要实现ORM框架功能,第一步就是得获得应用程序对象和数据库的映射关系,在MyORMFramework类型中定义了GetBindInfo函数来获得这种关系。该函数的参数是应用程序的对象类型,返回值是TableBindInfo类型,该类型就是对象-数据库映射信息。由于BindTableAttribute和BindFieldAttribute类型适合对应用对象类型做标记,但不适合快速查询信息,因此这里额外定义了TableBindInfo和FieldBindInfo类型保存映射关系,这两个类型的代码为
///<summary> /// 数据表绑定信息对象 ///</summary> private class TableBindInfo { ///<summary> /// 数据库表名 ///</summary> public string TableName = null; ///<summary> /// 对象类型 ///</summary> public Type ObjectType = null; ///<summary> /// 绑定信息对象 ///</summary> public BindTableAttribute Attribute = null; ///<summary> /// 绑定的字段信息对象 ///</summary> public FieldBindInfo[] Fields = null; ///<summary> /// 绑定的字段列表,格式为"字段1,字段2,字段3" ///</summary> public string FieldNameList = null; } ///<summary> /// 数据字段绑定信息对象 ///</summary> private class FieldBindInfo { ///<summary> /// 绑定的字段名 ///</summary> public string FieldName = null; ///<summary> /// 绑定的字段序号 ///</summary> public int FieldIndex = - 1; ///<summary> /// 对象属性信息 ///</summary> public System.Reflection.PropertyInfo Property = null; ///<summary> /// 数据类型 ///</summary> public Type ValueType = null; ///<summary> /// 默认值 ///</summary> public object DefaultValue = null; ///<summary> /// 绑定信息对象 ///</summary> public BindFieldAttribute Attribute = null; ///<summary> /// 将对象数据转换为数据库中的数据 ///</summary> ///<param name="v">对象数据</param> ///<returns>数据库数据</returns> public object ToDataBase( object v ) { if( v == null || DBNull.Value.Equals( v )) return DBNull.Value ; string Format = Attribute.WriteFormat ; if( Format != null && Format.Trim().Length > 0 ) { if( v is System.IFormattable ) { v = ( ( System.IFormattable ) v ).ToString( Format , null ); } } return v ; } ///<summary> /// 将从数据库中获得的数据转换为对象数据 ///</summary> ///<param name="v">从数据库获得的原始数据</param> ///<returns>转化后的对象数据</returns> public object FromDataBase( object v ) { // 若数据为空则返回默认值 if( v == null || DBNull.Value.Equals( v )) return DefaultValue ; // 进行格式化解析 string Format = Attribute.ReadFormat ; if( Format != null && Format.Trim().Length > 0 ) { string Value = Convert.ToString( v ); if( ValueType.Equals( typeof( DateTime ))) { if( Format == null ) return DateTime.Parse( Value ); else return DateTime.ParseExact( Value , Format , null ); } else if( ValueType.Equals( typeof(byte ))) { return byte.Parse( Value ); } else if( ValueType.Equals( typeof( short ))) { return short.Parse( Value ); } else if( ValueType.Equals( typeof( int ))) { return int.Parse( Value ); } else if( ValueType.Equals( typeof( float ))) { return float.Parse( Value ); } else if( ValueType.Equals( typeof( double ))) { return double.Parse( Value ); } return Convert.ChangeType( Value , ValueType ); } if( v.GetType().Equals( ValueType ) || v.GetType().IsSubclassOf( ValueType )) { // 若数据类型匹配则直接返回数值 return v ; } else { // 若读取的值和对象数据的类型不匹配则进行数据类型转换 System.ComponentModel.TypeConverter converter = System.ComponentModel.TypeDescriptor.GetConverter( ValueType ); if( converter != null && converter.CanConvertFrom( v.GetType()) ) { return converter.ConvertFrom( v ) ; } return Convert.ChangeType( v , ValueType ); } }//public object FromDataBase( object v ) } |
类型TableBindInfo用于保存对象类型和数据库表的映射关系,类型FildBindInfo用于保存对象属性和数据库字段的映射关系。TableBindInfo定义了一个Fields字段,是FieldBindInfo类型的数组,用于保存所有的对象属性和数据库字段的映射关系。这样我们就用TableBindInfo和FieldBindInfo组成了一个两层的树状列表,方便我们查询对象和数据库的绑定关系。
函数GetBindInfo的代码为
///<summary> /// 在内部缓存的映射信息列表,此处为了提高速度。 ///</summary> private static System.Collections.Hashtable myBufferedInfos = new System.Collections.Hashtable(); ///<summary> /// 获得指定类型的数据表映射信息对象 ///</summary> ///<param name="ObjectType">对象类型</param> ///<returns>获得的映射信息对象</returns> ///<remarks> /// 本函数内部使用了 myBufferedInfos 来缓存信息,提高性能。 ///</remarks> private TableBindInfo GetBindInfo( Type ObjectType ) { if( ObjectType == null ) { throw new ArgumentNullException("OjbectType"); } // 查找已缓存的映射信息对象 TableBindInfo info = ( TableBindInfo ) myBufferedInfos[ ObjectType ] ; if( info != null ) { return info ; } // 若未找到则创建新的映射信息对象 BindTableAttribute ta = ( BindTableAttribute ) System.Attribute.GetCustomAttribute( ObjectType , typeof( BindTableAttribute )); if( ta == null ) { return null; } TableBindInfo NewInfo = new TableBindInfo(); NewInfo.ObjectType = ObjectType ; NewInfo.Attribute = ta ; NewInfo.TableName = ta.Name ; if( NewInfo.TableName == null || NewInfo.TableName.Trim().Length == 0 ) { // 若在特性中没有指明绑定的表名则使用默认的对象类型名称 NewInfo.TableName = ObjectType.Name ; } System.Text.StringBuilder myFieldList = new System.Text.StringBuilder(); System.Collections.ArrayList fields = new System.Collections.ArrayList(); // 遍历所有的公开的实例属性来获得字段绑定信息 foreach( System.Reflection.PropertyInfo p in ObjectType.GetProperties( System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public )) { BindFieldAttribute fa = ( BindFieldAttribute ) Attribute.GetCustomAttribute( p , typeof( BindFieldAttribute )); if( fa != null ) { FieldBindInfo NewFieldInfo = new FieldBindInfo(); NewFieldInfo.Attribute = fa ; NewFieldInfo.FieldName = fa.Name ; if( NewFieldInfo.FieldName == null || NewFieldInfo.FieldName.Trim().Length == 0 ) { // 若在特性中没有指明绑定的字段名则使用默认的属性名称 NewFieldInfo.FieldName = p.Name ; } if( myFieldList.Length > 0 ) { myFieldList.Append(","); } myFieldList.Append( FixFieldName( NewFieldInfo.FieldName ) ) ; NewFieldInfo.Property = p ; NewFieldInfo.ValueType = p.PropertyType ; NewFieldInfo.DefaultValue = GetDefaultValue( p ); fields.Add( NewFieldInfo ); } } NewInfo.Fields = ( FieldBindInfo[]) fields.ToArray( typeof( FieldBindInfo )); NewInfo.FieldNameList = myFieldList.ToString(); // 缓存绑定信息对象 myBufferedInfos[ ObjectType ] = NewInfo ; return NewInfo ; } |
为了提高程序性能,本类型内部定义了一个myBufferedInfos的哈希列表,该列表保存了各种应用程序类型和数据库的映射关系,在GetBindInfo函数中,程序首先检查myBufferedInfos列表获得缓存的数据。若缓存中没有所需的数据,则使用Attribute.GetCustomAttribute函数从指定的对象类型中获得附加的BindTableAttribute特性,若没有获得特性则表示对象类型没有附加特性,因此无法获得任何绑定信息。若获得了一个BindTableAttribute对象,则创建一个TableBindInfo对象,填充TableBindInfo中的信息。
接着程序调用对象类型的GetProperties函数获得该对象类型中定义的所有属性,这里使用了一种System.Reflection.BindingFlags类型的参数,该参数就指明获得的属性的样式,其中Instance样式指明只查找实例属性,而不是静态属性;Public样式指明只查找公开的属性(public),不查找私有的属性。GetProperties函数返回的是一个System.Reflection.PropertyInfo类型的数组,然后我们遍历所有找到的属性,对每一个属性调用Attribute.GetCustomAttribute函数获得该属性附件的类型为BindFieldAttribute特性,若找到则表示该对象属性映射到数据库字段中,于是创建一个FieldBindInfo类型的对象,填充该对象。完成对所有的属性的遍历后,我们就设置TableBindInfo的Fields字段,然后将获得的TableBindInfo对象保存在myBufferedInfos中,最后函数返回创建的TableBindInfo对象,这个就是指定应用对象类型的数据库映射信息。
在ORM框架操作数据库时会频繁的调用GetBindInfo函数获得对象类型的数据库映射关系,因此需要使用myBufferedInfos哈希列表缓存这种映射关系,这会比较大的提高框架程序的运行性能。
查询数据
框架定义了一个ReadObjects的函数用于查询数据库获得数据,并根据对象-数据库映射关系创建若干个应用程序对象。其主要代码为
///<summary> /// 使用指定的SQL查询语句查询数据库并读取多条数据库记录对象 ///</summary> ///<param name="strSQL">SQL查询语句</param> ///<param name="ObjectType">要读取的对象类型</param> ///<param name="MaxObjectCount">最多读取的对象个数</param> ///<returns>读取的对象组成的数组</returns> public object[] ReadObjects( string strSQL , Type ObjectType , int MaxObjectCount ) { // 检查参数 if( strSQL == null ) { throw new ArgumentNullException("strSQL"); } if( ObjectType == null ) { throw new ArgumentNullException("ObjectType"); } // 检查数据库映射信息 this.CheckBindInfo( ObjectType , false ); // 检查数据库连接 this.CheckConnetion(); // 创建SQL命令对象 using( System.Data.IDbCommand cmd = myConnection.CreateCommand()) { // 执行SQL查询,获得一个数据读取器 cmd.CommandText = strSQL ; System.Data.IDataReader reader = cmd.ExecuteReader( MaxObjectCount == 1 ? System.Data.CommandBehavior.SingleRow : System.Data.CommandBehavior.SingleResult ); System.Collections.ArrayList list = new System.Collections.ArrayList(); TableBindInfo table = this.GetBindInfo( ObjectType ); lock( table ) { // 设置字段序号,提高性能 foreach( FieldBindInfo field in table.Fields ) { field.FieldIndex = - 1 ; } for( int iCount = 0 ; iCount < reader.FieldCount ; iCount ++ ) { string name = reader.GetName( iCount ); foreach( FieldBindInfo field in table.Fields ) { if( EqualsFieldName( name , field.FieldName )) { field.FieldIndex = iCount ; } } } while( reader.Read()) { // 根据对象类型创建对象实例 object obj = System.Activator.CreateInstance( ObjectType ); // 读取对象属性值 if( InnerReadValues( obj , table , reader ) > 0 ) { list.Add( obj ); } if( MaxObjectCount > 0 || list.Count == MaxObjectCount ) { break; } }//while }//lock reader.Close(); // 返回读取的对象数组 return list.ToArray(); }//using } |
该函数的参数为SQL查询语句,要创建的对象类型和最多能创建的对象的个数。在该函数中,首先根据数据库连接对象创建一个命令对象,然后执行SQL查询语句,获得一个System.Data.IDataReader对象,然后调用GetBindInfo函数获得对象类型和数据库的映射信息。首先对数据读取器的各个字段名称和对象类型中绑定的各个属性的名称进行比较,设置各个对象类型的属性对应的字段序号,以加快数据读取速度。
然后遍历数据读取器,对每行记录使用System.Activator.CreateInstance创建指定类型的对象实例,然后调用InnerReadValue函数从数据读取器中读取数据并填充对象的属性值。
在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 ; } } |
这个函数的参数是应用程序对象实例列表,在函数中遍历这个列表,对于每一个对象实例调用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的数据表,该数据表存放的是公司员工的信息,其字段有
对于这张表我们编写了对应的应用程序对象类型,其主要代码为
[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框架。其用户界面为
查询数据
用户界面上的“刷新”按钮就是读取数据库,然后刷新员工名称列表,其主要代码为
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()); } } |
该函数中首先调用员工信息编辑对话框来输入新增员工的信息,该对话框的用户界面为
用户确认输入新增员工的信息后,程序调用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#的一种比较高级的编程技巧,好好利用可以构造出非常灵活的程序框架。