利用C#的动态类型来实现与rails类似的元编程(1)

来源:互联网 发布:软件发票 税点 编辑:程序博客网 时间:2024/05/19 07:26

熟悉ruby on rails的开发员都知道,在ruby中,有一个很重要的特性,就是能够实现元编程,特别是在用于开发Web应用的rails框架中,用的特别多。在rails中,要创建一个动态方法并与数据库表字段相关联,主要的的步骤大概有这些:

1、首先配置好数据库的连接。

2、创建一个ActiveRecord模型,这个模型与数据库的表名称有一定的关系(具体的可以参考相关rails文档)

3、创建的一个ActiveRecord是继承于Base类的,其中Base类中封装了基本的CRUD操作

3、然后在程序运行时,动态关联到表的字段,或者执行数据可的CRUD操作。

 

比如有一个作者表author,包含有字段id,first_name,last_name。然后用rails工具生成一个与之关联的Module类Author,它是一个空类:

class Author < ActiveRecord::Base

end

 

然而,在实际操作中,我们却可以这样操作它:

 @author = Author.find(id)

 @name = @author.first_name

 @author.destory

 

这些在静态语言C#中是不可能做得到的。在C#的动态方法没有出现之前,通常为了实现实体与数据库的表映射,都是先创建一个固定的实体类,实体类的属性通过Attribute映射到表的字段,一般都是像下面的代码所示:

    [TableMap("overtimeinfo","id")]   
    public class OverTimeEntity
    {
        [ColumnMap("id",DbType.Int32)]
        public int GUID{get;set;}       
       
        [ColumnMap("applyUser",DbType.String,"")]
        public  string UserName{get;set;}
       
        [ColumnMap("applyuserid",DbType.Int32)]
        public  int UserId{get;set;}

        [ColumnMap("FormNo",DbType.String,"")]
        public  string FormNo{get;set;}

      }

 

以上的C#代码能这样写还得靠C# 3.0提供了属性的自动实现,在这之前,都是需要要有相应的私有字段的,所以那时的办法是事先定义好一个与表结构相同的抽象类,抽象类定义好属性,然后利用CodeDom根据抽象类动态编译一个抽象类的实现类,下面的代码为抽象类:

    [TableMap("overtimeinfo","id")]
    public abstract class OverTimeEntity
    {
        [ColumnMap("id",DbType.Int32)]
        public abstract int GUID{get;set;}       
       
        [ColumnMap("applyUser",DbType.String,"")]
        public abstract string UserName{get;set;}
       
        [ColumnMap("applyuserid",DbType.Int32)]
        public abstract int UserId{get;set;}

        [ColumnMap("FormNo",DbType.String,"")]
        public abstract string FormNo{get;set;}

        [ColumnMap("flowid",DbType.String,"")]
        public abstract string Flowid{get;set;}

  }

 

在C#中,通过以上的途径来实现与数据库的ORM映射,可以想象那时的编写一个实体类那么的难,特别是存在几十、甚至上百个表的时候,尽管可以自己编写一个代码工具。

 

幸运的是,在.NET 4.0中,C#已经提过了与动态语言类似的动态方法调用,一切都变得那么的容易了,可以终于从代码中解脱出来,专注于业务实现方面。下面的例子简单的演示了C#是如何实现动态方法调用的,只要是用到了关键字dynamic :

class DynamicTest
    {
        public void PrintText()
        {
            Console.WriteLine("call dynamic method....");
        }
    }

上面定义了一个类,然后我们就可以这样创建一个实例:

   static void Main(string[] args)
        {
            dynamic test = new DynamicTest();
            test.PrintText();
            Console.ReadKey(true);

        }

 

虽然这样看起来与DynamicTest test = new DynamicTest();创建一个实例后调用PrintText没有任何的区别,但是别忘记了,动态方法最主要的是它在编译时是不检测,而在运行时检测的,所以这中间可以让我们做很多可以改变原有类的事情。

 

现在我就通过一个示例来演示下如何利用C#的动态方法来实现与数据库表的映射。

 

一、创建基类以及具体的实体类

 

首先,创建一个所有Module都要集成的基类BaseActiveRecord,里面封装了CRUD的数据库操作,还有一个获取表名和主键名的属性,BaseActiveRecord类的代码如下:

  public abstract class BaseActiveRecord
    {
        

         /// <summary>
        /// 表名称
        /// </summary>
        public virtual string TableName { get; protected set; }

 

        /// <summary>
        /// 主键
        /// </summary>
        public virtual string PrimaryKey { get; protected set; }

 

        /// <summary>
        /// 表字段
        /// </summary>
        public virtual string[] Columns { get; protected set; }


        public void save() { Console.WriteLine("save ....."); }

        public void delete() { Console.WriteLine("delete .....");  }

        public void update() { Console.WriteLine("update ....."); }

        public dynamic findById(dynamic id) { Console.WriteLine("get ....."); return null; }

        public dynamic findAll() { Console.WriteLine("getall .....");  return null; }
    }

 

    public static class BaseActiveRecordExtensions
    {
        public static dynamic initiation(this BaseActiveRecord o)
        {
           EntityClassGenerator entity = new EntityClassGenerator();
           return entity.GenerateEntity(o.GetType());
        }
    }

 

BaseActiveRecordExtensions类定义了一个BaseActiveRecord的扩展方法,用来生成实体与数据库表的映射,其实这里也可以没有必要定义这个扩展方法的。基类定义了好后,创建一个实体类Author了,代码如下:

 

[TableMap(TableName = "AUTHORS",PrimaryKey="ID")]
    partial class Author : BaseActiveRecord
    {

    }

 

我们定义具体类仅需做的就是这些,实体类就是一个空白类,没有包含任何的属性,方法。在上面的代码中用到了一个自定义属性TableMap,它的作用就是后面需要用到的利用CodeDom生成Author时指明映射的表名称以及主键,此外还用到了partial关键字,因为在C#中,一个相同的类利用partial关键字就可以实现存放于多个代码文件中。

 

二、创建把实体类映射到数据库的工具类

在创建映射到数据库表的工具类中,用到了CodeDom类库。工具类的主要实现原理是这样的:

1、根据实体类的Type创建一个同类名的类。

2、获取实体类自定义属性TableMap的TableName值,根据TableName值从数据库中获取表的字段结构,如字段名、字段类型、主键。

3、根据获取到的表结构动态生成实体类

4、编译成功后返回动态类型

5、调用。

 

下面就是工具类EntityClassGenerator的代码:

 

class EntityClassGenerator
    {
        private const string DATE = "DATE";
        private const string NUMBER = "NUMBER";
        private const string VARCHAR2 = "VARCHAR2";

        private Type entityTypeInterface;
        private CodeCompileUnit compileUnit;
        private CodeNamespace tempdAssemblyNameSpace;
        private string className = "";
        private StringCollection referencedAssemblies;

        /// <summary>
        /// 动态生成代码,并且编译
        /// </summary>
        /// <param name="entityType"></param>
        /// <returns></returns>
        public dynamic GenerateEntity(Type entityType)
        {
            Type type = CacheProxy.GetChchedString(entityType.Name) as Type;
            // 存在缓存中,则直接取出来
            if (type != null)
            {
                return type.Assembly.CreateInstance(type.FullName);
            }
            entityTypeInterface = entityType;
            compileUnit = new CodeCompileUnit();
            tempdAssemblyNameSpace = new CodeNamespace(entityTypeInterface.Namespace);
            tempdAssemblyNameSpace.Imports.Add(new CodeNamespaceImport("System"));
            tempdAssemblyNameSpace.Imports.Add(new CodeNamespaceImport("System.Data"));
            className = entityType.Name;

            referencedAssemblies = new StringCollection();
            referencedAssemblies.Add("System.dll");
            referencedAssemblies.Add("System.Data.dll");
            referencedAssemblies.Add(entityType.Assembly.Location);

            CodeTypeDeclaration generateClass = CreateClass();
            tempdAssemblyNameSpace.Types.Add(generateClass);
            compileUnit.Namespaces.Add(tempdAssemblyNameSpace);

            CodeDomProvider provider = new CSharpCodeProvider();

            CompilerParameters cp = new CompilerParameters();
            foreach (string refassembly in referencedAssemblies)
                cp.ReferencedAssemblies.Add(refassembly);

            cp.IncludeDebugInformation = false;
            cp.GenerateInMemory = true;

            // 把程序集的输入路径设置为临时文件目录
            cp.OutputAssembly = Path.GetTempPath() + className + ".dll";

            //输出CS代码到文件
           // OutputSourceCode(compileUnit, provider, "text");

            CompilerResults results = provider.CompileAssemblyFromDom(cp, compileUnit);
            Assembly createdAssembly = results.CompiledAssembly;
            Type resultType = createdAssembly.GetType(tempdAssemblyNameSpace.Name + "." + className);

            // 存入缓存
            CacheProxy.CacheObjectForEver(entityType.Name, resultType);

            Object obj = createdAssembly.CreateInstance(resultType.FullName);
            return obj;
        }

        /// <summary>
        /// 创建类
        /// </summary>
        /// <returns></returns>
        private CodeTypeDeclaration CreateClass()
        {
            CodeTypeDeclaration ctd = new CodeTypeDeclaration(className);
            ctd.IsClass = true;
            ctd.IsPartial = true;
            ctd.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            ctd.BaseTypes.Add(entityTypeInterface.BaseType);
            ctd.CustomAttributes.Add(new CodeAttributeDeclaration("Serializable"));

            string tableName = "";

            // 获取表名
            TableMapAttribute attr=  Attribute.GetCustomAttribute(entityTypeInterface, typeof(TableMapAttribute)) as TableMapAttribute;
            tableName = attr.TableName;
            string columnName = "";
            string dataType = "";
            List<CodePrimitiveExpression> collection = new List<CodePrimitiveExpression>();
            CodeMemberField field;
            CodeMemberProperty property;

            DataTable table = getTable(tableName);

            // 构建字段、属性
            foreach (DataRow row in table.Rows)
            {
                columnName = row["column_name"].ToString();
                dataType = row["data_type"].ToString();
                collection.Add(new CodePrimitiveExpression(columnName.ToLower()));

                // 构建私有字段
                field = new CodeMemberField(ConvertDBType2CLR(dataType),"_"+columnName.ToLower());
                ctd.Members.Add(field);
                // END 构建私有字段

                // 构建属性
                property = new CodeMemberProperty();
                property.Name = columnName.ToLower();
                property.Type = new CodeTypeReference(ConvertDBType2CLR(dataType));
                property.Attributes = MemberAttributes.Public;
                property.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_" + columnName.ToLower())));
                property.SetStatements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_" + columnName.ToLower()), new CodePropertySetValueReferenceExpression()));
                ctd.Members.Add(property);
                // END 构建属性
            }

            // 构建构造函数
            CodeConstructor defaultConstructor = new CodeConstructor();
            defaultConstructor.Attributes = MemberAttributes.Public;

            // TableName
            CodeExpression left = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "TableName");
            CodeExpression right = new CodePrimitiveExpression(tableName);
            CodeAssignStatement asEntity = new CodeAssignStatement(left, right);
            defaultConstructor.Statements.Add(asEntity);

            // PrimaryKey
            left = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "PrimaryKey");
            right = new CodePrimitiveExpression(getPrimaryKey(tableName));
            asEntity = new CodeAssignStatement(left, right);
            defaultConstructor.Statements.Add(asEntity);
    
            // Columns
            left = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "Columns");
            right = new CodeArrayCreateExpression(typeof(System.String),collection.ToArray());
            asEntity = new CodeAssignStatement(left, right);
            defaultConstructor.Statements.Add(asEntity);

            ctd.Members.Add(defaultConstructor);
            // END 构建构造函数
            return ctd;
        }


        /// <summary>
        /// 根据表名获取表结构
        /// </summary>
        /// <param name="tableName"></param>
        /// <returns></returns>
        private DataTable getTable(string tableName)
        {
           DataHelper helper = new DataHelper();
           helper.open();
           string sql = string.Format("select column_name,data_type from user_tab_columns  where table_name = '{0}'", tableName);
           DataSet set = helper.GetDataSet(sql);
           helper.close();
           return set.Tables[0];
        }

        /// <summary>
        /// 获取主键
        /// </summary>
        /// <param name="tableName"></param>
        /// <returns></returns>
        private string getPrimaryKey(string tableName)
        {
            string sql = string.Format("select column_name  from user_cons_columns  where constraint_name = (select constraint_name from user_constraints where table_name = '{0}'  and constraint_type = 'P')",tableName);
            DataHelper helper = new DataHelper();
            helper.open();
           string value = helper.getObject<string>(sql);
            helper.close();
            return value;
        }

        /// <summary>
        /// 把数据库类型转换成CLR
        /// </summary>
        /// <param name="dataType"></param>
        /// <returns></returns>
        private string ConvertDBType2CLR(string dataType)
        {
            if (dataType.ToUpper() == VARCHAR2)
            {
                return "System.String";
            }
            else if (dataType.ToUpper() == DATE)
            {
                return "System.DateTime";
            }
            else if (dataType.ToUpper() == NUMBER)
            {
                return "System.Int32";
            }
            else
            {
                return "dynamic";
            }
        }

        /// <summary>
        /// 把生成的代码输出到文件
        /// </summary>
        /// <param name="compileUnit"></param>
        /// <param name="provider"></param>
        /// <param name="strClassName"></param>
        protected void OutputSourceCode(CodeCompileUnit compileUnit, CodeDomProvider provider, string strClassName)
        {
            ICodeGenerator gen = provider.CreateGenerator();
            StreamWriter writer = new StreamWriter(@"f:/temp/" + strClassName + ".cs", false);
            CodeGeneratorOptions cop = new CodeGeneratorOptions();
            cop.IndentString = "    ";
            cop.BracingStyle = "C";
            gen.GenerateCodeFromCompileUnit(compileUnit, writer, cop);
            writer.Close();
        }
    }

 

 

三、运行程序,把表映射到实体。

好了,准备工作都已经做好了,现在让我们来看看效果吧,在main方法中输入下列测试代码:

class Program
    {
        static void Main(string[] args)
        {
            Author author = new Author();
           dynamic  auth = author.initiation();

           Console.WriteLine("auth's TableName is {0}", auth.TableName);
           Console.WriteLine("auth's PrimaryKey is {0}", auth.PrimaryKey);
            // 属性赋值
           auth.first_name = "William";
           auth.last_name = "Henry";
           Console.WriteLine("auth's name is {0} {1}", auth.first_name, auth.last_name);
            // 执行保存方法
           auth.save();
           Console.ReadKey(true);

     }

 

 映射工具生成的代码文件:

[Serializable()]
 public partial class Author : BaseActiveRecord
 {
  
  private int _id;
  private string _first_name;
  private string _last_name;
  public Author()
  {
   this.TableName = "AUTHORS";
   this.PrimaryKey = "ID";
  }
  
  public virtual int id
  {
   get   {    return this._id;   }
   set   {    this._id = value;  }
  }
  
  public virtual string first_name
  {
   get   {    return this._first_name;   }
   set   {    this._first_name = value;  }
  }
  
  public virtual string last_name
  {
   get   {    return this._last_name;   }
   set   {    this._last_name = value;  }
  }
 }

 

执行后的效果图:

 

OK,已经达到了我们想要的效果了。在上面的实体映射生成工具的地方,有一个地方需要注意的是,就是每次调用initiation()的时候都会重新生成一遍代码,所以我们利用了一个缓存类来存储CodeDom生成的类型,我们可以在第一次生成的时候可以把这个类型放在一个缓存中,以后再次用到的时候就没有必要再次连接数据库重新生成了,除非你的表结构已经更改了。

 

在这篇文章中,主要就是介绍了利用代码生成来构建ORM映射,下一篇将会把操作数据库的CRUD方法实现,在下篇中,将会用到一个动态构建SQL的工具。

 

原创粉丝点击