• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

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

原作者: [db:作者] 来自: [db:来源] 收藏 邀请
C#发现之旅第十一讲 使用反射和特性构造自己的ORM框架

袁永福 2008-6-24


系列课程说明

    为了让大家更深入的了解和使用C#,我们将开始这一系列的主题为“C#发现之旅”的技术讲座。考虑到各位大多是进行WEB数据库开发的,而所谓发现就是发现我们所不熟悉的领域,因此本系列讲座内容将是C#在WEB数据库开发以外的应用。目前规划的主要内容是图形开发和XML开发,并计划编排了多个课程。在未来的C#发现之旅中,我们按照由浅入深,循序渐进的步骤,一起探索和发现C#的其他未知的领域,更深入的理解和掌握使用C#进行软件开发,拓宽我们的视野,增强我们的软件开发综合能力。

本系列课程配套的演示代码下载地址为 https://files.cnblogs.com/xdesigner/cs_discovery.zip

本系列课程已发布的文章有
C#发现之旅第十讲 文档对象模型

本课程说明

    在本课程中将使用.NET框架提供的反射和特性的功能来构造自己的ORM框架。从而学习C#.NET的一些高级编程技术。本课程的演示程序下载地址为 https://files.cnblogs.com/xdesigner/MyORM.zip

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>
///
应用程序的主入口点。
/// </summary>
[System.STAThread]
static void Main()
{
     Application.Run(new frmTestORM());
}

    使用特性,我们可以在对象属性上附加数据库字段映射信息,使用反射,我们可以动态的读取和设置对象的属性值。这样特性和反射可以成为我们实行ORM的技术基础。

    关于反射和特性的详细信息可以参考MSND中的相关文档。

ORM框架设计

    我们首先来设计一个ORM框架。说到框架大家一定联想到.NET框架,J2EE框架。这些框架都是大公司劳苦数年才能完成,结构复杂功能强大。其实我们都可以根据各自需要自己开发一些通用的支持性质的软件,并美其名曰框架。现在我们就来设计一个轻量级的ORM框架,并应用我们今天要学习的反射和特性的.NET编程技术。

    既然是轻量级的,我们就不考虑所有的情况,只考虑经常遇到的简单情况,复杂情况不予考虑。很多时候我们的编程对象和数据库表之间是存在简单的影射关系的,比如一个对象类型对应一个数据表,对象的一个属性对应数据表中的一个字段。此时我们可以定义两种特性,一个数据数据库表绑定特性,名为BindTableAttribute,用于将一个对象类型绑定到一个指定表名称的数据表上;还有一个数据库字段绑定特性,名为BindFieldAttribute,用于将一个对象属性绑定到一个指定名称的字段上面。

    下图就是一个映射关系的例子,数据库中有个名为Employees的数据表,而开发者定义了DB_Employees类型。通过使用BindTableAttribute特性,将DB_Employess类型映射到数据表Employees,而是用BindFieldAttribute特性将DB_EmployeesEmployeeID属性映射到数据库字段EmployeeID上面。类似的DB_Employees中的很多属性都映射到数据表Employees中的某个字段上,当然不是所有的对象类型的属性映射到数据库字段。通过在程序代码中,我们可以使用硬编码的方式将对象类型及其属性映射到数据库中的表和字段上面。

    这种将映射信息保存在代码中的方式有利有弊,好处是程序代码比较集中,修改代码方便,坏处就是当数据库结构或者映射关系发生改变时,需要修改代码,这导致重新编译重新部署。一些ORM框架使用XML配置文件来保存对象和数据库的映射关系,不过这会导致代码,数据库和映射配置文件的三者同步更新的操作,工作量大,会加大开发成本,当然好处是当数据库结构或者映射关系发生改变时,只需要修改数据库和配置文件,程序代码不需要更新,从这方面看有利于系统的维护。不过在很多实践中,数据库或映射关系改变时,很容易导致程序代码必须作相应的修改,此时会导致代码,数据库和映射配置文件的同步更新工作。因此映射配置信息采用何种保存模式需要开发者自己权衡,不过在这里由于是要演示使用反射和特性的,因此映射配置信息是保存在代码中的。当然我们可以建立一个ORM框架,既支持使用特性存储映射关系,也可以使用映射配置文件,不过比较复杂,本框架程序是演示程序,不会实现该功能。

    开发者在编制存储数据的类型后,使用BindTableAttributeFieldBindAttribute特性建立了映射关系后,ORM框架程序就能根据这些类型来操作数据库了,目前的设计将提供以下几种功能。

查询数据库,返回对象

    使用本功能,框架可以根据指定的SQL查询语句和对象类型查询数据库,并根据查询结果生成若干个对象,并设置刚刚创建的对象的属性值为字段值。在这个功能中,需要首先指定SQL查询语句和对象类型。

    在这个功能中,框架程序首先获得对象类型的所有公开属性,获得其附加的BindFieldAttribute特性,获得这些属性绑定的数据库字段名。然后执行SQL查询,遍历所有查询的纪录,对每一个记录都创建一个数据对象,并根据字段和属性的映射关系将数据库字段值保存到对象的属性值中。如此就实现了查询数据库获得对象的功能。

这个功能中需要用户指定SQL查询语言,也可以根据对象类型绑定的数据表名称来自己拼凑SQL语句。

将对象插入到数据库

    在本功能中,框架程序使用反射获得对象类型附加的BindTableAttribute特性,获得该对象映射的数据表名;然后遍历所有的公开实例属性,若属性附加了BindFieldAttribute特性,则获得该属性映射的字段名。然后收集所有的属性值和它们映射的字段名,使用字符串拼凑生成一个InsertSQL语句。然后调用数据库连接对象执行这个SQL语句,以实现向数据库新增记录的功能。

根据对象修改数据库记录

在本功能中,框架程序使用指定的对象来修改数据库中的记录。此时对象类型中至少有一个属性附加了关键字段映射特性。框架程序使用反射获得对象类型附加的BindTableAttribute 特性,获得该对象映射的数据表名,然后遍历属性,获得对象属性和数据库字段之间的映射关系。然后收集属性值,使用字符串拼凑生成一个“Update 数据表名 Set 字段1=属性1的值 ,字段2=属性2的值 ”的SQL语句。然后还遍历属性,找到所有附加了关键字段特性的属性以及绑定的字段名,拼凑出“Where 关键字段1=属性1的值 and 关键字段2=属性2的值”,这样就能拼凑出一个完整的更新数据库用的UpdateSQL语句,然后调用数据库连接对象执行这个SQL语句,就能实现更新数据库记录的功能。

根据对象删除数据库记录

    在本功能中,框架程序获得对象类型绑定的数据表名,并遍历所有的附加了绑定关键字段的特性,然后拼凑出“Delete From 数据表名Where 关键字段1=属性1的值 and 关键字段2=属性2的值”的SQL语句,然后调用数据库连接对象来执行这个SQL更新语句,这样就实现了删除数据库记录的功能。

框架程序代码说明

    根据程序设计,我已经初步的把框架程序开发出来,现在对其源代码进行说明。

数据表绑定信息 BindTableAttribute类型

    框架程序中首先定义了BindTableAttribute类型,该类型就保存了对象类型映射的数据库表的名称。其源代码为

/// <summary>
///
数据表名绑定特性
///
</summary>
[System.AttributeUsage( System.AttributeTargets.Class , AllowMultiple = false ) ]

public
class BindTableAttribute : System.Attribute
{
    /// <summary>
    /// 初始化对象
    /// </summary>
    public BindTableAttribute( )
    {
    }
    /// <summary>
    /// 初始化对象
    /// </summary>
    /// <param name="name">数据表名</param>
    public BindTableAttribute( string name )
    {
        strName = name ;
    }
    private string strName = null;
    /// <summary>
    /// 数据表名
    /// </summary>
    public string Name
    {
        get
        {
            return strName ;
        }
    }
}

    BindTableAttribute演示了如何实现自己的特性。所有的特性都是从System.Attribute类型上面派生的,在定义类型时还使用System.AttributeUsage特性来表明这个自定义特性的使用范围,这里使用了Class样式,表示BindTableAttribute特性只能用在其它的Class类型前面,若放置在InterfaceStruct类型前面,或者放在对象成员的前面则会出现编译错误。这里还是用语句 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框架的主要程序模块,它根据类型BindTableAttributeBindFieldAttribute提供的信息将应用程序对象和数据库的表和字段进行绑定,然后向数据库查询,新增,修改和删除数据库记录。应用程序使用ORM框架也基本上就是创建一个MyORMFramework的实例,然后调用它的成员。

获得对象-数据库绑定信息

    框架要实现ORM框架功能,第一步就是得获得应用程序对象和数据库的映射关系,在MyORMFramework类型中定义了GetBindInfo函数来获得这种关系。该函数的参数是应用程序的对象类型,返回值是TableBindInfo类型,该类型就是对象-数据库映射信息。由于BindTableAttributeBindFieldAttribute类型适合对应用对象类型做标记,但不适合快速查询信息,因此这里额外定义了TableBindInfoFieldBindInfo类型保存映射关系,这两个类型的代码为

/// <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类型的数组,用于保存所有的对象属性和数据库字段的映射关系。这样我们就用TableBindInfoFieldBindInfo组成了一个两层的树状列表,方便我们查询对象和数据库的绑定关系。

    函数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");
     }
  

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap