來一點反射和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"); }
           
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 的 getPropertysetProperty  方法有泛型實現和重載,所以只有遍歷實體類所有的方法。

寫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,動態實體類的使用約束

這里說的“動態實體類”是通過程序在運行時動態創建得到實體類,而不是預先在源碼中寫好的實體類。對本方案而言,使用動態實體類有以下幾點約束:

  1. 使用接口(interface)定義實體類
  2. 實體類屬性定義需要get,set 訪問器同時存在(否則怎么保存數據到數據庫?)
  3. 屬性名稱跟表字段名稱一致,且屬性類型跟字段的數據類型相兼容
  4. 接口名稱為“I”打頭的表名稱,否則需要使用時候映射一下

 

如果你不想有這些約束,或者想靈活映射字段和屬性,那么還是手寫實體類吧,多寫一行代碼,象本文開頭示例的那個實體類一樣。

-----------------------------------------

歡迎加入PDF.NET開源技術團隊,做最快最好的開發框架!

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM