数据访问框架是ado.net的一个包装,它是orm的一种补充,同vb6版本一样,我们希望达到以下几个目标
数据源抽象
意思是不依赖具体的提供者,像SqlClient或是OracleClient,OleDb,Odbc等
事务抽象
有两层意义,其一是通常我们在最初编程时不考虑事务,事实上,我们建议在dao中是不做事务处理的,事务处理通常在service层,甚至在 Action(SBF的Operation)中进行。这样,在后期,我们就需要将一些不在事务的一个或多个dao方法连接到一个事务中。在.net中,这种目标的实现要我们多遵守一些规则,我们会在下面实现具体原因。
其次,就是能抽象本地事务和com+事务,你的系统应该能在两者之间切换
当然,我们的99% 的努力都放在本地事务上。实现第一个要求是我们的重点。
.net 2.0的TransactionScope
显然TransactionScope能达到我们的需求,但是它在sql server2000上启动的是com+事务,显然,这并不是我们真正想要的结果
简化访问
提供一个类似于microsoft data acess block 之类的封装,但这个封装能支持上面的数据源抽象或事务抽象。
在我的实现中,借鉴了spring (java)版本的设计思想,但总的来说,这里的实现非常简单而有效,两个接口IdataSource,IadoOperations,一个实用类,DataSourceUtils,几个缺省实现:SqlClientDataSource,OledbDataSource,SqlClientTemplate 就是全部。
在具体使用代码中,只使用接口IdataSource,IadoOperations去进行具体的操作,实现类通过ioc注入
数据源抽象
有多种方法可以做到数据源抽象,通常使用工厂方法,这是ado.net 2.0的做法。
DbConnection con = DbProviderFactories.GetFactory("System.Data.SqlClient").CreateConnection();
con.ConnectionString = "server=(local);user id=sa;pwd=;database=mis2";
con.Open();
我不喜欢工厂方法,我更喜欢spring java版本的数据访问方式:通过一个IdataSource接口来获得具体的数据库连接。这种方式像策略模式。
public interface IDataSource
{
int Timeout
{
get;
}
IDbConnection GetConnection();
bool ShouldClose(IDbConnection con);
}
通过实现SqlClientDataSource,OleDbDataSource,OracleDataSource,OdbcDataSource ,我们就可以隔离数据源的差异(实际上是数据库连接的差异)
这里,大家可以看到,我们还增加了ShouldClose,从spring (java)的SmartDataSource抄袭而来的这个特性使我们能够自由的在程序中使用断开或不断开的连接而对程序本身没有影响。
事务抽象
我们知道.net的事务模型是需要传递一个IDbTransaction的实例,这样你的自动事务处理的代码和显式事务处理的代码是不同的:
自动事务处理代码
[code]
//获取连接
IdbCommand cmd=con.CreateCommand();
cmd.CommandText=””;
cmd.ExecuteNonQuery();
//关闭连接
[/code]
显式事务处理的代码
[code]
//获取连接
IdbCommand cmd=con.CreateCommand();
cmd.CommandText=””;
cmd.Transaction=trans; //trans可能是通过函数参数或是其它方式获取
cmd.ExecuteNonQuery();
//关闭连接
[/code]
这非常糟糕,因为通常我们刚开始编码时是不做事务处理的,而且作为一种原则[b]在dao中是没有也是不应该有事务处理的[/b],事务是一种粗粒度的东西,它应该在服务或是服务更上的facade层,按这种方式,可能你一开始必须要按第二种方式编码,否则会在以后遭遇麻烦(当你将多个dao方法组合在一个业务逻辑中时)。
注:在SohoWorks.BusinessFramework.Data.SqlServer命名空间下的SqlServerTransactionManager可以使你不必一定要按第二种方式编码,它对原来的代码影响更小,不过当前,我们仅有sql server的实现,由于我们工作的数据库范围较小,我们现在还没有探讨其它实现的可能性
内幕:以sqlclient为例
在.net 1.1中,sqlconnection实际上是持有这个transaction的(LocalTransaction属性持有对BeginTranaction过程产生的事务对象的一个弱引用,不过LocalTransaction属性是不公开的),因此我们想ms可以在CreateCommand或设置连接时传递这个transaction对象,而不用我们去做。当然,它没有做,接口上也没有约束这点,其它实现也不会这样做。
在.net 2.0,sqlconnection实际上不持有这个事务了,事实上.net 2.0的事务同.net 1.1有很大的差异,在.net 1.1中,Sqltransaction显式的向数据库发出begin transaction ,commit transaction等命令,而在.net中,一切都交由一个内部的类SqlInternalConnection去处理了。
当然,在第二种方法中,trans绝对不应该是通过函数参数来得到的,其实当你启动事务时,你应该将它保存在一个threadstatic的词典中,然后,在这个上下文的应用就可以获取这个transaction,在我们的业务框架中,这是通过DataSourceUtils来实现的
下面是具体使用的代码
[code]
IDataSource dataSource=new SqlClientDataSource("server=(local);user id=sa;pwd=;database=zhengda");
IDbConnection con=DataSourceUtils.GetConnection(dataSource);
IDbCommand cmd=con.CreateCommand();
cmd.Transaction=DataSourceUtils.GetTransaction(dataSource);
IDataParameter p1=cmd.CreateParameter();
p1.ParameterName="@username";p1.Value="admin";
IDataParameter p2=cmd.CreateParameter();
p2.ParameterName="@password";p2.Value="123456";
cmd.CommandText="select username from operators where username=@username and password=@password";
cmd.Parameters.Add(p1);
cmd.Parameters.Add(p2);
IDataReader reader=cmd.ExecuteReader();
while (reader.Read())
{
//做一些事情
}
reader.Close();
DataSourceUtils.CloseConnectionIfNecessary(con,dataSource);
[/code]
注意DataSourceUtils.GetTransaction 可能返回null或是返回一个有效的事务对象,你不必关注这点,事务管理器的实现和DataSourceUtils之间有一种默契。
在我们的业务框架中,一般是通过IAdoOperations的实现类来做数据操作的,因此你不必考虑这点
[code]
IDataSource dataSource=new SqlClientDataSource("server=(local);user id=sa;pwd=;database=zhengda");
IAdoOperations template=new SqlClientTemplate(dataSource);
IDataParameter[] parameters=new IDataParameter[2];
parameters[0]=template.CreateParamter("@username","admin");
parameters[1]=template.CreateParamter("@password","123456");
SafeDataReader reader=template.ExecuteReader("select username from operators where username=@username and password=@password",CommandType.Text,parameters);
while (reader.Read())
{
//做一些事情
}
reader.Close();
DataSourceUtils.CloseConnectionIfNecessary(con,dataSource);
[/code]
这种事务抽象的过程如下
① 当事务管理器开始事务时,将事务对象或连接对象放到一个threadStatic的词典中
② 从词典中获取事务对象,传给command
① 递交事务时,从词典中获取事务对象,并递交
④回退时,从词典中获取事务对象,并回退
⑤ 当事务递交和回退时,从词典中删除事务对象,并执行关闭连接过程
由于现在通常是用断开连接的方式工作,每个过程都会打开或断开连接,因此,实际上要对这个过程进行抽象,如果不这样做,怎么能保证事务在运行在几个过程中(想想看某一个过程在结束后把连接都关闭了,让事务怎么继续进行)
在我们的框架中,DataSourceUtils的getConnection和CloseConnectionIfNecessary就做这些事情
①如果有一个事务存在,则从该事务中获取连接 (GetConnection)
②如果有一个事务存在,则不关闭连接(CloseConnectionIfNecessary)
在ado和jdbc中,事务的执行是通过连接对象来执行的,这样,我们其实只是保存连接的实例,过程同上面类似,在vb6的sbf框架实现中,我就是这样干的。在SqlServerTransactionManager实现中,也是通过保存连接的实例来进行的。
上面的几句话实际上就是spring(java)版本事务抽象的实质,当然,对于spring(java),实际的实现会更复杂。
我在vb6和.net中也实现了这种抽象,当然,目前的状态仅满足自用,它不考虑嵌套事务的实现,我的目标主要是进行跨方法组合事务和抽象本地事务和com+事务的区别。
对于spring.net 的data项目,我觉的非常的糟糕,spring.java版本的数据功能是建立在dataSource接口的基础上的,这个接口抽象了具体数据源的差异,但spring.net data项目一开始就背离了这个路线,目前,还是个半拉子的项目。所有当前模块中,最最差的
SafeDataReader
在上个例子中,大家可以看到我们这里返回的是SafeDataReader,这是数据访问框架的一部分,SafeDataReader 从CSLA中偷过来的概念,它主要解决以下问题:
l 按列名查询值
l 对于DBNull返回原始类型,如int32就返回0
在我们这个框架中,一个内部DataReaderClosed事件被添加,一个内部的属性Connection被添加,这样,当外部的reader.Close一旦被执行,AdoTemplate内部就会调用
DataSourceUtils.CloseConnectionIfNecessary(reader.Connection,dataSource);
这样,同我们整个处理框架保持一致。
当然,IdataReader实际上应该在内部持有创建它的command对象的,当然,也是隐藏着的
事务实现
事务的实现非常简单,首先,我们抽象一个接口ItransactionManager
然后,我们创建一个AdoTransactionManager,大家可以从代码中看到AdoTransactionManager如何同DataSourceUtils的内部方法进行交互,这部分代码可参见
SohoWorks.BusinessFramework.Data.AdoTransactionManager
我们还可以创建一个ComPlusTransactionManager,这个管理器非常简单
这部分代码参见 SohoWorks.BusinessFramework.Data.ComPlusTransactionManager部分
IadoOperations
IadoOperations完成第三个目标,IadoOperations针对不同数据源有不同的实现,这主要是由ado.net设计的另一个缺陷造成的,就是IDataAdapter,不像IdbCommand,只有IdataAdapter的创建依赖于具体的实现,如SqlDataAdapter, IadoOperations定义了常见的操作如
ExecuteNonQuery
ExecuteReader
ExecuteScalar
ExecuteDataSet
UpdateDataSet
CreateDbCommand
CreateParameter
UpdateDataSet是最复杂的方法,有两种实现,1是使用CommandBuilder,二是通过传递insertcommand,updatecommand,deleteComand
使用CommandBuilder
public void TestUpdateDataSet1()
{
IDataSource dataSource=new SqlClientDataSource("server=(local);user id=sa;password=;database=zhengda");
ITransactionManager trans=new AdoTransactionManager(dataSource);
trans.BeginTransaction();
IAdoOperations adoTemplate=new SqlClientTemplate(dataSource);
IDbCommand selectCmd=adoTemplate.CreateDbCommand("select * from operators");
DataSet ds=adoTemplate.ExecuteDataSet(selectCmd);
ds.Tables[0].Rows.Add(new object[]{"jjx","123456","Admins"});
try
{
adoTemplate.UpdateDataSet(ds,selectCmd);
ds.Tables[0].PrimaryKey=new DataColumn[]{ds.Tables[0].Columns["username"]};
DataRow row=ds.Tables[0].Rows.Find("jjx");
if (row!=null)
{
row["username"]="jianxiao";
adoTemplate.UpdateDataSet(ds,selectCmd);
}
if (row!=null)
{
row.Delete();
adoTemplate.UpdateDataSet(ds,selectCmd);
}
trans.CommitTransaction();
}
catch(Exception ex)
{
trans.RollbackTransaction();
throw ex;
}
}
传递InsertCommand,UpdateCommand,DeleteCommand
public void TestUpdateDataSet()
{
IDataSource dataSource=new SqlClientDataSource("server=(local);user id=sa;password=;database=zhengda");
ITransactionManager trans=new AdoTransactionManager(dataSource);
trans.BeginTransaction();
IAdoOperations adoTemplate=new SqlClientTemplate(dataSource);
IDbCommand insertCmd=adoTemplate.CreateDbCommand("insert into operators (username,password,role) values (@username,@password,@role)");
insertCmd.Parameters.Add(adoTemplate.CreateParameter("@username","username",DataRowVersion.Current));
insertCmd.Parameters.Add(adoTemplate.CreateParameter("@password","password",DataRowVersion.Current));
insertCmd.Parameters.Add(adoTemplate.CreateParameter("@role","role",DataRowVersion.Current));
IDbCommand updateCmd=adoTemplate.CreateDbCommand("update operators set username=@username,password=@password,role=@role where username=@oldusername");
updateCmd.Parameters.Add(adoTemplate.CreateParameter("@username","username",DataRowVersion.Current));
updateCmd.Parameters.Add(adoTemplate.CreateParameter("@password","password",DataRowVersion.Current));
updateCmd.Parameters.Add(adoTemplate.CreateParameter("@role","role",DataRowVersion.Current));
updateCmd.Parameters.Add(adoTemplate.CreateParameter("@oldusername","username",DataRowVersion.Original));
IDbCommand deleteCmd=adoTemplate.CreateDbCommand("delete from operators where username=@username");
deleteCmd.Parameters.Add(adoTemplate.CreateParameter("@username","username",DataRowVersion.Original));
DataSet ds=adoTemplate.ExecuteDataSet("select * from operators");
ds.Tables[0].Rows.Add(new object[]{"jjx","123456","Admins"});
try
{
adoTemplate.UpdateDataSet(ds,insertCmd,updateCmd,deleteCmd);
ds.Tables[0].PrimaryKey=new DataColumn[]{ds.Tables[0].Columns["username"]};
DataRow row=ds.Tables[0].Rows.Find("jjx");
if (row!=null)
{
row["username"]="jianxiao";
adoTemplate.UpdateDataSet(ds,insertCmd,updateCmd,deleteCmd);
}
if (row!=null)
{
row.Delete();
adoTemplate.UpdateDataSet(ds,insertCmd,updateCmd,deleteCmd);
}
trans.CommitTransaction();
}
catch(Exception ex)
{
trans.RollbackTransaction();
throw ex;
}
}
大家看到,第二种方式看起来相当可怕,但实际上,通过外部配置,这一切都可以缓解,事实上,我们要求你在利用ioc外部配置所有的数据访问Command。
关于SqlServerTransactionManager
如果仅仅是支持sql server ,其实有更简单的方法,只要调整获取连接和关闭连接部分即可,不必为IDbCommand传递事务对象,这样,更容易移值原来的代码(不必使用我们的IAdoperations实现,但我们的实现仍能同这种方式协同工作)。因此,现在的版本修改了DataSourceUtils的AddTransactionHolder/AddTransactionHolder内部方法为AddResourceHolder/RemoveResourceHolder
SqlServer命名空间下的SqlServerTransactionManager holder活动的连接对象,简单的在这个连接对象中执行begin transaction,commit transaction,rollbacktransaction ,这部分尚未加错误处理,但它能够正常的工作
IDataSource dataSource=new SqlClientDataSource("server=(local);user id=sa;pwd=;database=zhengda");
ITransactionManager trans=new SqlServerTransactionManager(dataSource);
IAdoOperations adoTemplate=new SqlClientTemplate(dataSource);
const string deleteSql = "delete from operators where username='xxx'";
const string countSql = "select count(*) from operators";
const string sql = "insert into operators (username,password,role) values ('xxx','xxx','xxx')";;
adoTemplate.ExecuteNonQuery(deleteSql);
trans.BeginTransaction();
//传统的代码能工作在事务下
IDbConnection con=DataSourceUtils.GetConnection(dataSource);
IDbCommand cmd=con.CreateCommand();
int count=(int)adoTemplate.ExecuteScalar(countSql);
cmd.CommandText=sql;
cmd.ExecuteNonQuery();
Assert.IsTrue((count+1)==(int)adoTemplate.ExecuteScalar(countSql));
cmd.CommandText=deleteSql;
cmd.ExecuteNonQuery();
DataSourceUtils.CloseConnectionIfNecessary(con,dataSource);
// 传统代码结束
adoTemplate.ExecuteNonQuery(sql);
Assert.IsTrue((count+1)==(int)adoTemplate.ExecuteScalar(countSql));
trans.RollbackTransaction();
Assert.IsTrue((count)==(int)adoTemplate.ExecuteScalar(countSql));
trans.BeginTransaction();
adoTemplate.ExecuteNonQuery(sql);
trans.CommitTransaction();
Assert.IsTrue((count+1)==(int)adoTemplate.ExecuteScalar(countSql));
adoTemplate.ExecuteNonQuery(deleteSql);
结合orm
是否能结合orm使用,取决于orm的设计是否开放,我们在以后将尝试整合一些orm |