利用Expression实现DbReader到对象属性的绑定

最近在工做中的一个项目使用了一种很小众的数据库:Vertica。小众的东西有不少缺点,好比它的.Net client没有实现LINQ。而这个项目有大量的读取数据库的操做,以前用惯了LINQ,如今却要回到原始社会,写command,从DbReader里把值读出来,写到对象的属性上,想一想就各类不爽:大量的hard code的字段名称,不光容易写错,还容易写漏;赋值时,又要处理null值,又要处理各类数据类型的转换。不光代码写起来麻烦,还要写不少测试来保证这些代码的正确。而在LINQ的帮助下,这一切都是透明的。sql

为了能心情愉悦的完成项目,期望HP能出个LINQ provider是没戏的了,我决定从LINQ中借鉴点东西来。因为项目中大部分查询都会比较复杂,把一个lambda表达式翻译成sql的实现太复杂了,根本没办法在一个很短的时间内完成,而写个sql语句,调用DbCommand对象执行下,也不是很麻烦的事,我真正在乎的是诸以下面的代码,写起来很麻烦。数据库

obj.PropertyA = (int)reader.GetValue(reader.GetOrdinal("FieldA"))

而若是能够按照下面的方式来写,就比较容易了,不光不容易写错,并且还有智能提示的帮助,在代码的输入速度上也会有很大的提高。express

reader.BindValue(obj, o => o.PropertyA);

如今目标有了,问题是如何将其转换为我实际想要的功能。而要实现从DbReader里读取数据。将数据作适当的类型转换(若是须要的话),而后赋值到对应的属性上这一系列操做,须要从上面这个代码里获取到以下信息:ide

  • 属性名称
  • 属性类型
  • 数据库字段名称
  • 数据库字段类型
  • 属性的setValue方法

所幸,这一切信息咱们都能从上面代码中的lambda表达式中获取出来。性能

首先,咱们须要来定义BindValue方法:测试

public static void BindValue<T>(this DbReader, T obj, Expression<Func<T, TProp>> prop)
{
}

咱们不能把prop参数定义成委托,而是Expression<TDelegate>类型。有了这个表达式对象,一切都变的很容易了:this

            var memberExp = prop.Body as MemberExpression;
            var memberInfo = memberExp.Member;
            var fieldName = memberInfo.Name;
            var typeName = memberInfo.DeclaringType.FullName;
            var propType = (memberInfo as PropertyInfo).PropertyType;

经过上面的代码,咱们知道了字段的名称,字段的类型,以及字段的MemberInfo,有了这些,咱们就能够经过反射将DbReader里的值获取出来,而后赋值到对象的属性上了。spa

不过因为读取数据库是个频繁的操做,若是每次都经过反射的方式去赋值,在性能上有些损失。为了程序编写的方便而损失性能有些不太合适,所以,咱们须要更近一步:动态的生成一个方法,而之后就能够直接调用方法,而不用经过反射的方法了。在以往,想动态的生成一些代码,只能经过System.Reflection.Emit命名空间下的类来实现,十分困难;而有了Expression这一利器之后,作这件事就容易多了。咱们要作的事就是将obj.PropertyA = (int)reader.GetValue(reader.GetOrdinal("FieldA"))这一操做分解,将各个部分对应到相应的Expression对象上,而后将这些对象组合起来,获得一个LambdaExpression对象,最后编译一下,获得咱们最终想要的方法,一个代理对象。最终效果以下:翻译

                var company = new CompanyEntity();
                company.BindTo(reader)
                    .With(item => item.CompanyId)
                    .With(item => item.Name)
                    .With(item => item.CountryId)
                    .With(item => item.Address);

下面给出完整的实现 (因为项目是3.5的,若是是在4.0下,实现则会更简单些):代理

    /// <summary>
    /// this class provides some functions about the data from the database, like convert the data to target's type.
    /// </summary>
    public static class DbDataHelper
    {
        readonly static object lockObj = new object();
        private static Dictionary<string, Delegate> propBinders = new Dictionary<string, Delegate>();

        /// <summary>
        /// bind the data from DbReader to the property in the expression.
        /// </summary>
        /// <typeparam name="TObj"></typeparam>
        /// <typeparam name="TProp"></typeparam>
        /// <param name="obj"></param>
        /// <param name="prop">to specify the property which you want to bind</param>
        /// <param name="reader"></param>
        /// <param name="colName"></param>
        /// <param name="customerConv"></param>
        private static void BindValue<TObj, TProp>(
            this TObj obj,
            Expression<Func<TObj, TProp>> prop,
            DbDataReader reader,
            string colName,
            Func<object, TProp> customerConv)
        {
            if (prop.NodeType != ExpressionType.Lambda) throw new ArgumentException("prop");
            if (prop.Body.NodeType != ExpressionType.MemberAccess) throw new ArgumentException("prop");

            var memberExp = prop.Body as MemberExpression;
            var memberInfo = memberExp.Member;
            var fieldName = memberInfo.Name;
            var typeName = memberInfo.DeclaringType.FullName;
            var propType = (memberInfo as PropertyInfo).PropertyType;

            if (!propBinders.ContainsKey(typeName + "-" + fieldName))
            {
                lock (lockObj)
                {
                    if (!propBinders.ContainsKey(typeName + "-" + fieldName))
                    {
                        if (colName == null)
                        {
                            colName = fieldName;
                            // if colName != fieldName, get colName from DbColumnAttribute
                            var colAttrs = memberInfo.GetCustomAttributes(typeof(DbColumnAttribute), false);
                            if (colAttrs != null && colAttrs.Length == 1)
                            {
                                colName = (colAttrs[0] as DbColumnAttribute).Name;
                            }
                        }

                        var paraObjExp = Expression.Parameter(typeof(TObj), "obj");
                        var paraDbReaderExp = Expression.Parameter(typeof(DbDataReader), "reader");

                        var fldIdx = reader.GetOrdinal(colName);
                        var constFldIdxExp = Expression.Constant(fldIdx);
                        var fldType = reader.GetFieldType(fldIdx);

                        var getValueCallExp = Expression.Call(
                            Expression.Convert(paraDbReaderExp, reader.GetType()),
                            reader.GetType().GetMethod("GetValue"),
                            constFldIdxExp);

                        MethodCallExpression convValueExp = null;
                        if (customerConv != null)
                        {
                            convValueExp = Expression.Call(
                                Expression.Constant(customerConv.Target),
                                customerConv.Method,
                                getValueCallExp);
                        }
                        else
                        {
                            convValueExp = Expression.Call(
                                typeof(DbDataHelper).GetMethod("GetValue", BindingFlags.NonPublic | BindingFlags.Static),
                                getValueCallExp,
                                Expression.Constant(fldType),
                                Expression.Constant(propType));
                        }

                        var setValueExp = Expression.Call(
                            Expression.Convert(paraObjExp, typeof(TObj)),
                            (memberInfo as PropertyInfo).GetSetMethod(),
                            Expression.Convert(convValueExp, propType));
                        var lambda = Expression.Lambda<Action<TObj, DbDataReader>>(setValueExp, paraObjExp, paraDbReaderExp);
                        var action = lambda.Compile();
                        propBinders.Add(typeName + "-" + fieldName, action);
                    }
                }
            }
            var setAction = propBinders[typeName + "-" + fieldName] as Action<TObj, DbDataReader>;
            setAction(obj, reader);
        }

        /// <summary>
        /// type convert help function
        /// </summary>
        /// <param name="input"></param>
        /// <param name="inputType"></param>
        /// <param name="outputType"></param>
        /// <returns></returns>
        private static object GetValue(object input, Type inputType, Type outputType)
        {
            if (inputType == outputType)
                if (input == DBNull.Value) return null;
                else return input;

            if (input == DBNull.Value
                && (outputType.IsGenericType && outputType.GetGenericTypeDefinition() == typeof(Nullable<>)
                || outputType == typeof(string)))
                return null;

            if (outputType.IsGenericType && typeof(Nullable<>) == outputType.GetGenericTypeDefinition())
            {
                var argType = outputType.GetGenericArguments()[0];
                if (argType == inputType)
                {
                    return input;
                }
                else
                {
                    var val = Convert.ChangeType(input, argType);
                    return val;
                }
            }
            return Convert.ChangeType(input, outputType);
        }

        /// <summary>
        /// bind the data from DbDataReader to the property in the expression
        /// </summary>
        /// <typeparam name="TObj"></typeparam>
        /// <typeparam name="TProp"></typeparam>
        /// <param name="binder"></param>
        /// <param name="prop"></param>
        /// <returns></returns>
        public static DataBinder<TObj> With<TObj, TProp>(
            this DataBinder<TObj> binder,
            Expression<Func<TObj, TProp>> prop)
        {
            binder.Obj.BindValue(prop, binder.Reader, null, null);
            return binder;
        }

        /// <summary>
        /// bind the data from DbDataReader to the property in the expression
        /// </summary>
        /// <typeparam name="TObj"></typeparam>
        /// <typeparam name="TProp"></typeparam>
        /// <param name="binder"></param>
        /// <param name="prop"></param>
        /// <param name="colName"></param>
        /// <param name="custConv"></param>
        /// <returns></returns>
        public static DataBinder<TObj> With<TObj, TProp>(
            this DataBinder<TObj> binder,
            Expression<Func<TObj, TProp>> prop,
            string colName,
            Func<object, TProp> custConv)
        {
            binder.Obj.BindValue(prop, binder.Reader, colName, custConv);
            return binder;
        }

        /// <summary>
        /// create a binder between obj and DbDataReader
        /// </summary>
        /// <typeparam name="TObj"></typeparam>
        /// <param name="obj"></param>
        /// <param name="reader"></param>
        /// <returns></returns>
        public static DataBinder<TObj> BindTo<TObj>(this TObj obj, DbDataReader reader)
        {
            return new DataBinder<TObj>(obj, reader);
        }

        /// <summary>
        /// a container to keep the object and DbDataReader for BindTo function
        /// </summary>
        /// <typeparam name="TObj"></typeparam>
        public class DataBinder<TObj>
        {
            /// <summary>
            /// 
            /// </summary>
            public TObj Obj { get; private set; }
            /// <summary>
            /// 
            /// </summary>
            public DbDataReader Reader { get; private set; }

            internal DataBinder(TObj obj, DbDataReader reader)
            {
                this.Obj = obj;
                this.Reader = reader;
            }
        }
    }

    /// <summary>
    /// if the property name is not equals to the name in the database, use it to specify the name in database
    /// </summary>
    public class DbColumnAttribute : Attribute
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        public DbColumnAttribute(string name)
        {
            this.Name = name;
        }

        /// <summary>
        /// 
        /// </summary>
        public string Name { get; set; }
    }
相关文章
相关标签/搜索