简介
在教程一中创建的数据访问层 (DAL) 将数据访问逻辑与表示逻辑清晰地分离开来。然而,尽管 DAL 从表示层中清晰地分离出数据访问层细节,它却并没有实施任何可能采用的业务规则。例如,我们想让我们的应用程序在 Discontinued 字段设为 1 时禁止对 Products 表的 CategoryID 或 SupplierID 字段的修改,还有,我们可能想实施一些资历规则以便禁止发生这样的情况:雇员被其后入职的另一雇员所管理。另一种常见的情形是授权 – 可能只有处于特定职位的用户可以删除产品或更改 UnitPrice 值。
通过本教程,我们可以了解怎样将业务规则集中到在表示层与DAL 之间充当数据交互中介的业务逻辑层 (BLL) 中。在真实的应用程序中,BLL 应作为一个单独的类库项目而实现。然而,为了简化项目结构,在这些教程中,我们以 App_Code 文件夹下的一系列的类来实现 BLL 。图 1 展示了表示层、BLL 和 DAL 之间的结构关系。
图1 :BLL 将表示层与数据访问层分隔开来并且实施业务规则。
步骤1 :创建 BLL 类
我们的BLL 将由四个类组成,分别对应 DAL 中不同的 TableAdapter 。每个 BLL 类都具有一些方法,这些方法可以从 DAL 中该类对应的 TableAdapter 中检索、插入、更新或删除数据并应用相应的业务规则。
为了更清楚地区分 DAL 的相关类与 BLL 的相关类,我们在 App_Code 文件夹下创建两个子文件夹:DAL 和 BLL 。创建时,只需右健单击 Solution Explorer 中的 App_Code 文件夹并选择 New Folder 。创建了这两个文件夹后,将教程一中创建的 Typed DataSet 移动到 DAL 子文件夹中。
然后,在BLL 子文件夹中创建四个 BLL 类文件。为此,右键单击 BLL 子文件夹,选择 Add a New Item ,然后选择 Class 模板。将这四个类分别命名为 ProductsBLL 、 CategoriesBLL 、 SuppliersBLL 和 EmployeesBLL 。
图2 :在App_Code 文件夹中添加四个新类
接下来让我们在每个类中添加一些方法,这些方法只是简单地封装教程一中为TableAdapters 定义的方法。目前,这些方法只是对 DAL 中内容的直接调用,稍后我们会返回到这些方法中来添加任何所需的业务逻辑。
注意: 如果您当前使用的是Visual Studio Standard Edition 或以上版本 ( 即,当前使用的不是Visual Web Developer ),您可以使用 Class Designer 以可视的方式随意设计自己的类。有关 Visual Studio 中该新特性的详细信息,请参见 Class Designer Blog 。
对于ProductsBLL 类,总共需要添加七个方法 :
- GetProducts() – 返回所有产品。
- GetProductByProductID(productID) – 返回具有指定产品 ID 的产品。
- GetProductsByCategoryID(categoryID) – 返回指定 种类 中的所有产品。
- GetProductsBySupplier(supplierID) – 返回来自指定供应商的所有产品。
- AddProduct(productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued) – 通过传入值将一个新产品插入到数据库中 ; 返回新插入记录的 ProductID 值。
- UpdateProduct(productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued, productID) – 通过传入值更新数据库中的一个现有产品 ; 如果正好更新了一行则返回 true , 否则返回 false 。
- DeleteProduct(productID) – 从数据库中删除指定产品。
ProductsBLL.cs
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsBLL
{
private ProductsTableAdapter _productsAdapter = null;
protected ProductsTableAdapter Adapter
{
get {
if (_productsAdapter == null)
_productsAdapter = new ProductsTableAdapter();
return _productsAdapter;
}
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, true)]
public Northwind.ProductsDataTable GetProducts()
{
return Adapter.GetProducts();
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductByProductID(int productID)
{
return Adapter.GetProductByProductID(productID);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID)
{
return Adapter.GetProductsByCategoryID(categoryID);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsBySupplierID(int supplierID)
{
return Adapter.GetProductsBySupplierID(supplierID);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Insert, true)]
public bool AddProduct(string productName, int? supplierID, int? categoryID,
string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
short? unitsOnOrder, short? reorderLevel, bool discontinued)
{
// Create a new ProductRow instance
Northwind.ProductsDataTable products = new Northwind.ProductsDataTable();
Northwind.ProductsRow product = products.NewProductsRow();
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull();
else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull();
else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull();
else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull();
else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull();
else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Add the new product
products.AddProductsRow(product);
int rowsAffected = Adapter.Update(products);
// Return true if precisely one row was inserted,
// otherwise false
return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(string productName, int? supplierID, int? categoryID,
string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false;
Northwind.ProductsRow product = products[0];
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull();
else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull();
else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull();
else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull();
else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull();
else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated,
// otherwise false
return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct(int productID)
{
int rowsAffected = Adapter.Delete(productID);
// Return true if precisely one row was deleted,
// otherwise false
return rowsAffected == 1;
}
}
这些方法 —GetProducts 、GetProductByProductID 、GetProductsByCategoryID 和 GetProductBySuppliersID ,只是返回数据,它们相当直接、简单,因为它们只是向下调用 DAL 中的内容。在一些场合下,可能会有一些业务规则需要在此层实现(例如基于当前已登录用户或用户所处职位的授权规则,可以访问不同的数据),但在这里我们只是保留这些方法不变。因此,对于这些方法, BLL 只是充当了一个代理的作用,表示层通过这个代理来访问数据访问层中的底层数据。
AddProduct 和 UpdateProduct 方法将产品各字段的值以参数形式传入,它们的作用分别是:添加一个新产品,更新一个现有产品。由于 Product 表的许多列,如 CategoryID 、 SupplierID 和 UnitPrice ,都可接受 NULL 值, AddProduct 和 UpdateProduct 中与这样的列相对应的输入参数使用 nullable 类型 。 Nullable 类型对于 .NET 2.0 来说是新类型,利用该类型所提供的技术,我们可以指示一个值类型是否可以是空类型。在 C# 中,可以通过在类型后加问号 ? 将一个值类型标记为 nullable 类型(例如 int? x; )。有关详情,请参见 C# 编程指南 中的 Nullable 类型 一节。
这三个方法均返回布尔值,该值指示是否成功的插入、更新或删除了一行。返回该值的原因是,方法的操作并不一定会影响到一行。例如,如果页面开发人员调用 DeleteProduct 时传入的 ProductID 并非一个现有产品的 ID ,则发给数据库的 DELETE 语句不会产生任何影响,因而 DeleteProduct 方法会返回 false。
请注意,当添加一个新产品或更新一个现有产品时,我们将新的或更改的产品的字段值用一组数值传入,而不是为此接受一个ProductsRow 实例。选择该方式的原因是, ProductsRow 类派生于 ADO.NET DataRow 类,而后者并没有一个默认的无参数构造函数。为了创建一个新的 ProductsRow 实例,首先要创建一个 ProductsDataTable 实例,然后调用它的 NewProductRow() 方法(就像我们在AddProduct方法中作的那样)。当我们使用 ObjectDataSource 插入或更新产品时,其缺陷就会暴露出来。简言之, ObjectDataSource 会尝试为输入的参数创建一个实例。如果 BLL 方法期待的是一个 ProductsRow 实例,则 ObjectDataSource 会尝试创建一个这样的实例,但是,由于缺少默认的无参数构造函数,该尝试失败。有关该问题的详细信息,请参见以下两个 ASP.NET 论坛: 使用强类型DataSet 更新ObjectDataSources 、 ObjectDataSource 与强类型DataSet 的问题 。
另外,AddProduct 和 UpdateProduct 中的代码都会创建一个ProductsRow 实例并以刚传入的值对该实例进行赋值。当向 DataRow 的一些 DataColumn 赋值时,可发生各种字段级的验证检查。因此,将传入的值进行一下人工的验证有助于确保传递给 BLL 方法的数据的有效性。不幸的是, Visual Studio 生成的强类型的 DataRow 类并不使用 nullable 类型。而为了给 DataRow 中的特定 DataColumn 赋数据库空值,我们必须使用 SetColumnNameNull() 方法。
在UpdateProduct 中,我们 首先用 GetProductByProductID(productID) 载入要更新的产品。尽管这看似是一次不必要的对数据库的操作,在将来的介绍并发优化的教程中,该往返将会被证明是值得的。并发优化技术可确保两个同时对同一数据进行操作的用户不会在不经意间覆盖彼此所作的更改。获取整个记录还使以下事情变得容易:在 BLL 中创建更新方法,使该方法只修改 DataRow 的所有列的一个子集。当我们研究 SuppliersBLL 类时,我们会看到这样一个例子。
最后,请注意对ProductsBLL 类使用了 DataObject 属性 ( 接近文件开头 , 类声明语句前面的 [System.ComponentModel.DataObject] 标签 ), 而其方法有 DataObjectMethodAttribute属性 。 DataObject 属性将该类标记为一个适合绑定到 ObjectDataSource 控件 的对象,而 DataObjectMethodAttribute 属性则指示该方法的用途。在将来的教程中可以看到, ASP.NET 2.0 的 ObjectDataSource 使得以声明的方式从类中访问数据变得容易。默认情况下,在 ObjectDataSource 向导的下拉列表中只显示出标记为 DataObject 的那些类,这样有助于在该向导中筛选出可绑定的那些类。 ProductsBLL 类没有这些属性一样会工作良好,但是,加入这些属性可以使得在 ObjectDataSource 向导下的工作更为轻松。
添加其它类
在完成 ProductsBLL 类的编写后,我们还需要添加一些处理种类、供应商及雇员数据的类。我们花一些时间用上面例子中的概念来创建下面的类和方法:
-
CategoriesBLL.cs
- GetCategories()
- GetCategoryByCategoryID(categoryID)
-
SuppliersBLL.cs
- GetSuppliers()
- GetSupplierBySupplierID(supplierID)
- GetSuppliersByCountry(country)
- UpdateSupplierAddress(supplierID, address, city, country)
-
EmployeesBLL.cs
- GetEmployees()
- GetEmployeeByEmployeeID(employeeID)
- GetEmployeesByManager(managerID)
值得注意的一个方法是 SuppliersBLL 类的UpdateSupplierAddress 方法。该方法提供了一个接口以便只更新供应商的地址信息。在内部实现上,该方法读取指定supplierID 的 SupplierDataRow 对象(使用GetSupplierBySupplierID 来读取),设置其相关地址属性,然后向下调用SupplierDataTable 的更新方法。UpdateSupplierAddress 方法如下:
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateSupplierAddress
(int supplierID, string address, string city, string country)
{
Northwind.SuppliersDataTable suppliers =
Adapter.GetSupplierBySupplierID(supplierID);
if (suppliers.Count == 0)
// no matching record found, return false
return false;
else
{
Northwind.SuppliersRow supplier = suppliers[0];
if (address == null) supplier.SetAddressNull();
else supplier.Address = address;
if (city == null) supplier.SetCityNull();
else supplier.City = city;
if (country == null) supplier.SetCountryNull();
else supplier.Country = country;
// Update the supplier Address-related information
int rowsAffected = Adapter.Update(supplier);
// Return true if precisely one row was updated,
// otherwise false
return rowsAffected == 1;
}
}
步骤 2:通过 BLL 类访问 Typed DataSets
在教程一中我们看到了直接使用 Typed DataSet 的编程例子。而现在我们已添加了一些 BLL 类,因此表示层应转而基于 BLL 而工作。教程一的 AllProducts.aspx 例子使用了 ProductsTableAdapter 来将产品列表绑定到一个 GridView,见下面的代码:
ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();
要使用 BLL 类,只需改变代码的第一行 – 只需用 ProductBLL 对象代替 ProductsTableAdapter 对象:
ProductsBLL productLogic = new ProductsBLL();
GridView1.DataSource = productLogic.GetProducts();
GridView1.DataBind();
也可以使用 ObjectDataSource 以声明的方式来访问BLL 类(如同 Typed DataSet )。我们将在后续教程中更为详细地讨论ObjectDataSource 。
图3 : 产品列表显示于GridView 中
步骤3 :向 DataRow 类添加字段级验证
字段级验证是进行插入或更新操作时针对业务对象的属性值而进行的检查。下面是对产品的一些字段级验证规则:
- ProductName 字段的长度不能超过 40 个字符。
- QuantityPerUnit 字段的长度不能超过 20 个字符。
- ProductID 、ProductName 和 Discontinued 字段是必需的,但所有其它字段是可选的。
- UnitPrice 、UnitsInStock 、UnitsOnOrder 和 ReorderLevel 字段必须大于等于零。
这些规则可以并且应该在数据库级表达出来。Products 表的相应列的数据类型可反映对 ProductName 和 QuantityPerUnit 字段的字符数限制(分别为 nvarchar(40) 和nvarchar(20) )。对字段是可选还是必需的表达是这样的:数据库表列允许还是不允许NULL 。四个 检查约束 的存在确保只有大于等于零的值才可赋值给UnitPrice 、UnitsInStock 、UnitsOnOrder 和 ReorderLevel 列。
这些规则除了在数据库级实施外还应在DataSet 级实施。事实上,字段长度以及某值是必需的还是可选的,已被DataTable 的 DataColumn 集定义。要查看现有的自动提供的字段级验证,可转到DataSet 设计器,从其中一个DataTable 中选择一个域,然后转至 Properties 窗口。如图 4 所示,ProductsDataTable 中的 QuantityPerUnit DataColumn 允许的最大长度是 20 个字符,并且允许 NULL 值。如果我们试图将 ProductsDataRow 的 QuantityPerUnit 属性设置为一个超过 20 个字符的字符串值,系统会抛出 ArgumentException 异常 。
图4 :DataColumn 提供基本域级验证
不幸的是,我们不能通过 Properties 窗口指定边界检查,如,UnitPrice 必须大于等于零这样的检查。为了提供此类字段级验证,需要创建一个针对DataTable 的 ColumnChanging 事件的Event Handler。如 前一教程 所述,Typed DataSet 创建的 DataSet 、DataTables 和 DataRow 对象可以通过使用部分类来扩展。利用该技术我们可以为ProductsDataTable 类创建一个 ColumnChanging 事件的Event Handler。首先,在 App_Code 文件夹下创建一个名为 ProductsDataTable.ColumnChanging.cs 的类。
图5 :在App_Code 文件夹中添加一个新类
其次,创建一个针对 ColumnChanging 事件的Event Handler以确保UnitPrice 、UnitsInStock 、UnitsOnOrder 和 ReorderLevel 列的值(如果不是 NULL )大于等于零。其中任何一列超出范围,系统都会给出ArgumentException 。
ProductsDataTable.ColumnChanging.cs
public partial class Northwind
{
public partial class ProductsDataTable
{
public override void BeginInit()
{
this.ColumnChanging += ValidateColumn;
}
void ValidateColumn(object sender,
DataColumnChangeEventArgs e)
{
if(e.Column.Equals(this.UnitPriceColumn))
{
if(!Convert.IsDBNull(e.ProposedValue) &&
(decimal)e.ProposedValue < 0)
{
throw new ArgumentException(
"UnitPrice cannot be less than zero", "UnitPrice");
}
}
else if (e.Column.Equals(this.UnitsInStockColumn) ||
e.Column.Equals(this.UnitsOnOrderColumn) ||
e.Column.Equals(this.ReorderLevelColumn))
{
if (!Convert.IsDBNull(e.ProposedValue) &&
(short)e.ProposedValue < 0)
{
throw new ArgumentException(string.Format(
"{0} cannot be less than zero", e.Column.ColumnName),
e.Column.ColumnName);
}
}
}
}
}
步骤 4:向 BLL 类添加定制的业务规则
除了字段级验证外,可能还有高级定制的业务规则,这些规则涉及不同的实体,或者涉及到不能在单个列中表达的概念,例如:
- 如果一产品为断货 (discontinued) 产品,其 UnitPrice 就不能被更新。
- 雇员的居住国必须与其经理的居住国相同。
- 如果某产品是其供应商提供的唯一产品,该产品就不能为断货产品。
BLL 类应含有检查,以确保遵守应用程序的业务规则。可将这些检查直接添加到它们所应用到的方法中。
假设我们的业务规则规定:如果某产品是指定供应商的唯一产品,该产品就不能标记为discontinued 。即,如果产品 X 是我们从供应商Y 处购买的唯一产品,我们就不能将 X 标记为 discontinued ;但是如果供应商 Y 为我们提供了三个产品:A 、B 和 C ,那么我们可将其中任何一个或所有的标记为discontinued 。这是一个奇怪的业务规则,但业务规则并不总是符合一般常识!
为了对 UpdateProducts 方法实施此业务规则,我们首先检查Discontinued 是否设置为 true ,如是,我们会调用GetProductsBySupplierID 来确定我们从该产品的供应商处购买了多少个产品。如果从该供应商处只购买了一个产品,我们就抛出ApplicationException 异常 。
public bool UpdateProduct(string productName, int? supplierID, int? categoryID,
string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false;
Northwind.ProductsRow product = products[0];
// Business rule check - cannot discontinue
// a product that is supplied by only
// one supplier
if (discontinued)
{
// Get the products we buy from this supplier
Northwind.ProductsDataTable productsBySupplier =
Adapter.GetProductsBySupplierID(product.SupplierID);
if (productsBySupplier.Count == 1)
// this is the only product we buy from this supplier
throw new ApplicationException(
"You cannot mark a product as discontinued if it is the only
product purchased from a supplier");
}
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull();
else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull();
else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull();
else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull();
else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull();
else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated,
// otherwise false
return rowsAffected == 1;
}
在表示层对验证错误进行响应
从表示层调用BLL 时,我们可以决定是尝试对任何可能出现的异常情况进行处理,还是让这些异常直接抛给ASP.NET (它们会引发HttpApplication 的错误事件)。要在编程使用 BLL 时处理一个异常,我们可以使用 try...catch 块,如下所示:
ProductsBLL productLogic = new ProductsBLL();
// Update information for ProductID 1
try
{
// This will fail since we are attempting to use a
// UnitPrice value less than 0.
productLogic.UpdateProduct(
"Scott s Tea", 1, 1, null, -14m, 10, null, null, false, 1);
}
catch (ArgumentException ae)
{
Response.Write("There was a problem: " + ae.Message);
}
在以后的教程中我们会看到,当使用一个Web 数据控件来插入、更新或删除数据时,可以通过一个Event Handler对从 BLL 抛出的异常进行处理而不用将该处理代码封装于try...catch 块中。
小结
一个结构良好的应用程序都有清晰的层次结构,每层都封装有特定的任务。在本系列文章的第一篇教程中,我们用Typed DataSet 创建了一个数据访问层;在本篇教程中,我们建立了一个业务逻辑层,该层包括我们的应用程序的App_Code 文件夹下的一系列类,这些类向下调用DAL 中的内容。我们的应用程序通过 BLL 实现了字段级和业务级逻辑。在本教程中,我们创建了一个独立的BLL ,除此之外的另一个选择是,利用部分类来扩展TableAdapters 的方法。但是,使用这一技术,我们并不能重写现有的方法,也不能象本文中采用的方式一样清晰地分隔开我们的DAL 和 BLL 。
完成 DAL 和BLL 的代码编写后,我们就可以着手编写我们的表示层代码了。在 下一教程 中,我们会短暂地偏离数据访问主题,转而去定义一个将为所有教程所使用的一致的页面布局。