六、数据库适配

1.概述

代码生成器需要解决的一个难题就是如何适配多种数据库。上文列出了各类数据库信息的提取,那么这里就是如何来适配不同类型的数据库了。适配数据库、封装数据这其实是ORM框架需要做的事情,所以如果觉得麻烦的可以直接使用现有的ORM框架也行。本文的核心是数据库适配不是ORM,所以不会像ORM框架那样设计的很复杂,也不涉及到对象关系映射。

2.设计思路

在没有用过ORM框架之前相信DBHelper是使用比较多的一种工具类,本文的数据库适配是对DBHelper的一种扩展,也即根据不同的配置来调用不同的DBHelper以满足对于不同数据库的操作需求。如图所示:

image

类图中定义了两个接口IDataProvider和ISchemaProvider。IDataProvider用于规范各个DBHelper,ISchemaProvider用于扩展需要获取数据库信息的DBHelper。DataProviderBase是所有DBHelper的基类。每个独立DBHelper只要继承DataProviderBase即可,如果该DBHelper需要提供数据库信息,那么就需要实现ISchemaProvider接口。

.NET已经内置支持SqlServer(.NET内置对于Oracle的支持已经声明过期了),而其它的诸如MySql,SQLite等需要加载第三方的驱动才能使用,这样会增加适配器对于第三方程序集的依赖,所以设计时需要把依赖第三方程序集的DBHelper做成类似插件的方式。这样既减少了程序集依赖又提高了适配器的可扩展性。文件结构如图:

image

Brilliant.Data是适配器的核心程序集,下面的3个程序集就是剥离开的DBHelper。而SqlServer是.NET内置支持的,所以直接内置在核心程序集中。DBContext类通过工厂方式根据不同配置来创建不同的DBHelper。

3.Provider的具体实现

基类设计的原则就是把各个DBHelper中公共的方法提取出来,把需要子类具体实现的方法抽象出来即可,但是设计的好与坏会影响后续的代码量以及可扩展性。部分设计代码如下:

public abstract class DataProviderBase : IDataProvider
{
    private ConnectionInfoBase _connInfo;

    /// <summary>
    /// 当前连接
    /// </summary>
    protected ConnectionInfoBase ConnInfo
    {
        get { return _connInfo; }
    }

    /// <summary>
    /// 构造器
    /// </summary>
    public DataProviderBase() { }

    /// <summary>
    /// 构造器
    /// </summary>
    /// <param name="connectionString">连接字符串</param>
    public DataProviderBase(ConnectionInfoBase connInfo)
    {
        this._connInfo = connInfo;
    }

    /// <summary>
    /// 变更连接字符串
    /// </summary>
    /// <param name="connectionString">连接字符串</param>
    public void ChangeConnectionString(string connectionString)
    {
        ConnInfo.ConnectionString = connectionString;
    }

    /// <summary>
    /// 检测连接是否可用
    /// </summary>
    /// <param name="connInfo">连接</param>
    /// <returns>true:可用 false不可用</returns>
    public bool CheckConnection(ConnectionInfo connInfo)
    {
        if (String.IsNullOrEmpty(connInfo.ConnectionString))
        {
            return false;
        }
        this._connInfo = connInfo;
        using (DbConnection conn = GetConnection())
        {
            try
            {
                if (conn.State != ConnectionState.Open)
                {
                    conn.Open();
                }
                return true;
            }
            catch
            {
                this._connInfo = null;
                return false;
            }
        }
    }

    /// <summary>
    /// 返回Command对象
    /// </summary>
    private DbCommand GetCommand(DbConnection conn, SQL sql)
    {
        if (conn.State != ConnectionState.Open)
        {
            conn.Open();
        }
        DbCommand cmd = GetCommand();
        cmd.Connection = conn;
        cmd.CommandText = sql.CmdText;
        cmd.CommandType = sql.CmdType;
        if (sql.Parameters != null)
        {
            cmd.Parameters.Clear();
            cmd.Parameters.AddRange(sql.Parameters);
        }
        return cmd;
    }

    /// <summary>
    /// 执行查询指令返回DataSet对象
    /// </summary>
    /// <param name="sql">查询指令</param>
    /// <returns>DataSet对象</returns>
    public DataSet ExecDataSet(SQL sql)
    {
        using (DbConnection conn = GetConnection())
        {
            DbCommand cmd = GetCommand(conn, sql);
            DbDataAdapter da = GetDataAdapter();
            da.SelectCommand = cmd;
            DataSet ds = new DataSet();
            da.Fill(ds);
            cmd.Parameters.Clear();
            return ds;
        }
    }

    /// <summary>
    /// 执行查询指令返回DataReader对象
    /// </summary>
    /// <param name="sql">查询指令</param>
    /// <returns>DataReader对象</returns>
    public IDataReader ExecDataReader(SQL sql)
    {
        DbConnection conn = GetConnection();
        DbCommand cmd = GetCommand(conn, sql);
        IDataReader dataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
        cmd.Parameters.Clear();
        return dataReader;
    }

    /// <summary>
    /// 执行查询指令返回第一行第一列的值
    /// </summary>
    /// <param name="sql">查询指令</param>
    /// <returns>第一行第一列的值</returns>
    public object ExecScalar(SQL sql)
    {
        using (DbConnection conn = GetConnection())
        {
            DbCommand cmd = GetCommand(conn, sql);
            object result = cmd.ExecuteScalar();
            cmd.Parameters.Clear();
            //新增对于DBNull的判定,将DBnull转换为null以供逻辑判断(2014-09-05)
            return result == DBNull.Value ? null : result;
        }
    }

    /// <summary>
    /// 执行查询指令返回受影响行数
    /// </summary>
    /// <param name="sql">查询指令</param>
    /// <returns>受影响行数</returns>
    public int ExecNonQuerry(SQL sql)
    {
        using (DbConnection conn = GetConnection())
        {
            DbCommand cmd = GetCommand(conn, sql);
            int result = cmd.ExecuteNonQuery();
            cmd.Parameters.Clear();
            return result;
        }
    }

    /// <summary>
    /// 返回一个新的Connection实例
    /// </summary>
    /// <returns>Connection实例</returns>
    protected abstract DbConnection GetConnection();

    /// <summary>
    /// 返回一个新的Command实例
    /// </summary>
    /// <returns>Command实例</returns>
    protected abstract DbCommand GetCommand();

    /// <summary>
    /// 返回一个新的DataAdapter实例
    /// </summary>
    /// <returns>DataAdapter实例</returns>
    protected abstract DbDataAdapter GetDataAdapter();
}

都是一些比较常用的方法,如获取数据集合以及执行SQL语句。当然Sql语句并没有直接使用字符串而是使用了SQL类来封装的。此外预留了3个抽象方法用于在子类中具体实现。如果子类不实现ISchemaProvider,那么子类只要重写上述3个方法即可。以SqlServer为例实现代码如下:

public class SqlServer : DataProviderBase, ISchemaProvider
{
    /// <summary>
    /// 构造器
    /// </summary>
    public SqlServer() { }

    /// <summary>
    /// 返回一个新的Connection实例
    /// </summary>
    /// <returns>Connection实例</returns>
    protected override DbConnection GetConnection()
    {
        return new SqlConnection(ConnInfo.ConnectionString);
    }

    /// <summary>
    /// 返回一个新的Command实例
    /// </summary>
    /// <returns>Command实例</returns>
    protected override DbCommand GetCommand()
    {
        return new SqlCommand();
    }

    /// <summary>
    /// 返回一个新的DataAdapter实例
    /// </summary>
    /// <returns>DataAdapter实例</returns>
    protected override DbDataAdapter GetDataAdapter()
    {
        return new SqlDataAdapter();
    }
}

其余的诸如Oracle,MySql,SQLite的实现方式类似,这里就不贴具体代码了,详细的实现方式请参照源码。ISchemaProvider是用于扩展Provider的功能的。该接口定义一组方法用于获取数据库信息用的。这些信息在上文中已经讲解过,也是做代码生成器必不可少的。为什么这里要单独提取一个接口用来获取数据库信息,而不集成在基类或者IDataProvider接口中呢?如类图所描述的,并不是所有的Provider都能过提供数据库信息的。所以为了考虑那些不能提供数据库信息的Provider只能单独提取出一个接口了。关于Provider实现ISchemaProvider的细节请参照源码的实现。

4.动态创建Provider实例

/// <summary>
/// 创建Provider对象
/// </summary>
/// <param name="providerName">provider名称</param>
/// <returns>Provider对象</returns>
private object CreateProvider(string providerName)
{
    Type type = null;
    if (!String.IsNullOrEmpty(providerName))
    {
        string namePrefix = "Brilliant.Data.Provider.";
        if (!providerName.Contains(namePrefix))
        {
            providerName = namePrefix + providerName;
        }
        string assemblyPath = String.Empty;
        if (providerName != typeof(SqlServer).FullName)
        {
            assemblyPath = String.Format("{0}.dll", providerName);
            if (!File.Exists(assemblyPath))
            {
                assemblyPath = String.Format(@"{0}\bin\{1}.dll", AppDomain.CurrentDomain.BaseDirectory, providerName);
                if (!File.Exists(assemblyPath))
                {
                    throw new Exception(String.Format("目标文件\"{0}\"不存在!", assemblyPath));
                }
            }
            Assembly assembly = Assembly.LoadFrom(assemblyPath);
            type = assembly.GetType(providerName);
        }
        else
        {
            type = Type.GetType(providerName, true);
        }
    }
    else
    {
        type = typeof(SqlServer);
    }
    return Activator.CreateInstance(type);
}

在DBContext中添加上述方法,本案例是根据config配置文件来动态创建DBHelper实例的。

<configuration>
  <connectionStrings>
    <add name="SqlServer" providerName="Brilliant.Data.Provider.SqlServer" connectionString="Data Source=192.168.1.101;Database=DB_Test;uid=sa;pwd=123"/>
  </connectionStrings>
</configuration>

其中providerName提供了指定Provider类的完整引用路径。上述方法就是依据该路径使用反射在DBContext初始化时动态创建Provider对象。大致原理就是这样,至于实现方式有很多种,依据不同的需求可以有不同的方式。给定的案例源码中有一个完整的实现案例可以参考。该实例中的Provider现了ISchemaProvider接口,同时加入不少后续要使用到的基础方法。稍微比文章中讲解的复杂,但是核心内容基本是一样的。该文章是针对传统DBHelper的扩展提供了一种可行性的思路。一般ORM框架除了对象关系映射之外,多数据库适配也是其必不可少的功能。所以了解多数据库适配也是后续了解ORM框架所不可或缺的。

为了节约空间和上传方便,使用了7z的格式。如果有不能解压或其它问题的请留言。

案例源码

posted @ 2015-07-19 17:49  最终的阿瓦隆  阅读(2260)  评论(0编辑  收藏  举报