Richie

Sometimes at night when I look up at the stars, and see the whole sky just laid out there, don't you think I ain't remembering it all. I still got dreams like anybody else, and ever so often, I am thinking about how things might of been. And then, all of a sudden, I'm forty, fifty, sixty years old, you know?

Enterprise Library Data Access Application Block结构

Summary:
如何入门使用Data Access Application Block,可以参考Enterprise Library 3.1中文帮助:数据访问应用程序块。这篇文章侧重在DAAB工作原理、代码结构上的一些注解。

DAAB中抽象类Database提供了针对数据库操作的接口,从它派生出来的各个数据库类使用工厂方式创建。我们可以基于这个机制进行扩展,实现对其它数据库的支持,例如:Ent Lib 2.0 DAAB添加MySql扩展 MySql5.0.27+MySql .Net Connector 5.0.2beta
DAAB希望使用通用接口对不同的数据库进行操作。DAAB的创建机制封装了不同数据库Provider的创建过程;Database类提供的接口是基于ADO.NET进行数据访问操作的一种指导。还有另外一个重要方面,例如NHibernate内部使用Dialect来封装数据库之间的差异(参数标识符、对象引用的开/闭符号、各种数据库函数的封装、语法差异等),这些扩展与Database类以及配置上总是需要一些结合点。
另外,参考写有效率的SQL查询(IV),在这里就有实际遇到Oracle.DataAccess 10.2的问题?,这都是执行SQL时参数类型信息给的不全,而导致数据库索引失效而带来性能问题(还有其它问题的,例如Oracle中的Clob、NClob,如果更新这些字段的时候没有指定参数为Clob、NClob,则存入的包含中文的数据有时会变成乱码,有时又是好的)。cnblogs中也有几篇文章提到参数信息不完全导致执行计划不能被重用的情况优化SQL Server的内存占用之执行缓存,详细的执行计划缓存参考SQL Server 2005 中的批编译、重新编译和计划缓存问题。另外看过MySql.Data.dll、System.Data.OracleClient.dll、Oracle.DataAccess.dll等从DbType枚举向相应数据库枚举,例如MySqlDbType、OracleDbType等转换时,很多时候可能不是你想要的结果,因此我们可能希望定义自己的标准类型,按照自己的需求向各个数据库类型转换。这也要求能够跟当前使用的数据库类型具有一个结合点。

上面这些是使用DAAB比较常见的扩展需求,这要求对DAAB内部处理方式比较熟悉。

OverView:
DAAB整体结构如下图:


结合使用方式来看Database类的主要工作。

ExecuteDataSet, ExecuteNonQuery, ExcecuteReader, ExecuteScalar
//create a default database instance 
Database database = DatabaseFactory.CreateDatabase(); 
DbCommand command = database.GetSqlStringCommand("select * from cms_box"); 
DataSet ds = database.ExecuteDataSet(command); 
string sql = "select * from cms_box where box_name like " + database.BuildParameterName("boxName"); 
command = database.GetSqlStringCommand(sql); 
database.AddParameter(command, "boxName", DbType.AnsiString, 30, ParameterDirection.Input 
    , 
true, 0, 0, "", DataRowVersion.Default, "%Box002%"); 
ds = database.ExecuteDataSet(command);

都有多个重载版本,主要可以分为2类,以参数中是否包含IDbTransaction划分。对于不使用Transaction参数的版本,DAAB每次新建一个Connection用于查询,自动Open和Close Connection对象;使用Transaction参数的版本,每次从Transaction中获取连接对象,而不是新建。

Database的内嵌类ConnectionWrapper,主要目的是对TransactionScope的支持。如果有使用TransactionScope,DAAB以配置文件(后面讲述)中每个Database的connection string作为key值,将当前TransactionScope中用到的所有Connection对象缓存起来。这样同一个TransactionScope中执行的每个操作,如果是同一个Database(指connection string相同,就算不是同一个实例),都使用一个Connection对象来执行;不同的Database使用不同的Connection对象。在执行TransactionScope.Complete()的时候DAAB会自动关闭释放当前TransactionScope的所有连接对象。
这个主要是Database内嵌类ConnectionWrapper和TransactionScopeConnections完成。

Stored Procedure
Database db = DatabaseFactory.CreateDatabase(); 
DataSet ds = db.ExecuteDataSet("SP_QueryUser"
    , 
new object[] { "%Kevin%", Convert.ToDateTime("2007-10-20") });
DbCommand cmd = db.GetStoredProcCommand("SP_QueryUser"
    , 
new object[] { "%Kevin%", Convert.ToDateTime("2007-10-20") }); 
ds = db.ExecuteDataSet(cmd);

执行存储过程时不需要提供参数列表(包括指定参数类型、长度等),DAAB使用ADO.NET中各种数据库Provider的xxxCommandBuilder.DeriveParameters()方法获取存储过程参数信息。DAAB内部在每个存储过程第一次使用时建一个新的连接调用DeriveParameters()获取参数,并将参数缓存起来,以后调用时从缓存中取参数信息。
完成这个处理的类是ParameterCache、CachingMechanism以及Database,各个派生自Database的具体类必须实现DeriveParameters方法。

DataSet Update
Database db = DatabaseFactory.CreateDatabase(); 
DataSet ds = db.ExecuteDataSet(CommandType.Text, "select * from cms_box"); 

foreach
 (DataRow row in ds.Tables[0].Rows) 

    row["SKIN_CODE"] = "BlueSky"; 
    
//other code that changes the dataset 

//the following code will cause a row update exception 
//because the text length is greater than the length of the database field 
ds.Tables[0].Rows[1]["SKIN_CODE"] = "text more than 10 chars"; 
update command DbCommand updateCmd = db.GetSqlStringCommand( 
    
string.Format("update cms_box set SKIN_CODE={0} where BOX_ID={1}" 
        , db.BuildParameterName("skinCode")
        , db.BuildParameterName("boxId"))); 
db.AddParameter(updateCmd, "skinCode", DbType.AnsiString, ParameterDirection.Input
    , "SKIN_CODE", DataRowVersion.Current, 
null); 
db.AddParameter(updateCmd, "boxId", DbType.Int32, ParameterDirection.Input
    , "BOX_ID", DataRowVersion.Current, 
null); 
//create other commands if needed 
db.UpdateDataSet(ds, ds.Tables[0].TableName, null, updateCmd, null, UpdateBehavior.Continue);

获取一个DataSet,对它编辑,然后可以调用Database.UpdateDataSet()批量更新。olumn和DataRowVersion属性。
UpdateDataSet方法可以指定批量大小,即每次提交的SQL中包含多少个更新语句。DAAB使用ADO.NET的DataAdapter进行批量更新,需要确认数据库Provider的DataAdapter实现了UpdateBatchSize方式,例如MySql.Data.dll 5.0.7版本就没有实现。DAAB没有为具体子类提供影响抽象基类Database UpdateDataSet()处理行为的机会,所以如果在SQL Server数据库上使用DataSet Update并使用BatchSize,这样的代码无法平滑过渡到MySql等不支持BatchSize的数据库。
UpdateBehavior枚举用来控制批量更新过程中,个别更新异常时的行为,有三种方式:
1. Standard。发生异常将终止更新过程,对于数据库使用隐式事务的情况,结果是部分更新部分未更新。
2. Continue。某个记录更新异常时继续更新随后的纪录。要支持这种方式,继承自Database的具体类必须重写SetUpRowUpdatedEvent()方法,在该方法中设置更新记录异常处理事件。
3. Transactional。对批量更新过程使用事务控制,要么全部成功,要么全部失败。如果指定了这种方式,使用的是不带DbTransaction参数的UpdateDataSet()方法,则DAAB在内部开始一个事物,用来完成这次批量更新操作。

Configuration
下面的配置示例都是写在应用程序配置文件中的,例如web.config或app.config。

1. DAAB已经提供了SqlDatabase、OracleDatabase、GenericDatabase,使用SqlDatabase、OracleDatabase的配置文件如下:
<configSections>
  
<section name="dataConfiguration" 
           type
="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data" />
  
<!--, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a-->
</configSections>
<dataConfiguration defaultDatabase="localServer"></dataConfiguration>
<connectionStrings>
  
<add name="localServer" connectionString="..." providerName="..." />
</connectionStrings>
其中,connectionStrings节点下的providerName为System.Data.SqlClient或者System.Data.OracleClient。
connectionStrings节点下面每一个add节点代表一个数据库配置,DatabaseFactory.CreateDatabase(string name)的name参数就是这个节点的name属性。不使用参数创建Database将使用dataConfiguration节点 defaultDatabase属性指定的默认数据库。

2. 假如从Database继承,派生了自己的Database类,例如Ent Lib 2.0 DAAB添加MySql扩展 MySql5.0.27+MySql .Net Connector 5.0.2beta中的MySqlDatabase,如下配置:
<configSections>
  
<section name="dataConfiguration"
           type
="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data" />
  
<!--, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a-->
</configSections>
<dataConfiguration defaultDatabase="localServer">
  
<providerMappings>
    
<add name="MySqlDatabase" databaseType="TS.DAL.MySqlDatabase, TS.DAL" />
  
</providerMappings>
</dataConfiguration>
<connectionStrings>
  
<add name="localServer" connectionString="..." providerName="MySqlDatabase" />
</connectionStrings>
connectionStrings节点中的previderName指向providerMappings中provider的配置。

3. 其它情况下,将使用GenericDatabase这个类,例如你可以使用OleDb、ODBC驱动。 这种情况配置跟1完全一样,可以选择的providerName可以从machine.config的DbProviderFactories中找到,使用invariant属性值,例如System.Data.OleDb、System.Data.Odbc等。

如果数据库的Provider并没有注册到machine.config的DbProviderFactories节点中,则可以使用下面的方式在应用程序配置文件中指定。假设使用MySql的Provider,没有安装MySQL Connector for Net(不会在machine.config的DbProviderFactories中注册),而只是把MySql.Data.dll文件拷贝到应用程序运行目录(并且假设没有从Database派生自己的MySqlDatabase类,而直接使用GenericDatabase):
<configSections>
  
<section name="dataConfiguration" 
           type
="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data" />
  
<!--, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a-->
</configSections>
<dataConfiguration defaultDatabase="localServer"></dataConfiguration>
<connectionStrings>
  
<add name="localServer" connectionString="..." providerName="MySql.Data" />
</connectionStrings>

<system.data>
  
<DbProviderFactories>
    
<add name="MySql Data Provider" invariant="MySql.Data" description="MySql Data Provider" 
        type
="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data" />
  
</DbProviderFactories>
</system.data>

使用GenericDatabase时有些功能不能使用,例如调用存储过程时自动获取参数、DataSet Update不能使用Continue方式、参数标识符为空等,因为GenericDatabase中没有实现这些功能。

DAAB通过EnterpriseLibrary Common库使用.Net标准方式读取配置文件信息,默认情况下,DatabaseFactory从应用程序配置文件(web.config或app.config)中读取,我们也可以改变这种方式,自定义一个DatabaseFactory(非常简单),从另外的文件中读取配置信息:
public static Database CreateDatabase()
{
    FileConfigurationSource config = 
new FileConfigurationSource(@"c:"database.config");
    DatabaseProviderFactory provider = 
new DatabaseProviderFactory(config);
    
return provider.CreateDefault();
}

最主要的配置节点为connectionStrings,这是必须的节点,下面每一个add子节点代表一个数据库。这个节点使用System.Configuration.ConfigurationManager读取,因此不需要在configSections节点中额外配置。
名称为dataConfiguration的section包括自定义的Database类(providerMappings节点)以及默认数据库配置,DAAB Configuration目录下的DatabaseSettings负责读取这个section的配置信息,DbProviderMapping类则负责读取自定义的Database类配置信息。
上面配置示例3中的system.data节点是属于.Net框架配置内容,不需要DAAB自己处理,.Net框架将自动从应用程序配置文件和machine.config中获取。

Build Process
创建Database过程使用到了EnterpriseLibrary的Common库和ObjectBuilder组件,是DAAB中最复杂的部分。

首先注意一下要创建的目标对象Database类,创建时都需要提供一个连接字符串和DbProviderFactory实例参数。SqlDatabase需要System.Data.SqlClient.SqlClientFactory实例,OracleDatabase需要System.Data.OracleClient.ClientFactory实例。

接下来,沿着创建处理可以找到驱动整个创建过程的关键点。
DAAB中的DatabaseFactory类是给客户端使用的工厂类,可以指定名称创建Database对象,或者创建默认的。它的工作很简单,使用Ent Lib Common库中的ConfigurationSourceFactory创建一个默认的ConfigurationSource,通过这个ConfigurationSource可以读取到DAAB的配置信息。然后用这个ConfigurationSource创建DatabaseProviderFactory实例,用DatabaseProviderFactory来创建Database对象并返回。
DatabaseProviderFactory什么事情也没做,只是继承了Ent Lib Common的NameTypeFactoryBase
NameTypeFactoryBase只是简单的调用Ent Lib Common的EnterpriseLibraryFactory。
驱动创建过程就是由EnterpriseLibraryFactory和ObjectBuilder一起完成的。

ObjectBuilder采用Chain模式一步步创建目标对象,每一个步骤都是从ObjectBuilder中BuilderStrategy抽象类派生的一个具体创建策略类,创建BuilderStrategy时提供的枚举指示ObjectBuilder在什么时候创建对象,例如PreCreation表示提前创建等。
EnterpriseLibraryFactory在静态构造方法中创建了4个BuilderStrategy,因此当它使用ObjectBuilder创建Database实例时就用下图所示的4个步骤进行:


1. 默认名称处理。
由ConfigurationNameMappingStrategy处理,主要是创建默认数据库时读取默认数据库名称;指定名称创建数据库时,它不做处理。
ConfigurationNameMappingStrategy要求被创建对象的类上面有ConfigurationNameMapper这个attribute,指定一个用于获取默认实例名称的类型(必须实现IConfigurationNameMapper接口)。DAAB中Database类使用[ConfigurationNameMapper(typeof(DatabaseMapper))],指定为DatabaseMapper这个类,所以ConfigurationNameMappingStrategy将创建DatabaseMapper,调用它的MapName来获取默认数据库名称。

DatabaseMapper使用了DAAB的一个类DatabaseConfigurationView,包括后面要讲到的自定义工厂也会使用这个类。
DatabaseConfigurationView为整个DAAB的配置提供一个全局视图,所以DAAB中需要获取配置的地方,都需要使用DatabaseConfigurationView完成,因此在里面可以看到有获取DefaultName、ConnectionString、ProviderMapping等方法。在配置一节中有讲到DatabaseSettings、DbProviderMapping等配置类,这些配置类只是对xml配置文件中相关节点的一种表示,可以把它们看作是数据载体类;而DatabaseConfigurationView可以看作是一个服务类,它对所有配置信息进行封装,并提供一些转换服务,这在后面会详细说明。

2. 单例模式处理。
由SingletonStrategy处理,这是ObjectBuilder采用一定机制,保证创建的目标对象为单例模式,DAAB不需要关心ObjectBuilder是怎样实现的。

3. 自定义工厂创建对象。
由ConfiguredObjectStrategy处理。DAAB创建各个Database实例时不是使用反射方式,而是通过自定义工厂类完成,ConfiguredObjectStrategy就是完成这个工作。
ConfiguredObjectStrategy要求被创建对象的类上面有CustomFactory这个attribute,指定用哪个工厂类来创建目标对象。这个工厂类必须实现ICustomFactory接口,ConfiguredObjectStrategy用反射创建工厂类,调用接口的CreateObject方法创建目标对象。
DAAB中Database类使用[CustomFactory(typeof(DatabaseCustomFactory))],指定为DatabaseCustomFactory这个类。但DAAB不能直接使用DatabaseCustomFactory来创建所有派生的Database实例,否则不能扩展,因此DAAB加入了另外一个机制,即每个派生自Database的类通过attribute告诉DatabaseCustomFactory使用哪个工厂完成创建操作,SqlDatabase通过[DatabaseAssembler(typeof(SqlDatabaseAssembler))]指定为SqlDatabaseAssembler,OracleDatabase指定为OracleDatabaseAssembler,GenericDatabase指定为GenericDatabaseAssembler。这样DatabaseCustomFactory在接口方法CreateObject中,只需要从派生自Database的具体类上获取attribute,创建IDatabaseAssembler实例,并用它创建目标对象。

前面配置文件讲解中有提到,创建DAAB自带的SqlDatabase、OracleDatabase时不需要使用providerMappings节点配置,只需指定providerName为System.Data.SqlClient或者System.Data.OracleClient就可以。System.Data.SqlClient和System.Data.OracleClient其实是machine.config DbProviderFactories中使用的名称,DatabaseCustomFactory在通过DatabaseConfigurationView获取ProviderMappings时,DatabaseConfigurationView先尝试从配置文件中的providerMappings节点读取用户自定义的ProviderMapping;如果没有,则检查是否是System.Data.SqlClient或者System.Data.OracleClient,如果是则使用内部创建的SqlDatabase或OracleDatabase的设置;如果仍然不匹配,则使用GenericDatabase的设置。

4. 附加事件。
由InstrumentationStrategy完成。DAAB支持附加事件,对Connection的Open和异常,以及Command、DataAdapter的执行事件和异常进行监控,用于捕获异常进行日志记录,以及添加性能计数器用于调试分析目的。InstrumentationStrategy为创建好的Database实例附加这些事件。
启用这个特性之后,需要对DAAB进行安装注册性能计数器等,这需要完全的系统权限,因为考虑到有些应用在部署时系统权限并不充分,所以DAAB默认是没有启用这个特性的,这里也不再深入。

posted on 2007-12-03 15:28  riccc  阅读(3361)  评论(2编辑  收藏  举报

导航