打造自己的数据访问层
http://database.51cto.com/art/201105/260494.htm
项目开发中,大多数开发人员的核心工作就是如何对数据进行存储及访问。.NET中,ADO.NET可以使用DbConnection进行连接,DataSet进行数据存储,DataAdapter进行数据更新。
项目开发中,大多数开发人员的核心工作就是如何对数据进行存储及访问。为了进行数据操作,我们首先得解决几个基本问题:
1、如何与一个数据库建立连接。
2、如何从数据库读取相应的数据。
3、如何对数据表进行增改操作。
.NET中,ADO.NET很方便的解决了上面三个问题,我们可以使用DbConnection进行连接,DataSet进行数据存储,DataAdapter进行数据更新。先看一段代码:
- //创建DbConnection对象连接数据库
- SqlConnection conn = new SqlConnection();
- conn.ConnectionString = "server=.;uid=sa;password=123456; database=DATA_BASE; max pool size=300;";
- //创建DataAdapter、Command对象,读取数据
- SqlDataAdapter da = new SqlDataAdapter();
- SqlCommand cmd = new SqlCommand();
- cmd.Connection = conn;
- cmd.CommandText = "SELECT * FROM TEST";
- da.SelectCommand = cmd;
- //创建DataSet对象,存储数据,建立与物理表的映射
- DataSet ds = new DataSet();
- da.Fill(ds, "TEST");
上述代码实现对数据库“DATA_BASE”中“TEST”表数据读取,并用DataSet时行存储。
既然读出了TEST表中的数据,接下来要解决的就是如何对TEST表进行增、删、改操作。
为实现增、删、改操作,需要为DataAdapter指定InsertCommand、DeleteCommand以及UpdateCommand,并为每个Command对象绑定参数:
- //新增数据
- cmd = new SqlCommand();
- cmd.Connection = conn;
- cmd.CommandText = "INSERT INTO TEST (ID, NAME, VAL) VALUES (@ID, @NAME, @VAL)";
- SqlParameter param = new SqlParameter("@ID", null);
- param.SourceColumn = "ID";
- cmd.Parameters.Add(param);
- param = new SqlParameter("@NAME", null);
- param.SourceColumn = "NAME";
- cmd.Parameters.Add(param);
- param = new SqlParameter("@VAL", null);
- param.SourceColumn = "VAL";
- cmd.Parameters.Add(param);
- da.InsertCommand = cmd;
-
- //修改数据
- cmd = new SqlCommand();
- cmd.Connection = conn;
- cmd.CommandText = "UPDATE TEST SET NAME = @NAME, VAL = @VAL WHERE ID = @ID";
- param = new SqlParameter("@ID", null);
- param.SourceColumn = "ID";
- cmd.Parameters.Add(param);
- param = new SqlParameter("@NAME", null);
- param.SourceColumn = "NAME";
- cmd.Parameters.Add(param);
- param = new SqlParameter("@VAL", null);
- param.SourceColumn = "VAL";
- cmd.Parameters.Add(param);
- da.UpdateCommand = cmd;
-
- //删除数据
- cmd = new SqlCommand();
- cmd.Connection = conn;cmd.CommandText = "DELETE FROM TEST WHERE ID = @ID";
- param = new SqlParameter("@ID", null);
- param.SourceColumn = "ID";
- cmd.Parameters.Add(param);
- da.DeleteCommand = cmd;
完成准备工作后,利用DataTable进行数据操作:
- DataTable dt = ds.Tables["TEST"];
- dt.PrimaryKey = new DataColumn[] { dt.Columns["ID"] };
- dt.Rows.Add(new object[]{
- Guid.NewGuid().ToString(), string.Format("测试:{0}", DateTime.Now), string.Format("测试值:{0}", DateTime.Now)
- });
- DataRow dr = dt.Rows.Find("f8dc2c64-f51a-4e99-bde1-a20069b09c3a");
- if (dr != null){
- dr["NAME"] = string.Format("测试修改:{0}", DateTime.Now);
- }
- dr = dt.Rows.Find("ed7d079b-81ec-4ba4-bf85-688621e495e7");
- if (dr != null){
- dr.Delete();
- }
最后调用DataAdapter的Update方法保存变更后的数据:
da.Update(ds, "TEST");
利用ADO.NET的上述方法,我们已经完成了对数据库的完整操作。
注:上述代码是利用DataAdapter对数据库进行读写的基本原理,非常重要,我们后面的改造都将以此作为依据。
虽然我们已经能完成对数据的操作,但现实际上还是存在很多问题:
1、我们只是对MSSql数据库进行操作,如果要对Oracle或MySql进行操作我们得定义新的Oracle或MySql数据对象,如果能由系统自动判断操作的数据库类型,我们就能省去对数据对象的关注。
2、我们做了太多的准备工作,我们只对一张表进行操作,如查我们要对多张表进行操作,参数又很多,实际上会产生大量的重复代码,我们得考虑消除这些代码。
对于上述问题,如果我们自己实现一个数据访问层,对ADO.NET对象进行封装,只关注实际的数据的操作,而不关注系统是如何与数据库进行连接、如何进行参数传递,那我们的需求就算基本满足了。
我们可以先进行假设,需要达成如下效果,以伪码的形式给出:
- 创建数据执行者:DataExecuter:execObj;
- 创建数据映射对象:DataMapping map;
- 由映射对象填充数据集:map.Fill(sqlText, "TEST", ds);
- 设置映射对象更新命令:map.SetCommands(Insert | Update | Delete, ds);
- DataTable进行实际增、删、改操作。
- 数据执行者执行最后的变更操作:execObj.Update(ds);
后面我将一步步看到上述假设是如何实现的。
当我们通过上面已了解了.NET对数据库操作的基本原理,并就Ado.net对象的使用提出了几点疑问:
1、如何由系统来判断数据库型。
2、如何消除这些重复代码。
而上篇中也提出了一种解决思路,对ADO.NET对象进行封装,具体应该如何实施?
1、需要一个对象,该对象用于建立内存表与物理表的之间映射关系,解决数据查询、更新操作,形成了数据映射对象,定义为DataMapping。
2、每一个映射对象只与一张物理建立映射关系,如果有多个这样的对象同时操作,如何解决?这时就需要另一个对象,用于添加映射对象集合,打包映射对象操作,形成了数据执行者,定义为DataExecutor。
想想看,只需要这两个基本对象,就可以形成简单的数据访问层了。
先实现DataMapping,它应具备如下功能。
1、需要知道物理表的信息,表名、主键集、字段集。
2、需要知道映射的是什么类型的数据库,MSSql数据库、Oracle数据库、MySql数据库或者其他类型的数据库。
3、可查询数据,即填充内存表。
4、可更新数据,并且可设置更新操作方式。
5、可加入到事务中去。
根据上述功能,可初步设计出的DataMapping类:
public
class
DataMapping
{
public
DataMapping()
{ }
///
<summary>
///
填充数据集
///
</summary>
public
void
Fill()
{
}
///
<summary>
///
设置更新命令
///
</summary>
public
void
SetCommands()
{
}
///
<summary>
///
设置数据提交事务
///
</summary>
public
void
SetTransaction()
{
}
///
<summary>
///
提交数据
///
</summary>
public
bool
Update()
{
}
///
<summary>
///
更新列名
///
</summary>
public
string
Columns
{
get
{
return
columns;
}
set
{
columns
=
value;
}
}
private
string
columns
=
""
;
///
<summary>
///
主键名
///
</summary>
public
string
KeyColumns
{
get
{
return
keyColumns;
}
set
{
keyColumns
=
value;
}
}
private
string
keyColumns
=
""
;
///
<summary>
///
表名
///
</summary>
public
string
TableName
{
get
{
return
tableName;
}
set
{
tableName
=
value;
}
}
private
string
tableName
=
""
;
}
再来实现DataExecutor类,它应具备的功能: 1、应该知道执行什么类型的数据库操作。
2、可以添加映射对象。
3、可以进行数据提交。
如何来知道执行的数据库类型,我们可以定义具体的执行者,比如MSSql执行者、Oracle执行者、MySql执行者。
可以初步设计出DataExecutor类
public
abstract
class
DataExecutor
{
private
IList
<
DataMapping
>
lisDataMappings
=
new
List
<
DataMapping
>
();
///
<summary>
///
添加数据映射对象
///
</summary>
public
void
AddDataMapping(DataMapping map)
{
}
///
<summary>
///
更新数据
///
</summary>
public
bool
Update()
{
}
}
public
class
MSSqlExecutor : DataExecutor
{
}
public
class
OracleExecutor : DataExecutor
{
}
public
class
MySqlExecutor : DataExecutor
{
}
准备就绪,开始行具体设计。 先从DataMapping的Fill方法入手,看看它是如何查询数据的。
public
void
Fill(
string
sqlText,
string
tableName, DataSet ds)
{
IDbConnection conn
=
具体的数据连接对象;
IDbDataAdapter dataAdapter
=
具体的数据适配对象;
IDbCommand cmd
=
具体的命令对象;
cmd.Connection
=
conn;
cmd.CommandText
=
sqlText;
dataAdapter.SelectCommand
=
cmd;
((DbDataAdapter)dataAdapter).Fill(ds, tableName);
}
问题出来了,这里出现了具体的对象,如何得到这些对象?
前面我们设计了MSSqlExecutor类,它已经知道具体的数据库类型,所以它也应该知道进行数据操作的具体的对象,DataMapping类是否可以引用该它,从而通过它来获取数据操作对象,因此,可以MSSqlExecutor类及DataMapping类进行修改,使DataMapping对MSSqlExecutor类产生依赖关系;这只是对MSSql数据库进行操作,现要改变数据库对象为Oracle了,DataMapping类应该也需要对OracleExecutor类产生依赖关系。
因此,这里可以设计一个接口,用于获取具体对象:
///
<summary>
///
映射执行接口
///
</summary>
public
interface
IMappingExecute
{
///
<summary>
///
获取连接对象
///
</summary>
IDbConnection GetConn();
///
<summary>
///
获取数据适配器
///
</summary>
IDbDataAdapter GetDataAdapter();
///
<summary>
///
获取命令对象
///
</summary>
IDbCommand GetCommand();
///
<summary>
///
获取命令参数
///
</summary>
IDbDataParameter GetDataParameter(
string
col);
///
<summary>
///
获取命令参数
///
数据库之间的命令参类是不一样的
///
MMSql是“@” + 列名,Oracle是 “:” + 列名,MySql是 “?” + 列名
///
</summary>
string
GetSourceColumn(
string
col);
}
改造后的MSSqlExecutor类为:
public
class
MSSqlExecutor : DataExecutor, IMappingExecute { }
改造后的DataMapping类为:
public
class
DataMapping
{
private
IDbConnection conn
=
null
;
private
IDbDataAdapter dataAdapter
=
null
;
///
<summary>
///
映射执行对象
///
</summary>
public
IMappingExecute ExecuteObject
{
set
{
executeObj
=
value;
conn
=
executeObj.GetConn();
}
}
private
IMappingExecute executeObj;
///
<summary>
///
填充数据集
///
参数:查询语句
///
参数:内存表名
///
</summary>
public
void
Fill(
string
sqlText,
string
tableName, DataSet ds)
{
dataAdapter
=
executeObj.GetDataAdapter();
IDbCommand cmd
=
executeObj.GetCommand();
cmd.Connection
=
conn;
cmd.CommandText
=
sqlText;
dataAdapter.SelectCommand
=
cmd;
((DbDataAdapter)dataAdapter).Fill(ds, tableName);
}
}
到此为止,查询功能算是完成了,接下来该实现更新功能了,从SetCommands入手,以新增操作为例:
///
<summary>
///
设置更新命令
///
</summary>
public
void
SetCommands(DataCommandType commandType, DataSet ds)
{
if
((commandType
&
DataCommandType.Insert)
==
DataCommandType.Insert)
{
CreateInsertCommand(ds);
}
if
((commandType
&
DataCommandType.Update)
==
DataCommandType.Update)
{
CreateUpdateCommand(ds);
}
if
((commandType
&
DataCommandType.Delete)
==
DataCommandType.Delete)
{
CreateDeleteCommand(ds);
}
}
///
<summary>
///
生成新增命令及SQL语句
///
</summary>
private
void
CreateInsertCommand(DataSet ds)
{
IList
<
string
>
lisColumns
=
GetColumns(ds);
StringBuilder sbCol
=
new
StringBuilder();
StringBuilder sbVal
=
new
StringBuilder();
foreach
(
string
col
in
lisColumns)
{
sbCol.AppendFormat(
"
, {0}
"
, col);
sbVal.AppendFormat(
"
, {0}
"
, executeObj.GetSourceColumn(col));
}
sbCol.Remove(
0
,
2
);
sbVal.Remove(
0
,
2
);
string
sqlText
=
string
.Format(
"
INSERT INTO {0} ({1}) VALUES ({2})
"
, tableName, sbCol.ToString(), sbVal.ToString());
IDbCommand cmd
=
executeObj.GetCommand();
cmd.Connection
=
conn;
cmd.CommandText
=
sqlText;
SetCommandParams(cmd, lisColumns);
dataAdapter.InsertCommand
=
cmd;
}
///
<summary>
///
获取列字段集
///
</summary>
private
IList
<
string
>
GetColumns(DataSet ds)
{
IList
<
string
>
lisColumns
=
new
List
<
string
>
();
if
(columns
!=
"
*
"
)
{
string
[] sltCol
=
columns.Split(
'
,
'
);
foreach
(
string
col
in
sltCol)
{
lisColumns.Add(col.Trim());
}
}
else
{
DataTable dt
=
ds.Tables[tableName];
foreach
(DataColumn dc
in
dt.Columns)
{
lisColumns.Add(dc.ColumnName);
}
}
return
lisColumns;
}
更新操作非常简单,就是在上一篇的数据操作原理的基础上动态生成了查询语句及参数绑定,不多做解释。
其中DataCommandType为自定义枚举类型:
///
<summary>
///
数据操作命令类型
///
</summary>
public
enum
DataCommandType
{
///
<summary>
///
新增
///
</summary>
Insert
=
1
,
///
<summary>
///
修改
///
</summary>
Update
=
2
,
///
<summary>
///
删除
///
</summary>
Delete
=
4
}
更新完后进行数据提交:
///
<summary>
///
提交数据
///
</summary>
public
bool
Update(DataSet ds)
{
return
((DbDataAdapter)dataAdapter).Update(ds, tableName)
>
0
;
}
至此,数据更新操作也已经完成,最后再看看数据执行者是如何进行批量提交。
这里产生的第一个问题是,什么时候数据执行者会人将映射对象加入到集合中来,其中一种方法是在DataMapping设置更新的时候自己加入到集合去。
因此, 映射执行接口得多添加一个方法,用于新增映射对象:
///
<summary>
///
添加数据映射对象
///
</summary>
void
AddDataMapping(DataMapping map);
修改SetCommand方法:
///
<summary>
///
设置更新命令
///
</summary>
public
void
SetCommands(DataCommandType commandType, DataSet ds)
{
//
设置更新事件时添加映射对象
executeObj.AddDataMapping(
this
);
}
现在执行者中已经存在了,可以进行最后的数据提交:
///
<summary>
///
更新数据
///
</summary>
public
bool
Update(DataSet ds)
{
using
(conn)
{
if
(conn.State
==
ConnectionState.Closed)
{
conn.Open();
}
IDbTransaction transaction
=
conn.BeginTransaction(IsolationLevel.ReadCommitted);
foreach
(DataMapping map
in
lisDataMappings)
{
map.SetTransaction(transaction);
}
try
{
foreach
(DataMapping map
in
lisDataMappings)
{
map.Update(ds);
}
transaction.Commit();
}
catch
(Exception ex)
{
transaction.Rollback();
throw
new
System.Exception(ex.Message);
}
}
return
true
;
}
//
DataMapping类设置事务
///
<summary>
///
设置数据提交事务
///
</summary>
public
void
SetTransaction(IDbTransaction transaction)
{
if
(dataAdapter.InsertCommand
!=
null
)
{
dataAdapter.InsertCommand.Transaction
=
transaction;
}
if
(dataAdapter.UpdateCommand
!=
null
)
{
dataAdapter.UpdateCommand.Transaction
=
transaction;
}
if
(dataAdapter.DeleteCommand
!=
null
)
{
dataAdapter.DeleteCommand.Transaction
=
transaction;
}
}
到些为止,我们自己的数据访问层功能已基本完成,但是,我们要如何对其进行调用,现在的方式真能方便的让我们进行调用吗?答案是否定的。
暂且至此为止,下一篇我们再进行最后的收尾工作,并最终打造成符合自己需求的数据访问层
我们已具体实现了数据访问层对应的功能,该进行收尾工作了,先来看段代码,试试上一篇实现的功能:
- string sqlText = "SELECT ID, NAME, VAL FROM TEST";
- string columns = "ID, NAME, VAL";
- DataSet ds = new DataSet();
- DataExecutor execObj = new MSSqlExecutor();
- DataMapping map = new DataMapping();
- map.ExecuteObject = execObj;
- map.TableName = "TEST";
- map.KeyColumns = "ID";
- map.Columns = "ID, NAME, VAL";
- DataMapping map = new DataMapping(execObj.GetInstant(), "TEST", "ID", columns);
- map.Fill(sqlText, "TEST");
- map.SetCommands(DataCommandType.Insert | DataCommandType.Update | DataCommandType.Delete, ds);
-
- bool isSuccess = execObj.Update();
果然已经完成了对数据库的读写操作了,至少不用再写大段的参数传递代码,功能都已经实现了,是不是就完成了?
仔细看看上面的代码,实际上还有问题尚未解决,看看这句:
- DataExecutor execObj = new MSSqlExecutor();
竟然在代码里直接实例化一个MSSql的执行对象,这样一开始提出的数据库之间的切换问题根本就没有从本质上解决。
再回过头来看上一篇,有一个方法public IDbConnection GetConn(),用来获取数据连接对像,之前并没有说明其如何实现。
我们知道DBConnection有两个关键信息:
1、与哪种类型的数据库产生连接,这个前面已经解决了。
2、传递与数据库连接的帐号信息、访问库信息的ConnectionString,这个并没有提及。
看看第二点以前是怎么做的:
- public IDbConnection GetConn()
- {
- if (conn != null)
- {
- return conn;
- }
- conn = new SqlConnection();
- conn.ConnectionString = 连接字串;
- return conn;
- }
上面出现了连接字串,这个字串从哪来?
总结下,要完成最终的数据访问输出,还需要解决两个问题:
1、动态进行不同数据库之间的切换。
2、解决数据连接字串的来源问题。
接着就来解决这两问题,先解决第二个问题,有个比较简单的方法,将连接字串写入配置文件中去,数据访问层只需知道它传递过来KEY值:
- <appSettings>
- <add key="ConnStr" value="server=.;uid=sa;password=123456;database=DATA_BASE;max pool size=300"/>
- </appSettings>
第一个问题解决了,只剩下最后一个问题了,如何动态切换不同的数据库,也就是说,在使用的时候不需要自己NEW一个对象,而是通过第三方来创建一个对象,实际上,设计模式里已提出了方案,创建型模式,有兴趣的朋友可以自行研究,我们这里只需要用到简单工厂模式:
- public sealed class ExecutorFactory
- {
- public static DataExecutor Create()
- {
- return Create(DatabaseType.MSSql);
- }
- public static DataExecutor Create(DatabaseType dbType)
- {
- AbstractDataBase dataBase = null;
- Switch(dbType)
- {
- case DatabaseType.MSSql:
- dataBase = new MSSqlDataBase();
- break;
- case DatabaseType.Oracle:
- dataBase = new OracleDataBase();
- break;
- }
- return dataBase.Create();
- }
- }
现在可对这句代码进行替换了:DataExecutor execObj = new MSSqlExecutor();
替换为:DataExecutor execObj = ExecutorFactory.Create();
至此,问题都解决了,切换数据库是只需更改DatabaseType为相应的数库类型。
接下来再考虑下,如果改变数据库类型也不需要变动程序,能不能实现?
还是利用配置文件,只是此时提供的不是类型字串,而是实际的数据执行者程序集信息,再利用.NET的天然优势反射可以实现了。
最终配置文件为:
- <appSettings>
- <add key="ConnStr" value="server=.;uid=sa;password=123456;database=DATA_BASE;max pool size=300"/>
- <add key="DBExecutor" value="FM.DataAccess, FM.DataAccess.MappingExcuter.MSSqlExecutor"></add>
- </appSettings>
改造后的工厂:
- public sealed class ExecutorFactory
- {
- public static DataExecutor Create()
- {
- return Create(null);
- }
- public static DataExecutor Create(string dataBaseTypeKey)
- {
- return Create(dataBaseTypeKey, null);
- }
- public static DataExecutor Create(string dataBaseTypeKey, string connStrKey)
- {
- if (string.IsNullOrEmpty(dataBaseTypeKey))
- {
- dataBaseTypeKey = "DBExecutor";
- }
- string[] sltDataBaseType = ConfigReader.Read(dataBaseTypeKey).Split(',');
- string asselblyName = sltDataBaseType[0];
- string nameSpace = sltDataBaseType[1].Trim();
- Assembly assembly = Assembly.Load(asselblyName);
- DataExecutor execObj = assembly.CreateInstance(nameSpace) as DataExecutor;
- execObj.SetConnectionString(connStrKey);
- return execObj;
- }
- }
到此为止,数据访问层最终完成,当然这里还有很多问题有待解决,但其基本框架已形成了,以此为依据,根据业务变化,实现自己的扩展,不断更新,最终才能真正形成完善的数据访问层。