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"); }
set { setProperty("UID", value); }
}
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”打頭的表名稱,否則需要使用時候映射一下
如果你不想有這些約束,或者想靈活映射字段和屬性,那么還是手寫實體類吧,多寫一行代碼,象本文開頭示例的那個實體類一樣。
-----------------------------------------
歡迎加入PDF.NET開源技術團隊,做最快最好的開發框架!