來一點反射和Emit,讓ORM的使用極度簡化
PDF.NET開發框架一直是號稱“無需反射”的,因為它的ORM框架(PDF.NET不僅僅是一個ORM框架,詳細請見官網)中實體類的設計很特別,不需要反射就能夠獲知對映的欄位資訊,我們用實際的例子來說明下。
1,實體類解析
假設有這樣一個數據庫LocalDb中有一個表Table_User ,如下圖:
圖中的資料庫用PDF.NET整合開發工具開啟,該工具可以在官網找到下載地址。找到該表後,在左邊的表名稱樹節點或者右邊的查詢視窗,滑鼠右鍵選單上,找到生成實體類的功能,具體過程這裡不做演示了,因為這不是本文的主題。
下面,我們看看生成的實體類:
[Serializable()] public partial class Table_User : EntityBase { public Table_User() { TableName = "Table_User"; EntityMap = EntityMapType.Table; //IdentityName = "標識欄位名"; IdentityName = "UID"; //PrimaryKeys.Add("主鍵欄位名"); PrimaryKeys.Add("UID"); } protected override void SetFieldNames() { PropertyNames = new string[] { "UID", "Name", "Sex", "Height", "Birthday" }; } /// <summary> /// /// </summary> public System.Int32 UID { get { return getProperty<System.Int32>("UID"); } set { setProperty("UID", value); } } /// <summary> /// /// </summary> public System.String Name { get { return getProperty<System.String>("Name"); } set { setProperty("Name", value, 50); } } /// <summary> /// /// </summary> public System.Boolean Sex { get { return getProperty<System.Boolean>("Sex"); } set { setProperty("Sex", value); } } /// <summary> /// /// </summary> public System.Single Height { get { return getProperty<System.Single>("Height"); } set { setProperty("Height", value); } } /// <summary> /// /// </summary> public System.DateTime Birthday { get { return getProperty<System.DateTime>("Birthday"); } set { setProperty("Birthday", value); } } }
在實體類的建構函式中,下面幾個屬性指明瞭表的一些特性:
TableName = "Table_User"; 表示實體類對映的表名稱;
EntityMap = EntityMapType.Table; 表示實體類的對映型別是一個表,當然還可以是檢視、儲存過程、函式等;
//IdentityName = "標識欄位名";
IdentityName = "UID";
//PrimaryKeys.Add("主鍵欄位名");
PrimaryKeys.Add("UID");
這個不用多說,有註釋了。注意主鍵可以設定多個的。
protected override void SetFieldNames() 該方法說明了實體類對映的哪些欄位。
public System.Int32 UID { get { return getProperty<System.Int32>("UID"); } set { setProperty("UID", value); } } UID屬性的Get和Set方法也很簡單,看名字就知道它的功能了。注意屬性中映射了欄位名稱,比如資料庫的欄位是UID,那麼屬性改個名字,象下面這樣寫也是完全可以的:
public System.Int32 UserId { get { return getProperty<System.Int32>("UID
2,問題和優化
因此,從總體上來說,PDF.NET實體類的結構很簡單,比起EF的DbFirst方式和其它ORM框架的實體類來說,要簡單很多,所以我一般情況下都是手寫實體類,但是對於不是很熟悉框架的朋友來說,如果沒有程式碼工具,要手寫還是比較麻煩,畢竟屬性的Get和Set訪問器還是要多寫一行程式碼。
如果我們將實體類先抽象出來一個介面,然後讓框架根據該介面,自動繼承EntityBase基類和實現介面的屬性方法,那該多好啊!
PS:這個想法我已經想了好幾年了,但總覺得不是很有必要。現在,CodeFirst越來越流行了,都是先定義實體類,然後在定義或者自動建立資料庫。同樣,PDF.NET的廣大使用者也要求能夠更簡單的使用框架,跟上時代潮流。所以,我最近才付諸實際行動。
我們用一點反射和一點Emit,來完成這個過程:
反射得到建構函式和屬性定義:
//得到型別生成器
TypeBuilder typeBuilder = modBuilder.DefineType(newTypeName, newTypeAttribute, newTypeParent, newTypeInterfaces);
typeBuilder.AddInterfaceImplementation(targetType);
//定義建構函式
BuildConstructor(typeBuilder, newTypeParent, targetType.Name);
//以下將為新型別宣告方法:新型別應該override基型別的所以virtual方法
PropertyInfo[] pis = targetType.GetProperties();
List<string> propertyNames = new List<string>();
foreach (PropertyInfo pi in pis)
{
propertyNames.Add(pi.Name);
//屬性構造器
PropertyBuilder propBuilder = typeBuilder.DefineProperty(pi.Name,
System.Reflection.PropertyAttributes.HasDefault,
pi.PropertyType,
null);
MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final;
//構造Get訪問器
MethodBuilder getPropMethodBuilder = typeBuilder.DefineMethod("get_" + pi.Name,
getSetAttr,
pi.PropertyType,
Type.EmptyTypes);
GeterIL(pi.Name, newTypeParent, pi.PropertyType, getPropMethodBuilder);
//構造Set訪問器
MethodBuilder setPropMethodBuilder = typeBuilder.DefineMethod("set_" + pi.Name,
getSetAttr,
null,
new Type[] { pi.PropertyType });
SeterIL(pi.Name, newTypeParent, pi.PropertyType, setPropMethodBuilder);
//新增到屬性構造器
propBuilder.SetGetMethod(getPropMethodBuilder);
propBuilder.SetSetMethod(setPropMethodBuilder);
}
MethodBuilder SetFieldNamesBuilder = typeBuilder.DefineMethod("SetFieldNames", MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.HideBySig);
SetFieldNamesIL(newTypeParent, SetFieldNamesBuilder, propertyNames.ToArray());
//真正建立,並返回
Type resuleType=typeBuilder.CreateType();
Emit方式得到屬性訪問器的具體構造過程:
/// <summary>
/// 構造Get訪問器
/// </summary>
/// <param name="propertyName"></param>
/// <param name="baseType"></param>
/// <param name="propertyType"></param>
/// <param name="methodBuilder"></param>
void GeterIL(string propertyName, Type baseType, Type propertyType, MethodBuilder methodBuilder)
{
MethodInfo getProperty = null;
MethodInfo[] ms = typeof(EntityBase).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic);
foreach (MethodInfo info in ms)
{
if (info.Name == "getProperty" && info.IsGenericMethod)
{
getProperty = info;
break;
}
}
getProperty = getProperty.MakeGenericMethod(propertyType);
var ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldstr, propertyName);
ilGenerator.Emit(OpCodes.Call, getProperty);
ilGenerator.Emit(OpCodes.Ret);
}
/// <summary>
/// 構造Set訪問器
/// </summary>
/// <param name="propertyName"></param>
/// <param name="baseType"></param>
/// <param name="propertyType"></param>
/// <param name="methodBuilder"></param>
void SeterIL(string propertyName, Type baseType, Type propertyType, MethodBuilder methodBuilder)
{
MethodInfo setProperty =null;//= baseType.GetMethod("setProperty", BindingFlags.Instance | BindingFlags.NonPublic);
MethodInfo[] ms = typeof(EntityBase).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic);
foreach (MethodInfo info in ms)
{
if (info.Name == "setProperty" )
{
if (info.GetParameters().Length == 2)
{
setProperty = info;
break;
}
}
}
var ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldstr, propertyName);
ilGenerator.Emit(OpCodes.Ldarg_1);
//是否是值型別
if (propertyType.IsValueType)
ilGenerator.Emit(OpCodes.Box, propertyType);
ilGenerator.Emit(OpCodes.Call, setProperty);
ilGenerator.Emit(OpCodes.Ret);
}
在上面的IL程式碼方法中,EntityBase 的 getProperty 和setProperty 方法有泛型實現和過載,所以只有遍歷實體類所有的方法。
寫Emit程式碼也不是想象中的那麼複雜,基本過程就是先手工寫好C#程式碼,編譯得到Exe或者Dll,然後用ILDASM或反編譯工具,得到IL程式碼,最後就是看著IL程式碼,用Emit一個個對應發出程式碼,就行了。
OK,我們將這個程式碼封裝到一個EntityBuilder類中,定一個構造實體類的方法
private static Dictionary<Type, Type> dictEntityType = new Dictionary<Type, Type>();
private static object sync_lock = new object();
/// <summary>
/// 根據介面型別,建立實體類的例項
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T CreateEntity<T>() where T:class
{
Type targetType = null;
Type sourceType = typeof(T);
if (sourceType.BaseType == typeof(EntityBase)) //如果本身是實體類,則不生成
{
targetType = sourceType;
}
else
{
if (!dictEntityType.TryGetValue(sourceType, out targetType))
{
lock (sync_lock)
{
if (!dictEntityType.TryGetValue(sourceType, out targetType))
{
EntityBuilder builder = new EntityBuilder(sourceType);
targetType = builder.Build();
dictEntityType[sourceType] = targetType;
}
}
}
}
T entity = (T)Activator.CreateInstance(targetType);
return entity;
}
萬事俱備,只欠東風!
3,更簡單的使用方式
下面,我們將前面的實體類抽象出一個介面ITable_User :
public interface ITable_User
{
DateTime Birthday { get; set; }
float Height { get; set; }
string Name { get; set; }
bool Sex { get; set; }
int UID { get; set; }
}
再做一點準備工作,在應用程式配置檔案裡面配置一下連線,框架預設取最後一個配置:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="local"
connectionString="Data Source=.;Initial Catalog=LocalDB;Integrated Security=True"
providerName="SqlServer" />
</connectionStrings>
</configuration>
然後像下面這樣使用實體類並查詢:
static void TestDynamicEntity()
{
ITable_User user = EntityBuilder.CreateEntity<ITable_User>();
//如果介面的名稱不是"ITableName" 這樣的格式,那麼需要呼叫 MapNewTableName方法指定
//((EntityBase)user).MapNewTableName("Table_User");
OQL qUser = OQL.From((EntityBase)user).Select(user.UID, user.Name, user.Sex).END;
List<ITable_User> users = EntityQuery.QueryList<ITable_User>(qUser, MyDB.Instance);
}
在程式碼中,只需要
EntityBuilder.CreateEntity<ITable_User>(); 這樣的方式,定義一個實體類的介面,就自動建立了我們的實體類,是不是非常簡單了?
有了實體類,然後可以像普通實體類那樣來使用ORM查詢語言--OQL,不過原來的EntityQuery泛型實體查詢類得改進下,才可以支援“動態實體類”的查詢。
當前功能已經在PDF.NET Ver 4.6.4.0525 版本實現,之前的版本,大家可以去開源專案下載:http://pwmis.codeplex.com
4,動態實體類的使用約束
這裡說的“動態實體類”是通過程式在執行時動態建立得到實體類,而不是預先在原始碼中寫好的實體類。對本方案而言,使用動態實體類有以下幾點約束:
- 使用介面(interface)定義實體類
- 實體類屬性定義需要get,set 訪問器同時存在(否則怎麼儲存資料到資料庫?)
- 屬性名稱跟表字段名稱一致,且屬性型別跟欄位的資料型別相相容
- 介面名稱為“I”打頭的表名稱,否則需要使用時候對映一下
如果你不想有這些約束,或者想靈活對映欄位和屬性,那麼還是手寫實體類吧,多寫一行程式碼,象本文開頭示例的那個實體類一樣。
-----------------------------------------