在文章重点对AdventureWorks贸易系统的基本概况数据库设计和CLR存储过程进行了讲解本文主要说明该系统的数据访问层实现方法
实现数据访问层
本节将讲解数据访问层的实现该层包括与AdventureWorks数据库通信的所有必要类和方法首先使用Visual Studio 创建新的Visual C#类库项目AdventureWorksTraderDataAccess当这个项目创建后可修改默认类名称为ProductCategoryDB示例说明了ProductCategoryDB类的实现代码
示例实现ProductCategoryDB类
using System;
using SystemData;
using SystemDataCommon;
using SystemDataSqlClient;
using SystemCollectionsGeneric;
using SystemText;
using AdventureWorksTraderEntities;
using MicrosoftPracticesEnterpriseLibraryData;
namespace AdventureWorksTraderDataAccess
{
public class ProductCategoryDB
{
private DataColumnMapping[] mappings = new DataColumnMapping[] { new DataColumnMapping(ProductCategoryID ProductCategoryID) new DataColumnMapping(Name Name) new DataColumnMapping(rowguid Rowguid) new DataColumnMapping(ModifiedDate ModifiedDate) };
public IList<ProductCategory> GetProductCategories()
{
IList<ProductCategory> list = new List<ProductCategory>();
Database db = DatabaseFactoryCreateDatabase();
string storedProcedureName = GetProductCategories;
DbCommand dbCommand = dbGetStoredProcCommand(storedProcedureName);
using (IDataReader reader = dbExecuteReader(dbCommand))
{
while (readerRead())
{
ProductCategory temp = new ProductCategory();
ProductCategory category = (ProductCategory)DataAccessHelperPopulateEntity(temp mappings reader);
listAdd(category);
}
}
return list;
}
}
}
ProductCategoryDB类仅包括GetProductCategories方法该方法返回ProductCategory表中包括的所有类别在内部GetProductCategories()方法利用EntLib数据访问块来执行GetProductCategories存储过程接着将输出转换为由多个ProductCategory对象组成的泛型集合GetProductCategories()方法中的构造块实现了如下功能
使用EntLib数据访问块实现数据访问
使用泛型工具将输出转换为泛型集合
实现ProductCategory项目
使用泛型集合方式传送数据
以下内容详细说明这些构建块
使用数据访问块实现数据访问
ADONET提供了丰富的能够以多种方式获取和显示数据的功能即使使用ADONET提供的灵活性但有时候开发人员仍然会发现自己一遍又一遍的在重复编写相同的代码例如每个数据驱动的应用程序都需要访问数据库那么开发人员则要编写代码来连接数据库打开数据库执行SQL语句或者存储过程在客户应用程序中获取结果关闭数据库开发人员将被迫在所开发的几乎任何应用程序中编写这些重复性代码而这对于核心业务用户没有任何真正的价值在开发的每个应用程序中可能唯一可能不同的代码是SQL语句或者存储过程名称命令参数和连接字符串只要能够参数化这些变量就能够将多数重复性代码抽象到可重用类中那么在多个应用程序中都能够使用这些类微软已经以EntLib数据访问块的形式完成了这件事本实例将使用数据访问块来与数据库交流
什么是数据访问块
正如前文提到的当开发数据库应用程序时数据访问块用于完成开发人员所面对的多数常见任务数据访问块通过提供一组封装的方法能够大大简化访问数据库的多数常见方法每个方法都封装了实现返回数据所需的逻辑同时还能够管理数据库连接另外通过编写能够跨不同数据库类型使用而不必修改代码的数据访问代码数据访问块对ADONET 提供了补充这些类包括了提供给专用数据库实现功能(例如参数处理和游标)的代码
此外数据访问块还为SQL Server和Oracle提供了专用的继承类另外GenericDatabase类还允许通过任何经过配置的ADONET 中的DbProviderFactory对象使用应用程序块开发人员通过添加实现新的包括专用数据库功能的数据库类型或者提供了现有数据库的自定义实现来扩展应用程序块
数据访问块版本较斑斑有了革命性改进其设计重新设计用于使用ADONET 功能开发人员可以从/downloads下载和查找EntLib数据访问块
使用数据访问块的步骤
为了使用数据访问块需要执行以下步骤
在解决方案中添加对MicrosoftPracticesEnterpriseLibraryCommondll和MicrosoftPracticesEnterpriseLibraryDatadll程序集的引用使用添加应用选项导航到<驱动器名称>:\Program Files\Microsoft Enterprise Library January \bin文件夹中可实现添加对这些程序集的引用
在nfignfig或者自定义配置文件中添加必要的配置项为此将以下<configSections>元素添加到<configuration>元素下
<configSections>
<section name=dataConfiguration type=MicrosoftPracticesEnterpriseLibraryDataConfigurationDatabaseSettings MicrosoftPracticesEnterpriseLibraryData />
</configSections>
然后还要将<dataConfiguration>元素直接添加到<configuration>元素下如下所示
<dataConfiguration defaultDatabase=AdventureWorksDB/>
本实例将AdventureWorksDB数据库标记为默认数据库其在<connectionStrings>元素中独立声明
<connectionStrings>
<add name=AdventureWorksDB providerName=SystemDataSqlClient
connectionString=server=localhost; database=AdventureWorks; UID=user; PWD=word; />
</connectionStrings>
在代码中导入数据访问块的核心命名空间MicrosoftPractices EnterpriseLibraryData
在核心命名空间下开始编写类代码
既然读者已经了解了EntLib数据访问块的基础知识那么下面可以学习一下数据访问块中的关键类
Database对象
任何时候使用数据访问块首先必须处理Database类Database类表示数据库以及在数据库中执行命令的方法Database对象公开了以下方法使用这些方法能够对数据库执行CRUD操作
既然读者已经了解了Database类中的方法那么就可以重新考虑数据访问层ProductCategory类的GetProductCategories()方法以便理解集成数据访问块的方法
IList<ProductCategory> list = new List<ProductCategory>();
Database db = DatabaseFactoryCreateDatabase();
string storedProcedureName = GetProductCategories;
DbCommand dbCommand = dbGetStoredProcCommand(storedProcedureName);
using (IDataReader reader = dbExecuteReader(dbCommand))
首先调用DatabseFactory类的CreateDatabase()方法以获取Database对象实例正如名称暗示的那样DatabaseFactory类包括了创建Database对象的工厂方法注意CreateDatabase()是一个重载方法当调用无参数的CreateDatabase()方法时该方法将返回默认数据库为此可在nfig文件中将defaultDatabase属性设置为适当的数据库配置键如下所示
<dataConfiguration defaultDatabase = AdventureWorksDB />
当创建Database对象实例后接着可调用GetStoredCommand()方法以便生成ADONET 的DBCommand对象此后将DbCommand作为参数传递给ExecuteReader()方法并利用该方法执行存储过程
使用泛型程序实现输出转换
当获取了作为输出的SqlDataReader对象后下一步是将输出内容转换为可发送到业务逻辑层的泛型集合
GetProductCategories()方法利用辅助类DataAccessHelper(如示例所示)将SqlDataReader对象内容转换为对象
示例实现DataAccessHelper类
using System;
using SystemData;
using SystemDataSqlClient;
using SystemConfiguration;
using SystemDataCommon;
using SystemReflection;
using SystemCollections;
namespace AdventureWorksTraderDataAccess
{
public class DataAccessHelper
{
static public object PopulateEntity(object entity DataColumnMapping[] mappings IDataReader reader)
{
foreach (DataColumnMapping mapping in mappings)
{
int ordinalPosition = ;
try
{
ordinalPosition = readerGetOrdinal(mappingSourceColumn);
}
catch (IndexOutOfRangeException ex)
{
throw new PropertyColumnMappingException(mappingSourceColumn +
is not a valid SourceColumn ex);
}
object propertyValue = readerGetValue(ordinalPosition);
if (propertyValue != DBNullValue)
{
if (mappingDataSetColumn == ID)
{
Nullable<int> tempValue = (int)propertyValue;
propertyValue = tempValue;
}
object[] param = { propertyValue };
try
{
entityGetType()InvokeMember(mappingDataSetColumn BindingFlagsSetProperty | BindingFlagsInstance | BindingFlagsPublic | BindingFlagsNonPublic | BindingFlagsFlattenHierarchy | BindingFlagsStatic null entity param);
}
catch (Exception e)
{
throw new PropertyColumnMappingException
(GetPropertyColumnMappingExceptionMessage(mapping) e);
}
}
}
return entity;
}
private static string GetPropertyColumnMappingExceptionMessage
(DataColumnMapping mapping)
{
return Could not populate + mappingDataSetColumn +
property from the + mappingSourceColumn + database column;
}
}
}
在GetProductCategories()方法内部首先声明了一个DataColumnMapping数组该数组中包括ProductCategory表列与ProductCategory对象属性之间的所有映射信息
private DataColumnMapping[] mappings = new DataColumnMapping[] {
new DataColumnMapping(ProductCategoryID ProductCategoryID)
new DataColumnMapping(Name Name)
new DataColumnMapping(rowguid Rowguid)
new DataColumnMapping(ModifiedDate ModifiedDate)};
当调用PopulateEntity()时需要提供ProductCategory对象实例DataColumnMapping数组以及包括执行存储过程所得输出的SqlDataReader对象
ProductCategory category = (ProductCategory)DataAccessHelperPopulateEntity(temp mappings reader);
PopulateEntity()方法将SqlDataReader对象的每一行转换为具有适当属性值的ProductCategory对象
实现ProductCategory对象
除了数据访问和业务逻辑层组件之外本实例还包括另一个名为AdventureWorksTraderEntities的项目该项目公开了一组直接对应数据库中实体的对象ProductCategory表对应的是ProductCategory类(如示例所示)该类可作为容器存储与ProductCategory有关的数据
示例实现ProductCategory类
using System;
using SystemCollectionsGeneric;
using SystemText;
namespace AdventureWorksTraderEntities
{
[Serializable]
public class ProductCategory
{
private int _productCategoryID;
private string _name;
private Guid _rowguid;
private DateTime _modifiedDate;
public int ProductCategoryID
{
get { return _productCategoryID; }
set { _productCategoryID = value; }
}
public string Name
{
get { return _name; }
set { _name = value; }
}
public Guid Rowguid
{
get { return _rowguid; }
set { _rowguid = value; }
}
public DateTime ModifiedDate
{
get { return _modifiedDate; }
set { _modifiedDate = value; }
}
}
}
正如看到的ProductCategory类如同一个占位符每个属性都直接对应ProductCategory表中的一个列
使用泛型集合
读者应该记得GetProductCategories()方法的返回值该值是ProductCategory对象的泛型集合以下代码段专用于泛型集合
while (readerRead())
{
ProductCategory temp = new ProductCategory();
ProductCategory category = (ProductCategory)
DataAccessHelperPopulateEntity(temp mappings reader);
listAdd(category);
}
在以上代码中首先循环整个SqlDataReader对象然后将SqlDataReader对象的每一行都转换为一个ProductCategory对象最后将ProductCategory对象添加到泛型集合中
使用泛型可使得软件组件中的代码更加具有可重用性泛型是一种数据结构类型其中包含的代码保持不变参数的数据类型能够在使用时变化根据所提供的变量使用数据结构能够适应不同的数据类型每次使用泛型时可自定义不同的数据类型而无需重写任何内部代码泛型允许类结构接口委托和方法将它们所存储和操作的数据类型实现参数化
在ASPNET页面中GetProductCategories()返回的泛型集合直接绑定到ObjectDataSource控件当随后讲解本实例Web站点实现时将看到这些内容
实现ProductSubcategoryDB类
ProductSubcategoryDB类包括GetProductSubcategories()方法该方法根据产品类别返回所属所有子类别的列表这个方法的实现非常类似于ProductCategoryGetProductCategories()示例列举了ProductSubcategoryDB类的实现代码
示例实现ProductSubcategoryDB类
using System;
using SystemData;
using SystemDataCommon;
using SystemDataSqlClient;
using SystemCollectionsGeneric;
using SystemText;
using AdventureWorksTraderEntities;
using MicrosoftPracticesEnterpriseLibraryData;
namespace AdventureWorksTraderDataAccess
{
public class ProductSubcategoryDB
{
private DataColumnMapping[] mappings = new DataColumnMapping[] {
new DataColumnMapping(ProductSubcategoryIDProductSubcategoryID)
new DataColumnMapping(ProductCategoryIDProductCategoryID)
new DataColumnMapping(NameName)
new DataColumnMapping(rowguidRowguid)
new DataColumnMapping(ModifiedDateModifiedDate)};
public IList<ProductSubcategory> GetProductSubCategories(int productCategoryID)
{
IList<ProductSubcategory> list = new List<ProductSubcategory>();
Database db = DatabaseFactoryCreateDatabase();
string storedProcedureName = GetProductSubcategories;
DbCommand dbCommand = dbGetStoredProcCommand(storedProcedureName);
dbAddInParameter(dbCommand productCategoryID DbTypeInt productCategoryID);
using (IDataReader reader = dbExecuteReader(dbCommand))
{
while (readerRead())
{
ProductSubcategory temp = new ProductSubcategory();
ProductSubcategory subCategory = (ProductSubcategory)DataAccessHelperPopulateEntity(temp mappings reader);
listAdd(subCategory);
}
}
return list;
}
}
}
正如在示例看到的GetProductSubcategories()方法执行了GetProductSubcategories存储过程该存储过程接受productCategoryID参数此后的实现与GetProductCategories()方法相同
实现ProductDB类
正如名称暗示的那样示例所示的ProductDB类提供了专门访问AdventureWorks数据库中Product表的方法该类包括一个GetProducts()方法该方法根据提供的产品子类别ID返回所属的所有产品
示例实现ProdctDB类
using System;
using SystemData;
using SystemDataCommon;
using SystemDataSqlClient;
using SystemCollectionsGeneric;
using SystemText;
using AdventureWorksTraderEntities;
using MicrosoftPracticesEnterpriseLibraryData;
namespace AdventureWorksTraderDataAccess
{
public class ProductDB
{
private DataColumnMapping[] mappings = new DataColumnMapping[] {
new DataColumnMapping(ProductIDProductID)
new DataColumnMapping(NameName)
new DataColumnMapping(ProductNumberProductNumber)
new DataColumnMapping(MakeFlagMakeFlag)
new DataColumnMapping(FinishedGoodsFlagFinishedGoodsFlag)
new DataColumnMapping(ColorColor)
new DataColumnMapping(SafetyStockLevelSafetyStockLevel)
new DataColumnMapping(ReorderPointReorderPoint)
new DataColumnMapping(StandardCostStandardCost)
new DataColumnMapping(ListPriceListPrice)
new DataColumnMapping(SizeSize)
new DataColumnMapping(SizeUnitMeasureCodeSizeUnitMeasureCode)
new DataColumnMapping(WeightUnitMeasureCodeWeightUnitMeasureCode)
new DataColumnMapping(WeightWeight)
new DataColumnMapping(DaysToManufactureDaysToManufacture)
new DataColumnMapping(ProductLineProductLine)
new DataColumnMapping(ClassClass)
new DataColumnMapping(StyleStyle)
new DataColumnMapping(ProductSubcategoryIDProductSubcategoryID)
new DataColumnMapping(ProductModelIDProductModelID)
new DataColumnMapping(SellStartDateSellStartDate)
new DataColumnMapping(SellEndDateSellEndDate)
new DataColumnMapping(DiscontinuedDateDiscontinuedDate)
new DataColumnMapping(rowguidRowguid)
new DataColumnMapping(ModifiedDateModifiedDate)};
public IList<Product> GetProducts(int productSubcategoryID)
{
IList<Product> list = new List<Product>();
Database db = DatabaseFactoryCreateDatabase();
string storedProcedureName = GetProducts;
DbCommand dbCommand = dbGetStoredProcCommand(storedProcedureName);
dbAddInParameter(dbCommand productSubCategoryID DbTypeInt
productSubcategoryID);
using (IDataReader reader = dbExecuteReader(dbCommand))
{
while (readerRead())
{
Product temp = new Product();
Product prod = (Product)DataAccessHelperPopulateEntity(temp mappings reader);
listAdd(prod);
}
}
return list;
}
}
}
GetProducts()方法执行了GetProducts存储过程该存储过程接受产品子类别ID作为参数同时基于该产品子类别返回所属的所有产品列表
实现业务逻辑层
本节将关注业务逻辑层的实现该层封装在类库AdventureWorksTraderBiz中首先使用Visual Studio 创建新的名为AdventureWorksTraderBiz的Visual C#类库项目此后添加上一节中创建的AdventureWorksTraderDataAccess的引用一旦项目创建后可将名为ProductCategoryBiz的类添加到项目中接着如示例所示修改代码
示例实现ProductCategoryBiz类
using System;
using SystemCollectionsGeneric;
using SystemText;
using AdventureWorksTraderEntities;
using AdventureWorksTraderDataAccess;
using MicrosoftPracticesEnterpriseLibraryExceptionHandling;
namespace AdventureWorksTraderBiz
{
public class ProductCategoryBiz
{
public IList<ProductCategory> GetProductCategories()
{
try
{
ProductCategoryDB category = new ProductCategoryDB();
return categoryGetProductCategories();
}
catch (Exception ex)
{
bool rethrow = ExceptionPolicyHandleException(ex Log Only Policy);
if (rethrow)
{
throw;
}
return null;
}
}
}
}
示例中所示的GetProductCategories()是主要方法该方法通过调用数据访问层的GetProductCategories()方法来获取所需数据业务逻辑层中生成的任何异常都在catch块中通过EntLib异常处理块处理这是下一节关注的重要问题
由于业务逻辑层中剩余类(ProductSubcategoryBiz和ProductBiz)的实现与ProductCategoryBiz非常相似——除了它们调用的对应数据访问层类的方法不同——所以将不会详细讲解剩余类然而读者可从下载本实例的完整代码
快速浏览企业库中的异常处理块
开发人员编写的每个NET应用程序都需要处理异常以及从异常中恢复微软没有采取在NET应用程序中创建测试和维护传统代码的方法而是创建了称为企业库异常处理块(或者称为EntLib异常处理块)的应用程序块该应用程序块提供了实现异常处理所需的所有底层传统代码为达成本实例的目的将以统一高效的方式使用EntLib异常处理块处理异常在讨论异常处理块与Web站点AdventureWorksTrader集成的所需步骤之前读者需要理解异常处理块的基础知识
异常处理块中的关键组件
为理解异常处理块需要了解三个重要概念
异常处理当在代码中检测到异常时处理异常的过程
异常日志记录异常的过程该过程包括将格式化异常发送到事件日志或者发送电子邮件异常处理块利用日志和监测应用程序块来实现此功能
异常处理策略允许控制异常处理和使用外部配置文件记录的行为而不用在代码中实施这样的规则换句话说开发人员可在一个策略文件中定义异常处理然后在不改变代码的情况下在测试调试产品定型期间修改行为
使用异常处理块在代码中检测到异常时可做以下三件事情
第一可把异常封装为一个新的异常以便加入新的上下文信息或详细信息当新的异常传递到调用堆栈时仍可通过InnerException属性访问原始的异常
第二可用一个新的异常取代原有异常通常这样做的目的是不想让原始异常的详细信息跨应用程序边界传递
第三能够记录异常当然也可结合使用封装或取代的方法达成此目的或者记录原始异常并把它传递到调用堆栈
使用异常处理块
通过浏览/downloads和查找EntLib开发人员可下载EntLib缓存块其中包括了异常处理块在安装完企业库之后就可利用异常处理块开始编写代码为正确使用异常处理块需要遵循以下步骤
在解决方案中分别添加对MicrosoftPracticesEnterpriseLibraryCommondll和MicrosoftPracticesEnterpriseLibraryExceptionHandlingdll程序集的引用为此可使用添加引用选项并定位到<驱动程序名>:\Program Files\Microsoft Enterprise Library January \bin文件夹如果还需要结合异常处理使用日志记录则再添加对MicrosoftPracticesEnterpriseLibraryExceptionHandlingLoggingdll的引用
在nfig或者nfig文件中添加必要的配置项为此可在根<configuration>元素下的<configSections>元素中添加如下内容
<section name=exceptionHandling
type=MicrosoftPracticesEnterpriseLibraryExceptionHandling
ConfigurationExceptionHandlingSettingsMicrosoftPractices
EnterpriseLibraryExceptionHandling />
如果随同异常处理一起使用日志记录则还需要在<configSections>元素中添加以下设置
<section name=loggingConfiguration
type=MicrosoftPracticesEnterpriseLibraryLoggingConfiguration
LoggingSettingsMicrosoftPracticesEnterpriseLibraryLogging />
接下来直接在<configuration>元素下添加<exceptionHandling>元素在<exceptionHandling>内部可添加所有的异常处理策略以下代码说明在<exceptionHandling>中指定了一个名为Global Policy的策略
<exceptionHandling>
<exceptionPolicies>
<add name=Log Only Policy>
<exceptionTypes>
<add name=Exception type=SystemException mscorlib
Version= Culture=neutral PublicKeyToken=bace
postHandlingAction=ThrowNewException>
<exceptionHandlers>
<add logCategory=Default Category eventId= severity=Error
title=Exception Management Application Exception priority=
formatterType=MicrosoftPracticesEnterpriseLibrary
ExceptionHandlingTextExceptionFormatter
MicrosoftPracticesEnterpriseLibraryExceptionHandling
name=Logging Handler
type=MicrosoftPracticesEnterpriseLibraryExceptionHandling
LoggingLoggingExceptionHandler
MicrosoftPracticesEnterpriseLibrary
ExceptionHandlingLogging/>
</exceptionHandlers>
</add>
</exceptionTypes>
</add>
</exceptionPolicies>
</exceptionHandling>
正如这些设置所指示的那样有一个名为Log Only Policy的策略该策略能够记录异常使用<exceptionHandlers>节能够设置以适当方式处理异常的自定义异常处理程序在这种情况下自定义处理程序实现LoggingExceptionHandler类postHandlingAction属性用于根据策略设置异常处理之后的行为该属性包括以下可能值NoneNotifyRethrow和ThrowNewException
在代码中导入异常处理块的核心命名空间MicrosoftPracticesEnterpriseLibraryExceptionHandling
开始使用上述命名空间中的类编写代码
ExceptionPolicy对象
只要使用异常处理块就必须处理ExceptionPolicy类ExceptionPolicy类公开了静态方法HandleException()利用该方法可使客户端应用程序与异常处理块交互此时将策略作为参数提供HandleException()方法使用类工厂来为相应的策略创建ExceptionPolicyImpl类型的对象ExceptionPolicyImpl对象具有一个ExceptionPolicyEntry对象集合在名为策略的配置文件中每种异常类型都对应一个对象对每一种异常类型ExceptionPolicyEntry对象都包含一个实现IExceptionHandler接口的对象集合当执行策略时对象集合能够提供异常处理块使用的序列每个实现IExceptionHandler接口的对象都与对应处理方法的类型相关联
异常处理方法是 NET类其封装了异常处理逻辑并实现了定义在异常处理块中的IExceptionHandler接口默认情况下异常处理块包含以下三种异常处理程序
封装处理程序此异常处理程序使用一个异常封装另一个异常
取代处理程序此异常处理程序用一个异常取代另一个异常
日志记录处理程序此异常处理程序对异常信息进行格式化处理例如消息和堆栈跟蹤然后日志记录处理方法将这些信息记录到日志块以作日后查证
既然已经了解了异常处理块的基础知识下面将重新讲解业务逻辑层类的代码以便理解异常处理块的集成
public IList<ProductCategory> GetProductCategories()
{
try
{
ProductCategoryDB category = new ProductCategoryDB();
return categoryGetProductCategories();
}
catch (Exception ex)
{
bool rethrow = ExceptionPolicyHandleException(ex Log Only Policy);
if (rethrow)
{
throw;
}
return null;
}
}
在try块中无论何时发生异常都将在catch块中捕获异常并在其中调用ExceptionPolicy对象的HandleException()方法以便记录异常在记录异常之后可查看HandleException()方法的返回值(根据配置文件中的postHandlingAction属性)以便确定是否需要向调用者抛出异常
小结
本文介绍的数据访问层实现中涉及集成重用块开发人员使用这些块能够快速方便的实现数据访问异常处理等很多重复性高的任务建议读者在学习这些重用块之后将其应用到自身的项目开发过程中从而提高开发效率
下一篇文章讲解Web站点部分的实现方法