1.概述
代码生成器需要解决的一个难题就是如何适配多种数据库。上文列出了各类数据库信息的提取,那么这里就是如何来适配不同类型的数据库了。适配数据库、封装数据这其实是ORM框架需要做的事情,所以如果觉得麻烦的可以直接使用现有的ORM框架也行。本文的核心是数据库适配不是ORM,所以不会像ORM框架那样设计的很复杂,也不涉及到对象关系映射。
2.设计思路
在没有用过ORM框架之前相信DBHelper是使用比较多的一种工具类,本文的数据库适配是对DBHelper的一种扩展,也即根据不同的配置来调用不同的DBHelper以满足对于不同数据库的操作需求。如图所示:
类图中定义了两个接口IDataProvider和ISchemaProvider。IDataProvider用于规范各个DBHelper,ISchemaProvider用于扩展需要获取数据库信息的DBHelper。DataProviderBase是所有DBHelper的基类。每个独立DBHelper只要继承DataProviderBase即可,如果该DBHelper需要提供数据库信息,那么就需要实现ISchemaProvider接口。
.NET已经内置支持SqlServer(.NET内置对于Oracle的支持已经声明过期了),而其它的诸如MySql,SQLite等需要加载第三方的驱动才能使用,这样会增加适配器对于第三方程序集的依赖,所以设计时需要把依赖第三方程序集的DBHelper做成类似插件的方式。这样既减少了程序集依赖又提高了适配器的可扩展性。文件结构如图:
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的格式。如果有不能解压或其它问题的请留言。