您的位置:首页 > 编程语言 > C#

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

2009-12-25 17:43 417 查看
熟悉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的工具。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: