熟悉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的工具。