ADO.NET 多数据库、多层解决方案






3.78/5 (23投票s)
介绍如何在多数据库、多层环境中使用 ADO.NET。
引言
本文是我关于 C# 和 ADO.NET 中的多层应用程序的实现方法。该程序包含 GUI(图形用户界面)、BO(业务对象)和 DO(数据对象)。在我看来,多层程序是指 GUI 没有逻辑(没有做出决策的代码),DO 只负责从数据库读取和保存数据,而所有其他操作都由 BO 完成。这就是我用这段代码尝试实现的目标。
在解决了多层问题后,我想创建一个**多数据库**平台。在代码中,我为每个 DataProvider 或 DBMS(数据库管理系统)添加了一个不同的 DO。这意味着 BO 只需要调用正确的 DO 版本,并将它们组合成一个 `DataSet`。
在我的代码中,甚至可以从 Access 数据库加载数据,并将更改保存到 MS SQL Server 数据库。但我并不推荐这样做。
最后但同样重要的是,我想创建一个能够解决数据库并发错误的框架。我认为可以以相同的方式处理所有数据库并发错误,无论使用何种数据库和表。
使用示例
在 zip 文件中,有一个名为“Multi-Tier”的文件夹,其中包含解决方案文件。只需在 VS.NET 2003 中打开它并运行即可。要测试 Access 数据库的代码,无需额外操作。`Northwind` 数据库也位于“Muti-Tier”文件夹中。
在工具栏上,有两个按钮已启用。第一个是加载按钮;另一个是保存按钮。加载按钮有一个下拉菜单,您可以在其中选择数据库和连接。选择源后,单击加载按钮,数据应会加载到网格中。现在可以更改、添加或删除行。完成所有更改后,按保存按钮,更改就会保存到数据库。
如果在加载或保存过程中发生错误,状态栏会显示一条消息。
* 使用 MS SQL 和 `SqlClient` 时的注意事项。
如果使用 `SqlClient`,则必须在 `Northwind` 数据库中执行以下查询。这是为了测试数据库并发。
ALTER TABLE dbo.Customers ADD Timestamp timestamp NULL.
使用代码
我的示例包含 4 个项目。第一个(`MultiTier.Data`)包含与 DO 相关的功能,但对于每个 DO 都相同。其他 3 个是使用 `Northwind` 数据库的示例应用程序。
- `MultiTier.Example` 包含 GUI。
- `MultiTier.Example.Business` 是 BO。
- `MultiTier.Example.Data` 包含 DO 代码。
GUI 有两个按钮,一个用于从数据库加载数据,另一个用于将更改保存到数据库。状态栏会显示最后一次操作的输出 5 秒。
Data 类
`Data` 类是一个抽象类,包含基本函数和属性。
using System;
using System.Data;
namespace MultiTier.Data {
public abstract class Data {
private DataTable dtDataTable;
private Exception exException;
private long lngAffectedRecords;
private string strConnectionString;
private string strTableName;
private Data() {
}
protected Data(string connection) {
this.ConnectionString = connection;
this.ConstructDataAdapter();
}
protected DataTier(string connection, string tableName) {
this.ConnectionString = connection;
this.TableName = tableName;
this.ConstructDataAdapter();
}
protected DataTable Table {
get { return dtDataTable; }
set { dtDataTable = value; }
}
public string ConnectionString {
get { return strConnectionString; }
set { strConnectionString = value; }
}
public string TableName {
get { return strTableName; }
set { strTableName = value; }
}
public long AffectedRecords {
get { return lngAffectedRecords; }
set { lngAffectedRecords = value; }
}
public Exception CurrentException {
get { return exException; }
set { exException = value; }
}
protected abstract void ConstructDataAdapter();
public abstract DataTable ReadData();
public abstract void SaveData(DataTable dataTable);
public abstract void InitializeConnection();
}
}
派生类实现特定于数据库的代码。我的示例中有一个 `OleDB` 类和一个 `Sql` 类,但可以添加其他类。代码包含 `ReadData` 和 `SaveData` 函数。`DataAdapter` 在派生自这些类之一的类中创建(参见数据对象代码)。
public override DataTable ReadData() {
/*--- Create a new DataSet ---*/
this.Table = new DataTable(this.TableName);
this.Table.Locale = CultureInfo.InvariantCulture;
try {
/*--- Fill the DataSet with the Customers ---*/
daDataAdapter.Fill(this.Table);
/*--- Return the DataSet ---*/
return this.Table;
}
catch (OleDbException ex) {
/*--- An error occurred, so we roll the transaction back ---*/
this.CurrentException = ex;
/*--- Return the DataSet ---*/
return this.Table;
}
}
public override void SaveData(DataTable dataTable) {
this.Table = dataTable;
OleDbTransaction trTransaction = null;
/*--- Save the data ---*/
try {
/*--- Set up the conection manually ---*/
InitializeConnection();
cnConnection.Open();
/*--- Begin a transaction ---*/
trTransaction = cnConnection.BeginTransaction();
/*--- Make all database changes ---*/
this.AffectedRecords = daDataAdapter.Update(this.Table);
/*--- Commit the changes ---*/
trTransaction.Commit();
}
catch (DBConcurrencyException ex) {
/*--- An error occurred, so we roll the transaction back ---*/
this.CurrentException = ex;
trTransaction.Rollback();
}
catch (OleDbException ex) {
/*--- An error occurred, so we roll the transaction back ---*/
this.CurrentException = ex;
trTransaction.Rollback();
}
finally {
/*--- Close the connection that we manually opened ---*/
trTransaction = null;
cnConnection.Close();
cnConnection = null;
}
}
工作原理
GUI 只是一个带有网格的窗体,它调用 BO 中的 `ReadNorthwind` 并返回一个 `DataSet`。BO 调用 DO 中的 `ReadData` 方法,并从该方法返回一个 `DataTable`。BO 将 `DataTable` 放入 `DataSet`,添加主键、关系和自定义列。然后将 `DataSet` 发送到 GUI。
当 GUI 调用 `SaveNorthwind` 时,`DataSet` 会随之传递。然后 BO 调用 DO 中的 `SaveData` 方法。DO 然后保存 `DataTable`。
数据对象代码(DO)
using System;
using System.Data;
using System.Data.OleDb;
namespace MultiTier.Example.Data {
public class CustomersOle : MultiTier.Data.OleDB {
public CustomersOle(string connection) : base(connection) {
}
protected override void ConstructDataAdapter() {
string strQuery = "";
OleDbCommand cmSelect;
OleDbCommand cmUpdate;
OleDbCommand cmInsert;
OleDbCommand cmDelete;
/*--- Set up the Connection ---*/
InitializeConnection();
/*--- Set up the SELECT Command ---*/
strQuery = @"SELECT CustomerID,
CompanyName, ContactName, City, Region
FROM Customers
ORDER BY CompanyName";
cmSelect = null;
cmSelect = new OleDbCommand(strQuery, this.Connection);
cmSelect.CommandType = CommandType.Text;
/*--- Set up the UPDATE Command ---*/
strQuery = @"UPDATE Customers
SET CompanyName = @CompanyName ,
ContactName = @ContactName, City = @City, Region = @Region
WHERE CustomerID = @CustomerID";
cmUpdate = null;
cmUpdate = new OleDbCommand(strQuery, this.Connection);
cmUpdate.CommandType = CommandType.Text;
cmUpdate.Parameters.Add(new OleDbParameter("@CompanyName",
OleDbType.VarWChar, 40, "CompanyName"));
cmUpdate.Parameters.Add(new OleDbParameter("@ContactName",
OleDbType.VarWChar, 30, "ContactName"));
cmUpdate.Parameters.Add(new OleDbParameter("@City",
OleDbType.VarWChar, 15, "City"));
cmUpdate.Parameters.Add(new OleDbParameter("@Region",
OleDbType.VarWChar, 15, "Region"));
cmUpdate.Parameters.Add(new OleDbParameter("@CustomerID",
OleDbType.WChar, 5, "CustomerID"));
/*--- Set up the INSERT Command ---*/
strQuery = @"INSERT INTO Customers (CompanyName,
ContactName, City, Region, CustomerID)
VALUES (@CompanyName, @ContactName,
@City, @Region, @CustomerID)";
cmInsert = null;
cmInsert = new OleDbCommand(strQuery, this.Connection);
cmInsert.CommandType = CommandType.Text;
cmInsert.Parameters.Add(new OleDbParameter("@CompanyName",
OleDbType.VarWChar, 40, "CompanyName"));
cmInsert.Parameters.Add(new OleDbParameter("@ContactName",
OleDbType.VarWChar, 30, "ContactName"));
cmInsert.Parameters.Add(new OleDbParameter("@City",
OleDbType.VarWChar, 15, "City"));
cmInsert.Parameters.Add(new OleDbParameter("@Region",
OleDbType.VarWChar, 15, "Region"));
cmInsert.Parameters.Add(new OleDbParameter("@CustomerID",
OleDbType.WChar, 5, "CustomerID"));
/*--- Set up the DELETE Command ---*/
strQuery = @"DELETE FROM Customers
WHERE CustomerID = @CustomerID";
cmDelete = null;
cmDelete = new OleDbCommand(strQuery, this.Connection);
cmDelete.CommandType = CommandType.Text;
cmDelete.Parameters.Add(new OleDbParameter("@CustomerID",
OleDbType.WChar, 5, "CustomerID"));
/*--- Create and set up the DataAdapter ---*/
this.DataAdapter = new OleDbDataAdapter();
this.DataAdapter.SelectCommand = cmSelect;
this.DataAdapter.UpdateCommand = cmUpdate;
this.DataAdapter.InsertCommand = cmInsert;
this.DataAdapter.DeleteCommand = cmDelete;
/*--- Destroy connection object ---*/
this.Connection = null;
}
}
}
业务对象数据(BO)
public DataSet ReadNorthwind() {
DataSet dsDataSet = new DataSet("Northwind");
DataTable dtCustomers;
DataTier doCustomers;
try {
/*--- Reset Exception ---*/
this.Exception = null;
/*--- Make database choice ---*/
if (this.Provider == DataProvider.OleDB) {
doCustomers = new
MultiTier.Example.Data.CustomersOle(this.ConnectionString,
"Customers");
} else {
doCustomers = new
MultiTier.Example.Data.CustomersSql(this.ConnectionString,
"Customers");
}
/*--- Read Customers ---*/
dtCustomers = doCustomers.ReadData();
/*--- Catch errors ---*/
if (doCustomers.Exception != null) {
this.AffectedRecords = 0;
throw doCustomers.Exception;
} else {
/*--- Set keys on the DataTables ---*/
dtCustomers.PrimaryKey = new DataColumn[]
{ dtCustomers.Columns["CustomerID"] };
/*--- Add Columns to DataTable ---*/
/*--- Add Rows to DataTable ---*/
/*--- Add DataTables to DataSet ---*/
dsDataSet.Tables.Add(dtCustomers);
/*--- Add DataRelations to DataSet ---*/
/*--- Set the total of loaded records ---*/
this.AffectedRecords = doCustomers.AffectedRecords;
}
/*--- Return DataSet ---*/
return dsDataSet;
}
catch (Exception ex) {
this.Exception = ex;
return dsDataSet;
}
finally {
dtCustomers = null;
doCustomers = null;
}
}
public void SaveNorthwind(DataSet dsDataSet) {
DataTable dtCustomers;
DataTier doCustomers;
try {
/*--- Reset Exception ---*/
this.Exception = null;
/*--- Check for changes with the HasChanges method first. ---*/
if (dsDataSet != null & dsDataSet.HasChanges()) {
/*--- Grab all changed rows ---*/
dtCustomers = dsDataSet.Tables["Customers"].GetChanges();
/*--- Check for changes in the DataTable. ---*/
if (dtCustomers != null) {
/*--- Make database choice ---*/
if (this.Provider == DataProvider.OleDB) {
doCustomers = new
MultiTier.Example.Data.CustomersOle(this.ConnectionString,
"Customers");
} else {
doCustomers = new
MultiTier.Example.Data.CustomersSql(this.ConnectionString,
"Customers");
}
/*--- Save Customers ---*/
doCustomers.SaveData(dtCustomers);
/*--- Catch errors ---*/
if (doCustomers.Exception != null) {
this.AffectedRecords = 0;
throw doCustomers.Exception;
} else {
this.AffectedRecords = doCustomers.AffectedRecords;
}
}
}
}
catch (Exception ex) {
this.Exception = ex;
}
finally {
dtCustomers = null;
doCustomers = null;
}
}
图形用户界面代码(GUI)
private void LoadData() {
grdData.DataBindings.Clear();
dsDataSet = boNorthwind.ReadNorthwind();
grdData.DataSource = dsDataSet.Tables["Customers"];
if (boNorthwind.Exception == null)
SetMessage(boNorthwind.AffectedRecords + " records are loaded.");
else
SetMessage("While loading the record(s) a '" +
boNorthwind.Exception.GetType().ToString() + "' occured.");
}
private void SaveData() {
boNorthwind.SaveNorthwind(dsDataSet);
if (boNorthwind.Exception == null)
SetMessage(boNorthwind.AffectedRecords + " records are saved.");
else
SetMessage("While saving the record(s) a '" +
boNorthwind.Exception.GetType().ToString() + "' occured.");
}
并发错误
当用户 A 和用户 B 同时读取数据库中的一条记录,然后进行更改并保存回数据库时,可能会发生并发。最后更新记录的用户将覆盖第一个用户更新的数据。这可以通过 ADO.NET 中的一个异常 `DBConcurrency` 来捕获。
要产生这种异常,记录必须有一个版本号。这可以通过 SQL Server 在表中添加一个 Timestamp 列来实现。
我在 `SqlClient` 示例中使用 Timestamp 列。当您两次打开程序并在两个网格中更改同一行中的某些内容并尝试将更改保存回网格时,就会生成 `DBConcurrency` 错误。
UPDATE Customers
SET CompanyName = @CompanyName ,
ContactName = @ContactName, City = @City, Region = @Region
WHERE CustomerID = @CustomerID
AND Timestamp = @Timestamp
有关**并发**的更多信息,请通过 Google 搜索。
自动增量
在使用断开连接的数据时,我们需要确保 `DataTable`(断开连接)中的标识符或自动增量是唯一的,并且在数据库中也是唯一的。如果程序只有一个人使用,那么通常没有问题。自动增量值可以保存到数据库。但是,当多个用户正在操作同一个数据库时,当用户尝试添加一个 ID 已存在的行时,可能会发生并发错误。
要解决这个问题,您只需为表中与标识列关联的 `Column` 对象设置两个或三个属性即可。
AutoIncrement = True
这告诉 ADO.NET,当向本地(内存中)`DataTable` 添加新行时,它将自动管理此列中的值。根据构建 `DataTable` 时使用的选项,此属性可能已设置。
AutoIncrementSeed = 0
这告诉 ADO.NET 从一个特定值开始计数新的标识值——在本例中为零。您可以从任何地方开始,但我建议使用小于 1 的值,以避免与 `DataTable` 行集中当前存在的任何标识值发生冲突。不,这些新数字与其他应用程序中的 `DataTable` 发生冲突无关紧要——它们不会保存到数据库。
AutoIncrementStep = -1
这告诉 ADO.NET,对于每个新行,它将通过此量更改自动生成号。在本例中,-1 表示使新数字更大(在负方向上)。同样,这可以防止与 `DataTable` 中的其他行发生冲突。
当您最终使用 `DataAdapter` 的 `Update` 方法时,其 SQL 应该执行 `INSERT` 语句来添加新行——但**不**包含本地生成的标识值。
然后有两种方法可以将数据库生成的 ID 提取到断开连接的 `DataTable` 中。第一种方法是从数据库重新加载数据。这可能会带来开销,但其他用户所做的所有更改现在都在断开连接的 `DataTable` 中。用户正在处理准确的数据。第二种选择是仅获取数据库生成的 ID,并将更改应用到断开连接的 `DataTable`。这可以通过 `@@identity` 来完成,并且必须在每次插入后进行。
已知问题
以下列表是需要进一步思考的事项
在 `ReadNorthwind` 和 `SaveNorthwind` 中使用多态性- 尝试使用 `IDataAdapter`
从 BO 中移除 `LastMessage` 并替换为状态码,这样所有文本和消息都集中在 GUI 中。- 解决 DBConcurrency 错误
添加一个公共 BO 类,就像 DO 类一样- 解决 FxCop 中的错误和警告。
- 删除网格中所有选定的行。
- 在 DO 中抛出自己的错误。
以下列表显示了可以添加到示例中的内容
添加一个带有并发检查的 SQL 示例添加 `Orders` 表以及客户和订单之间的关系- 添加客户详细信息窗体
- 在客户详细信息窗体中使用相同的 BO 和 DO
- 向表中添加 GUID 并展示如何创建自动增量
- 以不会出错的方式保存相关记录(父/子)
- 新父项
- 新子项
- 已更改的父项
- 已更改的子项
- 已删除的父项
- 已删除的子项
- 在 BO 中添加 `DataSet` 或 `DataTable` 的事件
结论
在我看来,代码可以将数据逻辑、业务逻辑和 GUI 分开到不同的类和 DLL 中。事实上,应该可以添加一个 Web 窗体,只需要少量代码。我希望(在您的帮助下)更新功能并优化代码。
版本历史
- v0.1.100
下载源代码 - 540 Kb。第一个版本
- v0.1.200
- 布局小改动
- 添加了更多代码注释
- 将所有状态消息从 BO 移动到 GUI
- v0.1.300
- 添加了 `DataProvider` 枚举器
- 添加了连接到 `Northwind` SQL 版本的代码
- 添加了连接到 Access、MS SQL (Ole) 和 MS SQL (SqlClient) 的连接字符串
- 向 `MultiTier.Data` 添加了一个接受表名的额外构造函数。
- v0.1.400
- 在加载和保存期间添加了等待光标。
- 添加了第一页、上一页、下一页和最后一页按钮及函数。
- 添加了一个删除按钮,用于删除当前行。
- 在所有项目中链接了程序集文件。
- v0.1.500
- 在 GUI 中添加了 `try` 和 `catch`(`ReadData` 和 `SaveData` 方法)
- 添加了项目 `MultiTier.Common`
- 删除了项目 `MultiTier.Data`
- 添加了一个公共 BO 类
- 添加了用于获取 `Orders` 表的 DO
- 在 `Customers` 和 `Orders` 之间添加了关系
- 在 BO 中为 `Orders` 添加了自动增量
- 更改了代码中的一些注释
修订历史
- 2004 年 3 月 10 日 - 新版本,新截图,`DataRelation`,自动增量。
- 2004 年 3 月 4 日 - 新版本,BO 或 DO 无变化。
- 2004 年 3 月 1 日 - 新版本,额外示例,`DBConcurrency`,解决了之前的问题。
- 2004 年 2 月 26 日 - 初始版本。