Wind-Eagle

No pain,no gain!
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

数据访问层组件设计以及选型意识流 开篇

Posted on 2012-08-06 12:01  Andrew Yin  阅读(531)  评论(1编辑  收藏  举报
   

两年多没有登录园子了,可见哥的懒惰程度,刚刚通过邮箱找回了用户名和密码。

 

今天上午的任务是系统地写一篇文章来说明我最近开发的一个数据访问层组件(你也可以称之为OR Mapping 组件)的开发思路,以及把它和其他 OR Mapping 组件 进行PK。

 

你知道的,哥一向不喜欢说废话的,进入正题的意识流。

 

 首先,最基本的,我们对数据库的访问主要有哪些形式呢?哪些最基本的需求呢?

 

回答这个问题,我们来看看最最基础的ADO .NET有哪些数据访问接口,发现如下:

 

ExecuteNonQuery

ExecuteScalar

ExecuteReader

ExecuteDataset

FillDataSet

UpdateDataset

ExecuteXmlReader

 

很简单嘛,除了后面三个是微软特殊的以外,上面四个是都有的,微软也是从ASP时代就提供了的,十几年了,就没变过,可见,数据访问的基本需求是多么的稳定啊!

 

 但是,看似简单其实又不是那么简单 ,我们再来看看,我们执行一个数据库操作需要提供些什么信息吧。想想便有如下答案:

 

数据库Provider,

数据库连接字符串,

SQL 语句/存储过程名/表名,

CommandType,

输入参数,

输出参数

 

前两项,在我们的应用程序中值很固定,我们不用写死在程序里,可以配置起来,比如,可以创建一个Database对象,然后用Ioc框架(如Unity)注射一个单例, Database类代码如下:

 

    public class DataBase
    {
        private static DataBase _instance;
        public static DataBase Instance
        {
            get { return _instance ?? (_instance = new DataBase()); }
            set { _instance = value; }

        }

        public string ProviderAlians { getset; }
        public string ConnAlians { getset; }
    }

 

 

 当然,你也可以不把这两者绑在一起,Provider是Provider,Conn是Conn,在做数据库访问的时候,你不想用由两个字符串组成的Database对象作为一个参数,而是想用

string Provider 和一个 (IDbConnection或者IDbTransaction), 特别是你想一个数据库连接执行多个操作(多个操作可能是在一个事务里)。

我选这种方式,当然前面的那个Database类可不是白写,后面也是有用的。我会只用一个Conn参数,Provider 应该是个路由KEY,路由到合适的数据库访问AdoHelper类(后面会提到)。

 

 

 好了,上面的七个操作(当然,这个时代,后三个基本可以被忽视了),都可以有如下重载:

 

public virtual int ExecuteNonQuery(string conn, string commandText, CommandType commandType)
public virtual int ExecuteNonQuery(IDbConnection conn, string commandText, CommandType commandType)
public virtual int ExecuteNonQuery(IDbTransaction conn, string commandText, CommandType commandType)

 

 

或者一个泛型的

 

 

public virtual int ExecuteNonQuery<T>(T conn, string commandText, CommandType commandType)

 

 

我,作为一个超级大懒人,肯定是不会写一大堆重载方法的,所以我选泛型的!

 

哦,先等等,我们好像忘记了什么重要的东西,对,命令的参数,输入和输出参数。

 

 

好,加上参数后,我们的泛型方法就变成了:

 

public virtual int ExecuteNonQuery<T, TU>(T conn, string commandText, TU connParams, CommandType commandType, out IEnumerable<IDataParameter> parameters)

 

 为什么输入参数也要用泛型的呢(输出参数先不用)?

 

因为,我们想在底层为用户多做一些事,比如,让他不仅可以传IEnumerable<IDataParameter> 作为输入参数,还可以传

 

 

    DataRow,很好理解吧,但这不是我们的重点,DataSet,DataTable,DataRow这些弱类型的东西都是不推荐使用的。
    object[], 如果object是IDataParameter类型,直接加上,否则,创建一个输入参数,以object作为值
    object,反射其属性形成IDataParameter[]

 等等,当传一个object作为,我们需要配置文件来定义其属性和SQL命令参数之间的Mapping关系吧。

我们再多走一步,想想,我们的Mapping 关系以什么作为容器呢?在同一个链接字符串下,SQL命名是没有必要重复的吧,因此,我们的Mapping关系以链接字符串为容器。

 

到了这里,我们的泛型方法就变成了这样子:

 

 

public virtual int ExecuteNonQuery<T, TU>(T conn, string commandText, TU connParams, CommandType commandType, out List<IDataParameter> parameters, string conAlians = nullstring cmdAlians = null)
 

 

 我们把后两个参数,写成了带默认值的,是因为,只有conn的类型是String类型 或者 输入参数的类型TU不是DataRow、object[]和IEnumerable<IDataParameter>时,这两个参数才有用。当然,真正原因还是因为我太懒,不愿写太多的重载方法。

 

由于引入了配置文件(先别管这个配置文件是什么样子的),当我们的conn的类型是String类型时,我们传进来的并不是真正的连接字符串,而是给其配置的别名。

 

最后,输入参数和CommandType应该分别有默认值null和CommandType.StoredProcedure,考虑到用户的利益,这次我们不懒了,提供了重载,最后我们的方法是这个样子的:

 

public virtual int ExecuteNonQuery<T, TU>(T conn, string commandText, TU connParams, CommandType commandType, out List<IDataParameter> parameters, string conAlians = nullstring cmdAlians = null)
public virtual int ExecuteNonQuery<T, TU>(T conn, string commandText, TU connParams, out List<IDataParameter> parameters, string conAlians = nullstring cmdAlians = null)
public virtual int ExecuteNonQuery<T, TU>(T conn, string commandText, CommandType commandType, out List<IDataParameter> parameters, string conAlians = nullstring cmdAlians = null)
public virtual int ExecuteNonQuery<T, TU>(T conn, string commandText, out List<IDataParameter> parameters, string conAlians = nullstring cmdAlians = null)

 

分别为

ExecuteNonQuery

ExecuteScalar

ExecuteReader

ExecuteDataset

FillDataSet
ExecuteXmlReader

写上这些方法,然后我们就有了,4×6=24个类型的方法。

如果我们不“懒”,在用泛型方法和带默认值参数的时候都写成重载方法,那么3×4×4×24,就应该有1152个重载方法,这么多方法先不是我半个月写不出来,写出来了,用户使用的时候也讨厌。

因此,取舍很重要啊!基础组件的开发者们切记!

 

好了,我们现在有了24个泛型方法,我们应该把它们放在什么地方呢?我们创建了一个抽象基类AdoHelper(前面提到过), 然后我们为每一种数据库Provider都写一个继承于此基类的子类。

根据 配置文件中Provider 的别名来路由到具体的子类实现。

 

再来考虑SQL命令参数的问题,为了提高性能,我们可以把命令的参数缓存起来,特别是存储过程的参数,我们可以从数据库元数据中自动拿到,然后缓存起来,不需要手动给查询命令添加参数了。

 

我们创建了一个ADOHelperParameterCache类(具体参看源代码)专门来缓存SQL命令的参数,然后在AdoHelper中添加了以下这些方法:

 

 

 

        #region 自动获取参数

        //从缓存中获取参数,如果没有并且命令是存储过程,则调用DiscoverSpParameterSet获取并缓存起来
        public IDataParameter[] GetSpParameterSet(string connectionString, string commandText,
                                                           bool includeReturnValueParameter = false, CommandType commandType = CommandType.StoredProcedure)

        //从缓存中获取参数,如果没有并且命令是存储过程,则调用DiscoverSpParameterSet获取并缓存起来
        public IDataParameter[] GetSpParameterSet(IDbConnection connection, string commandText,
                                                           bool includeReturnValueParameter = false, CommandType commandType = CommandType.StoredProcedure)

        //手动把SQL命令的参数缓存起来
        public void CacheParameterSet(IDbConnection connection, string commandText, IDataParameter[] originalParameters,
                                                   bool includeReturnValueParameter = false)

        //从数据库元数据中获取存储过程的参数,调用DeriveParameters方法
        protected virtual IDataParameter[] DiscoverSpParameterSet(IDbConnection connection, string spName,
                                                        bool includeReturnValueParameter)

        protected abstract void DeriveParameters(IDbCommand cmd);

        #endregion

 

 然后我们还给AdoHelper加上了以下的方法来方便创建SQL参数:

 

        #region 参数相关

        public virtual IDataParameter GetInStringPara(string parameterName, string paraValue)
        {
            return GetParameter(parameterName, DbType.String, paraValue);
        }

        public virtual IDataParameter GetInIntegerPara(string parameterName, int paraValue)
        {
            return GetParameter(parameterName, DbType.Int32, paraValue);
        }

        public virtual IDataParameter GetOutParameter(string parameterName, DbType dbType)
        {
            return GetParameter(parameterName, dbType, ParameterDirection.Output);
        }

        public virtual IDataParameter GetOutParameter(string parameterName, DbType dbType, int size)
        {
            return GetParameter(parameterName, dbType, ParameterDirection.Output, size);
        }

        public virtual IDataParameter GetReturnParameter(string paramName)
        {
            return GetParameter(paramName, DbType.Int32, ParameterDirection.ReturnValue);
        }

        public virtual IDataParameter GetReturnParameter()
        {
            return GetParameter("ReturnValue", DbType.Int32, ParameterDirection.ReturnValue);
        }

        #region 标准加参数方法

        public abstract IDataParameter GetParameter();

        public abstract IDataParameter GetParameter(string parameterName);

        /// <summary>
        
///   根据参数名称和参数值,取得IDataParameter实例
        
/// </summary>
        
/// <param name="name"> 参数名称 </param>
        
/// <param name="value"> 参数值 </param>
        
/// <returns> IDataparameter参数实例 </returns>
        public virtual IDataParameter GetParameter(string name, object value)
        {
            IDataParameter parameter = GetParameter();
            parameter.ParameterName = name;
            parameter.Value = value;
            return parameter;
        }

        public abstract IDataParameter GetParameter(string parameterName, DbType dbType);

        public abstract IDataParameter GetParameter(string parameterName, DbType dbType, object paramValue);

        public abstract IDataParameter GetParameter(string parameterName, DbType dbType,
                                                    ParameterDirection paramDirection);

        public abstract IDataParameter GetParameter(string parameterName, DbType dbType,
                                                    ParameterDirection paramDirection, int size);

        #endregion

        #endregion

 

 考虑到各个数据库Provider的不同,我们还创建了这些属性和方法:

 

 

        private int _commandTimeout = 30;

        public virtual string FieldPrefix
        {
            get { return string.Empty; }
        }

        public virtual string FieldSuffix
        {
            get { return string.Empty; }
        }

        public virtual string ParamPrefix
        {
            get { return string.Empty; }
        }

        public int CommandTimeout
        {
            get { return _commandTimeout; }
            set { _commandTimeout = value; }
        }

        public abstract IDbConnection GetConnection(string connectionString);

        protected abstract IDbDataAdapter GetDataAdapter();

        public abstract bool DrHasRows(IDataReader dataReader);

        /// <summary>
        
///   This method clears (if necessary) the connection, transaction, command type and parameters from the provided command
        
/// </summary>
        
/// <remarks>
        
///   Not implemented here because the behavior of this method differs on each data provider.
        
/// </remarks>
        
/// <param name="command"> The IDbCommand to be cleared </param>
        protected virtual void ClearCommand(IDbCommand command)
        {
        }

 

 为了创建AdoHelper的实例,我们又添加了这些方法:

 

        protected static IDictionary<string, AdoHelper> ADOCache = new Dictionary<string, AdoHelper>();

        // NOTE: 不建议直接使用因为不是从缓存获取
        public static AdoHelper CreateHelper(string providerAssembly, string providerType)
        {
            object instance = Assembly.Load(providerAssembly).CreateInstance(providerType);
            if (instance is AdoHelper)
                return instance as AdoHelper;
            throw new Exception("The provider specified does not extends the AdoHelper abstract class.");
        }

        // NOTE: 不建议直接使用因为不是从缓存获取
        public static AdoHelper CreateHelper(DbProvideType type)
        {
            if (type == DbProvideType.Oracle)
                return new Oracle();
            if (type == DbProvideType.SqlServer)
                return new SqlServer();
            if (type == DbProvideType.OleDb)
                return new OleDb();
            throw new NotSupportedException("Not supported Provider" + (type));
        }

        public static AdoHelper CreateHelper(string providerAlias)
        {
            if (ADOCache.ContainsKey(providerAlias))
                return ADOCache[providerAlias];
            if (providerAlias == "OleDb")
                ADOCache.Add(providerAlias, new OleDb());
            if (providerAlias == "Oracle")
                ADOCache.Add(providerAlias, new Oracle());
            if (providerAlias == "SqlServer")
                ADOCache.Add(providerAlias, new SqlServer());
            Dictionary<string, ProviderAlias> config = WebConfigurationManager.GetSection("daabProviders"as Dictionary<string, ProviderAlias>;
            if (config != null)
                ADOCache.Add(providerAlias, CreateHelper(config[providerAlias.ToLower()].AssemblyName, config[providerAlias.ToLower()].TypeName));
            else
                throw new ArgumentException("Invalid Provider Name");
            return ADOCache[providerAlias];
        }

 

 最后,前面说了,我们的输入参数可以接受多种类型,而且还用了配置,因此我们还需要这些方法:

 

        #region utility methods

        // 从配置文件中得到SQL 命令配置信息
        public static CommandInfo GetCommandInfo(string conAlians, string cmdAlians)

        // 调用GetCommandInfo方法得到参数的Mapping信息,然后根据Mapping信息和obj的属性创建参数列表
        public static void AssignParameterValues<T>(IDataParameter[] commandParameters, T obj, string conAlians, string cmdAlians) where T : class

        // 把指定的IDataparameter参数数组附给IDbCommand对象
        protected virtual void AttachParameters(IDbCommand command, IDataParameter[] commandParameters)

        // 用DataRow 给参数列表赋值
        public static void AssignParameterValues(IDataParameter[] commandParameters, DataRow dataRow)

        // 用object[] 给参数列表赋值
        public static void AssignParameterValues(IDataParameter[] commandParameters, object[] parameterValues)

        // 设定IDbCommand 的各个参数
        protected virtual void PrepareCommand(IDbCommand command, IDbConnection connection,
                                      CommandType commandType, string commandText,
                                      IDataParameter[] commandParameters, out bool mustCloseConnection)

        #endregion utility methods

 

在发布这篇之前,让我们里看看神秘的配置文件吧,其实类型IBITAS的,如下:

 

 

 

 

 好了,我们的组件最基本的部分已经搞定了,我们默认提供了AdoHelper的SqlServer,MySql,Oracle,OleDb,Odbc的实现,其实是抄袭了微软企业库里的代码,把他们提供的重载变成了泛型方法,还加入了类似IBITAS那样的配置,后面我将在这个基础上进行三次封装。敬请期待。