来一点反射和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 的 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,动态实体类的使用约束

这里说的“动态实体类”是通过程序在运行时动态创建得到实体类,而不是预先在源码中写好的实体类。对本方案而言,使用动态实体类有以下几点约束:

  1. 使用接口(interface)定义实体类
  2. 实体类属性定义需要get,set 访问器同时存在(否则怎么保存数据到数据库?)
  3. 属性名称跟表字段名称一致,且属性类型跟字段的数据类型相兼容
  4. 接口名称为“I”打头的表名称,否则需要使用时候映射一下

 

如果你不想有这些约束,或者想灵活映射字段和属性,那么还是手写实体类吧,多写一行代码,象本文开头示例的那个实体类一样。

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

欢迎加入PDF.NET开源技术团队,做最快最好的开发框架!

 

 

 

 

 

 

    本文转自深蓝医生博客园博客,原文链接:http://www.cnblogs.com/bluedoctor/archive/2013/05/26/3100049.html,如需转载请自行联系原作者




相关文章
|
消息中间件 NoSQL Java
Redis监听Key的过期事件
在redis.conf配置文件中有个配置项:notify-keyspace-events " " ,默认是没有key的过期监听的,我们需要将其开启
2359 0
|
机器学习/深度学习 人工智能 算法
【悬念揭秘】ML.NET:那片未被探索的机器学习宝藏,如何让普通开发者一夜变身AI高手?——从零开始,揭秘构建智能应用的神秘旅程!
【8月更文挑战第28天】ML.NET 是微软推出的一款开源机器学习框架,专为希望在本地应用中嵌入智能功能的 .NET 开发者设计。无需深厚的数据科学背景,即可实现预测分析、推荐系统和图像识别等功能。它支持多种数据源,提供丰富的预处理工具和多样化的机器学习算法,简化了数据处理和模型训练流程。
325 1
|
机器学习/深度学习 自然语言处理
一文读懂深度学习:从神经元到BERT
自然语言处理领域的殿堂标志 BERT 并非横空出世,背后有它的发展原理。今天,蚂蚁金服财富对话算法团队整理对比了深度学习模型在自然语言处理领域的发展历程。
28536 0
|
Web App开发 安全 算法
让互联网更快:新一代QUIC协议在腾讯的技术实践分享
本文来自腾讯资深研发工程师罗成在InfoQ的技术分享。 1、前言 如果:你的 App,在不需要任何修改的情况下就能提升 15% 以上的访问速度,特别是弱网络的时候能够提升 20% 以上的访问速度。
2306 0
|
大数据
信息技术新工科产学研联盟第一届年会在北京隆重召开
信息技术新工科产学研联盟第一届年会于2018年1月14日在新云南皇冠假日酒店隆重召开。教育部、工信部领导莅临现场指导,已经加入联盟的500多家高校和100多家企业派出代表参会,并吸引到大批媒体记者现场采访,会场气氛十分热烈。
1825 0
|
Windows
使 IIS 6.0 可以在 64 位 Windows 上运行 32 位应用程序 试图加载格式不正确的程序。
原文 使 IIS 6.0 可以在 64 位 Windows 上运行 32 位应用程序 试图加载格式不正确的程序。  win7 64位操作系统上边运行IIS网站应用的时候,提示错误"试图加载格式不正确的程序。
1197 0
|
17小时前
|
云安全 人工智能 自然语言处理
|
5天前
|
搜索推荐 编译器 Linux
一个可用于企业开发及通用跨平台的Makefile文件
一款适用于企业级开发的通用跨平台Makefile,支持C/C++混合编译、多目标输出(可执行文件、静态/动态库)、Release/Debug版本管理。配置简洁,仅需修改带`MF_CONFIGURE_`前缀的变量,支持脚本化配置与子Makefile管理,具备完善日志、错误提示和跨平台兼容性,附详细文档与示例,便于学习与集成。
309 116