一、前言
相信许多人都百度过:“.net 数据库访问类”。然后就出来一大堆SqlHelper。我也用过这些SqlHelper,也自己写过,一堆静态方法,开始使用起来感觉很不错,它们也确实在很多时候可以很好的工作。ADO.NET已经封装很好了,我们很容易就可以实现自己的数据库访问类。
很久前,忘记在哪里看到过了,有一个朋友写了一篇【如何做一个好用的数据库访问类】(有兴趣的朋友仍然可以搜索到),这篇文章确实写得很好,作者很详细的讲解了如何设计一个好的数据库访问类;所谓“好“是指:轻量、易用、通用、高效。
其实代码是很久前就实现了,只是现在才总结记录,希望可以分享一下学习的过程。ok,在开始前先来看几个ADO.NET常见的面试题:
1. ADO.NET 5个核心对象是哪5个?
2. 与ADO.NET 相关对象中,哪些可以用于数据绑定?
3. DataSet与DataReader有什么区别?分别适用在什么情况?
二、需求
这是一个简单的、基于ADO.NET的数据库访问类,它最起码要具备以下特点:
1. 支持多种数据库
搞.net的视乎有一个固定的思维:数据库就是用sql server。额,只是很多用sql server,但不是全部,也有很多用 my sql 等的。我们并不能限制一定用什么数据库。
2. 支持多个数据库
有时候我们的应用程序会用到多个数据库,并且这些数据库还不是部署在同一台服务器上的。
3. 简单
满足常见的操作。
4. 可扩展
可以随时增加新的方法;对于具体的数据源,也可以有特有的操作。
三、主要说明
3.1 使用DbProviderFactory
既然要支持多种数据库,那么我们之前常写的SqlConnection、SqlCommand 就都不能用了,因为它们是针对sql server 数据源的。如果换成 my sql 就是 MySqlConnection, Oracle 就是 OracleConnection 了。
既然有那么多种Connection,很多朋友可能会想到通过设计模式来处理,例如定义一个父类(或接口),然后各种类型的数据库继承它,然后再通过一个工厂,来创建所需要的对象;以后要增加哪种类型的数据库就再增加一个对应的类就可以了。大概是像下面这样:
public abstract class DBHelper { public abstract void Open(string key){} public abstract int ExecuteNonQuery() { } public abstract object ExecuteScalar() { } public abstract DataSet GetDataSet() { } } public class SqlHelper : DBHelper { const char _prefix = ‘@‘; //实现抽象方法... } public class MySqlHelper : DBHelper { const char _prefix = "@"; //实现抽象方法... } public class OracleHelper : DBHelper { const char _prefix = ":"; //实现抽象方法... } public class DBFactory { public static DBHelper GetDBHelper() { //根据条件返回DBHelper } }
这样实现已经比用SqlXXX好很多了,这也是我之前写过的一种方式。但它仍然不够灵活,并且实现起来就会发现很多代码都是类似的,这就与我们上面的简单的需求相违背了。
通过上面的分析,我们知道用工厂模式可以解决我们的问题,但这不用我们自己实现,.net 早就提供这样的工厂:DbProviderFactory。由名称可以指定DbProviderFactory就是数据源提供程序工厂,负责创建具体的数据源提供程序。它根据 ProviderName就可以创建对应数据源的访问对象了。这样我们的实现也由具体变成抽象了,具体的SqlConection变成了抽象的DbConnection。
什么是 ProviderName? 在配置 web.config 的connectionStrings 时,就会有一个 providerNmae 属性,例如sql server就是 ”System.Data.SqlClient“,这个名称空间就是对应的数据源提供程序。
3.2 参数问题
不同数据库参数查询的格式可能不一样,例如 sql server/my sql 支持“@变量” 形式,而 oracle 支持“:变量”的形式。像上面的父类的写法,子类就必须定义自己的参数前缀。但这些用了DbProviderFactory后也不是问题了。
3.3 using 问题
我们都知道using是c#的语法糖,其实编译后就是 try-catch-finaly;uisng写起来比较优雅,而且在有异常的时候会自动调用对象的Disponse方法,避免有些人忘记调用。所以嵌套的 using,编译后就是嵌套的try-catch-finaly,但其实只要我们注意在抛异常的时候释放资源,一个try-catch-finaly即可。
3.4 DbDataReader 问题
实际项目中,我们更多的是使用DbDataReader而非DataSet/DataTable,而 DbDataReader需要自己逐行读取,这在每个调用的地方都这样写是很麻烦的,怎么解决?委托,又是它!
说到委托还有一个小小的建议,有些人喜欢自己去定义委托,但其实.net已经内置了3种委托:Func、Action、Predicate,并且提供了多个重载版本,应该优先考虑使用这些委托,在不满足的情况下,再去自定义。
3.5 在分层架构里的角色
为 DAL 层提供数据访问服务,由 DAL 直接调用;不涉及sql语句拼接、日志记录等。
四、例子
假设要调用一个 P_GetFriends存储过程,接收一个id参数,返回一个好友列表。如下:
public List<Friend> GetFriends(int id) { try { DBHelper helper = new DBHelper("dbConnectionKey"); DbParameter[] parameters = new DbParameter[] { helper.CreateDbParameter("id",id) }; return helper.ExecuteReader(CommandType.StoredProcedure, "P_GetFriends", parameters, reader => { return new Friend() { ID = reader.GetInt32(reader.GetOrdinal("ID")), Name = reader.GetString(reader.GetOrdinal("Name")) }; }); } catch { throw; } }
附源代码
public class DBHelper { #region 属性 /// <summary> /// 链接字符串 /// </summary> private string conStr; /// <summary> /// DB工厂 /// </summary> private DbProviderFactory provider; #endregion #region 构造函数 /// <summary> /// 构造函数 /// </summary> /// <param name="key">链接字符串键</param> public DBHelper(string key) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException("key"); } ConnectionStringSettings css = WebConfigurationManager.ConnectionStrings[key]; if (css == null) { throw new InvalidOperationException("未找到指定的链接字符串!"); } this.conStr = css.ConnectionString; this.provider = DbProviderFactories.GetFactory(css.ProviderName); } /// <summary> /// 构造函数 /// </summary> /// <param name="conStr">链接字符串</param> /// <param name="providerStr">数据源提供程序</param> public DBHelper(string conStr, string providerStr) { if (string.IsNullOrEmpty(conStr)) { throw new ArgumentNullException("conStr"); } if (string.IsNullOrEmpty(providerStr)) { throw new ArgumentNullException("providerStr"); } this.provider = DbProviderFactories.GetFactory(providerStr); this.conStr = conStr; } #endregion #region 外部方法 /// <summary> /// 执行命令,返回受影响行数 /// </summary> /// <param name="commandType">命令类型</param> /// <param name="sql">sql语句或存储过程名称</param> /// <param name="parameters">参数数组</param> /// <returns>受影响行数,失败返回-1</returns> public virtual int ExecuteNonQuery(CommandType commandType, string sqlOrProcName, IEnumerable<DbParameter> parameters) { DbConnection con = CreateConnection(); DbCommand cmd = CreateCommand(con, commandType, sqlOrProcName, parameters); try { con.Open(); int row = cmd.ExecuteNonQuery(); return row; } catch { throw; } finally { cmd.Dispose(); con.Close(); } } /// <summary> /// 执行命令,返回第一行第一列对象 /// </summary> /// <param name="commandType">命令类型</param> /// <param name="sql">sql语句或存储过程名称</param> /// <param name="parameters">参数数组</param> /// <returns>执行结果</returns> public virtual object ExecuteScalar(CommandType commandType, string sqlOrProcName, IEnumerable<DbParameter> parameters) { DbConnection con = CreateConnection(); DbCommand cmd = CreateCommand(con, commandType, sqlOrProcName, parameters); try { con.Open(); object obj = cmd.ExecuteScalar(); return obj; } catch { throw; } finally { cmd.Dispose(); con.Close(); } } /// <summary> /// 执行命令返回DataSet /// </summary> /// <param name="commandType">命令类型</param> /// <param name="sql">sql语句或存储过程名称</param> /// <param name="parameters">参数数组</param> /// <returns>DataSet</returns> public virtual DataSet GetDataSet(CommandType commandType, string sqlOrProcName, IEnumerable<DbParameter> parameters) { DbConnection con = CreateConnection(); DbCommand cmd = CreateCommand(con, commandType, sqlOrProcName, parameters); DataSet set = new DataSet(); DbDataAdapter adapter = this.provider.CreateDataAdapter(); try { con.Open(); adapter.SelectCommand = cmd; adapter.Fill(set); return set; } catch { throw; } finally { adapter.Dispose(); cmd.Dispose(); con.Close(); } } /// <summary> /// 执行命令返回DbDataReader /// </summary> /// <param name="commandType">命令类型</param> /// <param name="sql">sql语句或存储过程名称</param> /// <param name="parameters">参数数组</param> /// <param name="action">委托</param> /// <returns>对象列表</returns> public virtual List<T> ExecuteReader<T>(CommandType commandType, string sqlOrProcName, IEnumerable<DbParameter> parameters, Func<DbDataReader, T> action) { DbConnection con = CreateConnection(); DbCommand cmd = CreateCommand(con, commandType, sqlOrProcName, parameters); List<T> result = new List<T>(); try { con.Open(); DbDataReader reader = cmd.ExecuteReader(); try { while (reader.Read()) { var item = action(reader); result.Add(item); } return result; } catch { throw; } finally { reader.Dispose(); } } catch { throw; } finally { cmd.Dispose(); con.Close(); } } /// <summary> /// 批量执行sql语句 /// </summary> /// <param name="sqlList">sql语句集合</param> /// <param name="paramList">参数数组集合</param> /// <returns>执行成功或失败</returns> public virtual bool ExecuteSqlBatchByTrans(IEnumerable<string> sqlList, IEnumerable<List<DbParameter>> paramList) { DbConnection con = CreateConnection(); DbCommand cmd = CreateCommand(con, CommandType.Text); try { con.Open(); DbTransaction trans = con.BeginTransaction(); cmd.Transaction = trans; try { int length = sqlList.Count(); IEnumerable<DbParameter> parameters = null; for (int i = 0; i < length; i++) { cmd.CommandText = sqlList.ElementAt<string>(i); cmd.Parameters.Clear(); parameters = paramList.ElementAt<List<DbParameter>>(i); foreach (DbParameter pm in parameters) { cmd.Parameters.Add(pm); } cmd.ExecuteNonQuery(); } trans.Commit(); return true; } catch { trans.Rollback(); throw; } finally { trans.Dispose(); } } catch { throw; } finally { cmd.Dispose(); con.Close(); } } #endregion #region CreateDbParameter public DbParameter CreateDbParameter(string name, object value) { DbParameter parameter = this.provider.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; return parameter; } public DbParameter CreateDbParameter(string name, object value, ParameterDirection direction) { DbParameter parameter = this.provider.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; parameter.Direction = direction; return parameter; } public DbParameter CreateDbParameter(string name, object value, int size) { DbParameter parameter = this.provider.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; parameter.Size = size; return parameter; } public DbParameter CreateDbParameter(string name, object value, int size, DbType type) { DbParameter parameter = this.provider.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; parameter.Size = size; parameter.DbType = type; return parameter; } public DbParameter CreateDbParameter(string name, object value, int size, DbType type, ParameterDirection direction) { DbParameter parameter = this.provider.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; parameter.Size = size; parameter.DbType = type; parameter.Direction = direction; return parameter; } #endregion #region 私有方法 /// <summary> /// 获取链接实例 /// </summary> /// <returns>链接实例</returns> private DbConnection CreateConnection() { DbConnection con = this.provider.CreateConnection(); con.ConnectionString = this.conStr; return con; } /// <summary> /// 获取命令实例 /// </summary> /// <param name="con">链接实例</param> /// <param name="commandType">命令类型</param> /// <param name="sqlOrProcName">sql语句或存储过程名称</param> /// <returns>命令实例</returns> private DbCommand CreateCommand(DbConnection con, CommandType commandType, string sqlOrProcName, IEnumerable<DbParameter> parameters) { DbCommand cmd = InitCommand(con, commandType, parameters); cmd.CommandText = sqlOrProcName; return cmd; } /// <summary> /// 获取命令实例 /// </summary> /// <param name="con">链接实例</param> /// <param name="commandType">命令类型</param> /// <returns>命令实例</returns> private DbCommand CreateCommand(DbConnection con, CommandType commandType) { return InitCommand(con, commandType, null); } /// <summary> /// 初始化命令 /// </summary> /// <param name="commandType">命令类型</param> /// <param name="parameters">参数集合</param> /// <returns></returns> private DbCommand InitCommand(DbConnection con, CommandType commandType, IEnumerable<DbParameter> parameters) { DbCommand cmd = con.CreateCommand(); cmd.CommandType = commandType; if (parameters != null) { foreach (DbParameter pm in parameters) { cmd.Parameters.Add(pm); } } return cmd; } #endregion }