Skip to content

9. 数据库访问

9.1. 连接器 ADO.NET

让我们回顾一下多次使用的分层架构

在已研究的示例中,[dao]层目前利用了两种类型的数据源:

  • 硬编码在代码中的数据
  • 来自文本文件的数据

本章将探讨数据来自数据库的情况。此时,三层架构将演变为多层架构。此类架构有多种形式,我们将通过以下架构来研究其基本概念:

在上图架构中,[dao] [1] 层通过SGBD 专属的类库进行交互,该类库随 SGBD 一起提供。该层实现了被称为 ADO(Active X Data Objects)的标准功能。此类层被称为提供程序(此处指数据库访问提供程序)或连接器。 如今,大多数 SGBD 都配备了 ADO.NET 连接器,而在 .NET 平台初期并非如此。 .NET连接器未向[dao]层提供标准接口,因此该层代码中包含连接器的类名。 如果更换 SGBD,就意味着更换连接器和类,因此必须修改 [dao] 层。这既是一种高效的架构,因为连接器 .NET 专为某个特定的 SGBD 编写,因此能最大限度地利用该连接器;但同时也具有刚性,因为更换 SGBD 意味着必须更改 [dao] 层。 但这一观点需加以权衡:企业通常不会频繁更换 SGBD。此外,我们稍后将看到,自 .NET 2.0 版本以来,已推出一种通用连接器,它在不牺牲性能的前提下提供了灵活性。

9.2. 数据源的两种使用模式

.NET 平台支持通过两种不同方式利用数据源:

  1. 联机模式
  2. 脱机模式

联机模式下,应用程序

  1. 会与数据源建立连接
  2. 与数据源进行读写操作
  3. 关闭连接

脱机模式下,应用程序

  1. 与数据源建立连接
  2. 获取数据源全部或部分数据的内存副本
  3. 关闭连接
  4. 对数据的内存副本进行读写操作
  5. 工作完成后,建立连接,将修改后的数据发送至数据源以便其更新,然后关闭连接

本文仅探讨连接模式。

9.3. 数据库操作的基础概念

我们将以 SQL Server Compact 3.5 数据库为例,阐述数据库使用的主要概念。该 SGBD 数据库随 Visual Studio Express 一起提供。 这是一个轻量级的 SGBD,每次只能管理一个用户。不过,它足以用于入门数据库编程。后续我们将介绍其他 SGBD。

所采用的架构如下:

一个名为 [1] 的控制台应用程序将通过该 SGBD QZ 中的 Ado.Net 连接器,利用 SqlServer 类型的 Compact [3,4] 数据库XW2HTMLP003397ZQX。

9.3.1. 示例数据库

我们将直接在 Visual Studio Express 中构建数据库。为此,我们创建一个新的控制台项目。

  • [1]:该项目
  • [2]:打开“数据库资源管理器”视图
  • [3]:创建新连接
  • [4]:选择 SGBD 的类型
  • [5,6]:选择 SGBD SQL Server Compact
  • [7]:创建数据库
  • [8]:将一个 SQL Server Compact 数据库封装到一个后缀为 .sdf 的单一文件中。指定创建位置,此处为 C# 项目文件夹。
  • [9]:为新数据库命名为 [dbarticles.sdf]
  • [10]:选择法语。这会影响排序操作。
  • [11,12]:数据库可通过密码进行保护。此处为“dbarticles”。
  • [13]:确认信息页面。数据库将被实际创建:
  • [14]:刚刚创建的数据库名称
  • [15]:勾选“Save my password”选项,以免每次都需要重新输入
  • [16]:验证连接
  • [17]:一切正常
  • [18]:确认信息页面
  • [19]:连接信息显示在数据库浏览器中
  • [20]:目前数据库中尚无表。创建一个表。文章将包含以下字段:
    • id:唯一标识符——主键
    • nom:商品名称——唯一
    • prix:商品价格
    • stockactuel:当前库存
    • stockminimum:商品最低库存量(低于此值需补货)
  • [21]:字段 [id] 属于整数类型,并且是该表的主键 [22]。
  • [23]:该主键的类型为 Identity。 这一 SGBD SQL 服务器特有的概念表明,主键将由 SGBD 自身生成。在此,主键将是一个从 1 开始、每生成一个新键就递增 1 的整数。
  • [24]:其他字段已创建。需注意,字段[nom]具有唯一性约束 ,即必须与[25]不同。
  • [26]:为表命名
  • [27]:验证表结构后,该表将出现在数据库中。
  • [28]:查询表中的内容
  • [29]:目前表为空
  • [30]:向其中填入一些数据。当开始输入下一行时,当前行即被提交。字段 [id] 无需手动输入:该字段会在行提交时自动生成。

接下来需要配置项目,使当前位于项目根目录下的该数据库自动复制到项目的运行文件夹中:

  • [1]:请求查看所有文件
  • [2]:显示数据库[dbarticles.sdf]
  • [3]:将其纳入项目
  • [4]:向项目中添加数据源的操作会启动一个向导,但此处我们不需要该向导 [5]。
  • [6]:数据库现已加入项目。返回正常模式 [7]。
  • [8]:该项目及其数据库
  • [9]:在数据库属性中,可以看到[10],该数据库将自动复制到项目的运行文件夹中。我们即将编写的程序将从该位置读取数据。

既然现在已有可用数据库,我们就可以开始利用它了。在此之前,我们先回顾一下 SQL。

9.3.2. SQL语言的四个基本命令

SQL(结构化语言查询)是一种部分标准化的数据库查询和更新语言。 所有 SGBD 都遵循 SQL 的标准化部分,但同时为该语言添加了专有扩展,以利用 SGBD 的某些特殊特性。 我们已经遇到过两个例子:主键的自动生成以及表列的允许类型通常取决于SGBD。

我们介绍的SQL语言的四个基本命令是标准化的,并被所有SGBD所接受:

select col1, col2,... 
from table1, table2,...
where condition
order by expression
...
用于获取数据库中数据的查询语句。仅第一行的关键字为必填项,其余为可选项。此外还有其他未在此列出的关键字。
  1. 系统会将“from”关键字后面的所有表进行连接
  2. 仅保留位于 select 关键字后面的列
  3. 仅保留满足 where 关键字条件的行
  4. 根据 order by 关键字表达式排序后的行即构成查询结果。该结果是一个表。
insert into table(col1,col2, ...)
values (val1,val2, ...)
向表中插入一行。(col1, col2, ...) 指定要初始化为值 (val1, val2, ...) 的列。
update table
set col1=val1, col2=val2
where condition
更新满足条件的表行(若无where子句,则更新所有行)。对于这些行,col1列将接收值vali
delete from table
where condition
删除所有满足条件的表行

我们将编写一个控制台应用程序,用于向之前创建的数据库 [dbarticles] 发送 SQL 命令。以下是一个 执行示例。请读者理解所发送的 SQL 命令及其结果。

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;Persist Security Info=True]

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

Requête SQL (rien pour arrêter) : insert into articles(nom,prix,stockactuel,stockminimum) values('x',100,10,1)
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 100 10 1

Requête SQL (rien pour arrêter) : update articles set prix=prix*1.1 where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 110 10 1

Requête SQL (rien pour arrêter) : delete from articles where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
  • 第 1 行:连接字符串:其中包含连接数据库所需的所有参数。
  • 第 3 行:查询表 [articles] 的内容
  • 第16行:插入新行。需注意,此操作中字段id未被初始化,因为该字段的值将由SGBD生成。
  • 第 19 行:验证。第 28 行,确认该行已成功添加。
  • 第30行:将刚刚添加的商品价格提高10%。
  • 第 33 行:进行验证
  • 第 42 行:价格确实已上涨
  • 第44行:删除之前添加的商品
  • 第47行:验证
  • 第53-55行:该商品已不存在。

9.3.3. ADO.NET 连接模式的基础接口

让我们回到通过 ADO.NET 连接器调用数据库的应用程序架构:

连接模式下,该应用程序:

  1. 与数据源建立连接
  2. 对数据源进行读写操作
  3. 关闭连接

以下三个接口主要涉及这些操作:

  • IDbConnection,封装了连接的属性与方法。
  • IDbCommand,封装已执行的 SQL 命令的属性和方法。
  • IDataReader,封装了 SQL Select 命令结果的属性和方法。

接口 IDbConnection

用于管理与数据库的连接。我们将使用该接口中的以下方法 M 和属性 P

名称
类型
作用
ConnectionString
P
数据库连接字符串。它指定了与特定数据库建立连接所需的所有参数。
Open
M
建立与由 ConnectionString 定义的数据库的连接
Close
M
关闭连接
BeginTransaction
M
开始事务。
State
P
连接状态:ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken

如果 Connection 是实现接口 IDbConnection 的类,则可以按以下方式建立连接:

1
2
3
IDbConnection connexion=new Connection();
connexion.ConnectionString=...;
connexion.Open();

接口 IDbCommand

用于执行 SQL 命令或存储过程。我们将使用该接口的以下 M 方法和 P 属性:

名称
类型
作用
CommandType
P
指定要执行的内容 - 其值取自一个枚举:
- CommandType.Text:执行在属性 CommandText 中定义的命令 SQL。这是默认值。
- CommandType.StoredProcedure:执行数据库中的存储过程
CommandText
P
- 若 CommandType= CommandType.Text,则要执行的命令 SQL 的文本
- 若 CommandType= CommandType.StoredProcedure,则要执行的存储过程名称
Connection
P
用于执行命令 SQL 的连接 IDbConnection
Transaction
P
用于执行命令 SQL 的交易 IDbTransaction
Parameters
P
已配置的 SQL 命令的参数列表。命令 update articles set price=price*1.1 where id=@id 具有参数 @id。
ExecuteReader
M
用于执行命令 SQL Select。将获得一个对象 IDataReader,该对象表示 Select 的结果。
ExecuteNonQuery
M
用于执行SQL更新、插入、删除命令。该命令会返回受操作影响的行数(更新、插入、删除的行数)。
ExecuteScalar
M
用于执行 SQL 命令,而 Select 仅返回单一结果,例如:select count(*) from articles
CreateParameter
M
用于创建已配置的 SQL 命令的参数 IDbParameter
Prepare
M
用于优化带参数查询的执行,当该查询使用不同的参数多次执行时。

如果 Command 是实现接口 IDbCommand 的类,则不带事务的 SQL 命令的执行形式如下:

// 建立连接 
IDbConnection connexion=...
connexion.Open();
// 准备命令
IDbCommand commande=new Command();
commande.Connection=connexion;
// 执行 select 语句
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// 执行 update、insert、delete 命令
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// 关闭连接
connexion.Close();

接口 IDataReader

用于封装 SQL 和 Select 命令的结果。 一个 IDataReader 对象表示一个包含行和列的表格,需按顺序处理:先处理第一行,然后是第二行,……我们将使用该接口的以下方法 M 和属性 P

名称
类型
作用
FieldCount
P
表的列数IDataReader
GetName
M
GetName(i) 返回表 IDataReader 中第 i 列的名称。
Item
P
Item[i] 代表表 IDataReader 中当前行的第 i 列。
Read
M
跳转到表 IDataReader 的下一行。如果读取成功,则返回布尔值 True;否则返回 False
Close
M
关闭表 IDataReader
GetBoolean
M
GetBoolean(i):返回表 IDataReader 中当前行第 i 列的布尔值。 其他类似的方法如下:GetDateTimeGetDecimalGetDoubleGetFloatGetInt16GetInt32GetInt64GetString
Getvalue
M
Getvalue(i):将表 IDataReader 中当前行第 i 列的值转换为类型 object
IsDBNull
M
IsDBNull(i) 若表 IDataReader 当前行的第 i 列没有值(由值 SQL NULL 表示),则返回 True

IDataReader 对象的查询通常如下所示:

// 建立连接 
IDbConnection connexion=...
connexion.Open();
// 命令准备
IDbCommand commande=new Command();
commande.Connection=connexion;
// 执行select命令
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// 结果处理
while(reader.Read()){
     // 处理当前行
        ...
}
// 关闭读取器
reader.Close();
// 关闭连接
connexion.Close();

9.3.4. 错误处理

让我们回顾一下基于数据库的应用程序架构:

[dao]层在操作数据库时可能会遇到多种错误。这些错误将作为由连接器ADO.NET抛出的异常上报。 [dao] 层的代码必须处理这些异常。所有数据库操作都必须在 try / catch / finally 块中进行,以便捕获并处理可能发生的异常,并释放相应的资源。因此,上文中用于处理 Select 命令结果的代码将变为如下形式:

// 初始化连接
IDbConnection connexion=...
// 连接处理
try{
     // 打开
    connexion.Open();
     // 命令准备
    IDbCommand commande=new Command();
    commande.Connection=connexion;
     // 执行 select 命令
    commande.CommandText="select ...";
    IDbDataReader reader=commande.ExecuteReader();
     // 结果处理
    try{
        while(reader.Read()){
         // 处理当前行
        ...
    }finally{
         // 关闭读取器
        reader.Close();
    }
}catch(Exception ex){
     // 异常处理
    ...
}finally{
     // 关闭连接
    connexion.Close();
}
...

无论发生何种情况,对象 IDataReaderIDbConnection 都必须被关闭。因此,该关闭操作在 finally 语句中完成。

连接的关闭以及对象 IDataReader 的关闭可通过 using 子句实现自动化:

// 连接处理
try{
    using(IDbConnection connexion=...){
         // 打开
        connexion.Open();
         // 订单准备
        IDbCommand commande=new Command();
        commande.Connection=connexion;
         // 执行选择指令
        commande.CommandText="select ...";
        using(IDbDataReader reader=commande.ExecuteReader()){
             // 结果处理
            while(reader.Read()){
             // 处理当前行
            ...
        }// 使用 IData
    }//使用 IDbConnection
}catch(Exception ex){
     // 异常处理
    ...
}
..
  • 第 3 行,using 子句确保在 using(...){...} 代码块中打开的连接将在该代码块之外关闭,无论是以正常方式还是因异常抛出而退出代码块。 虽然节省了一个 finally,但重点并不在于这点微小的节省。使用 using 可以避免开发人员手动关闭连接。 然而,忘记关闭连接可能不会被察觉,并会在 SGBD 达到其所能支持的最大开放连接数时,以看似随机的方式导致应用程序“崩溃”。
  • 第 11 行:采用类似方法关闭 IDataReader 对象。

9.3.5. 示例项目配置

最终项目结构如下:

  • [1]:该项目将包含一个配置文件[App.config]
  • [2]:该项目使用两个默认未引用的DLL类,因此需将其添加到项目引用中:
    • [System.Configuration] 用于处理配置文件 [App.config]
    • [System.Data.SqlServerCe]:用于调用 Sql Server Compact 数据库
  • [3, 4]:说明如何向项目添加引用。
  • [5, 6]:回顾如何将文件 [App.config] 添加到项目中。

配置文件 [App.config] 如下所示:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
  <add name="dbSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
 </connectionStrings>
</configuration>
  • 第 3-5 行:复数形式的 <connectionStrings> 标签定义了数据库连接字符串。连接字符串的格式为 "参数1=值1;参数2=值2;..."。 它定义了与特定数据库建立连接所需的所有参数。这些连接字符串会随着每个 SGBD 而变化。[http://www.connectionstrings.com/] 网站提供了主要 SGBD 的连接字符串格式。
  • 第4行:定义了一个特定的连接字符串,此处为之前创建的SQL Server Compact dbarticles.sdf数据库的连接字符串:
    • name = 连接字符串的名称。C# 程序正是通过此名称来获取连接字符串的
    • connectionString:SQL Server Compact 数据库的连接字符串
    • DataSource:指定数据库路径。语法 |DataDirectory| 表示项目的运行目录。
    • Password:数据库密码。若无密码,则此参数省略。

用于获取先前连接字符串的 C# 代码如下:


string connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
  • ConfigurationManager 是 DLL [System.Configuration] 的类,用于处理 [App.config] 文件。
  • ConnectionsStrings["nom"]。ConnectionString:指< add name="nom" connectionString="..."> 标签的 connectionStrings 属性

项目现已配置完成。接下来我们将研究 [Program.cs] 类,此前我们已看过该类的运行示例。

9.3.6. 示例程序

程序 [program.cs] 如下:


using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Text;
using System.Text.RegularExpressions;
using System.Configuration;

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {

            // 控制台应用程序 - 执行通过键盘输入的 SQL 请求
            // 连接数据库,其连接字符串从配置文件中获取

            // 配置文件处理 [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // 显示连接字符串
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // 构建可接受的 SQL 命令词典
            string[] commandesSQL = new string[] { "select", "insert", "update", "delete" };
            Dictionary<string, bool> dicoCommandes = new Dictionary<string, bool>();
            for (int i = 0; i < commandesSQL.Length; i++) {
                dicoCommandes.Add(commandesSQL[i], true);
            }

            // 读取并执行通过键盘输入的命令 SQL
            string requête = null;    // 查询文本 SQL
            string[] champs;            // 查询字段    
            Regex modèle = new Regex(@"\s+");    // 连续空格

            // 键盘输入的 SQL 命令的输入-执行循环
            while (true) {
                // 查询请求
                Console.Write("\nRequête SQL (rien pour arrêter) : ");
                requête = Console.ReadLine().Trim().ToLower();
                // 结束了吗?
                if (requête == "")
                    break;
                // 将查询拆分为字段
                champs = modèle.Split(requête);
                // 请求有效吗?
                if (champs.Length == 0 || ! dicoCommandes.ContainsKey(champs[0])) {
                    // 错误信息
                    Console.WriteLine("Requête invalide. Utilisez select, insert, update, delete ou rien pour arrêter");
                    // 下一个请求
                    continue;
                }
                // 执行请求
                if (champs[0] == "select") {
                    ExecuteSelect(connectionString, requête);
                } else
                    ExecuteUpdate(connectionString, requête);
            }
        }

        // 执行更新请求
        static void ExecuteUpdate(string connectionString, string requête) {
    ...
        }

        // 正在执行 Select 查询
        static void ExecuteSelect(string connectionString, string requête) {
....
        }
    }
}
  • 第 1-6 行:应用程序中使用的命名空间。管理 SQL Server Compact 数据库需要第 3 行中的 [System.Data.SqlServerCe] 命名空间。 此处存在对 SGBD 专有命名空间的依赖。由此可推断,若更换 SGBD,则需修改程序。
  • 第 18 行:从文件 [App.config] 中读取数据库连接字符串,并在第 25 行显示。该字符串将用于建立与数据库的连接。
  • 第28-32行:一个存储四个授权命令名称的字典:select、insert、update、delete。
  • 第 40-62 行:用于处理用户通过键盘输入的 SQL 命令并将其执行于数据库的循环
  • 第48行:将键盘输入的行拆分为各个字段,以确定其第一个参数,该参数必须为:select、insert、update、delete
  • 第50-55行:如果查询无效,则显示错误信息并转到下一条查询。
  • 第57-61行:执行输入的命令SQL。 根据处理的是 select 命令,还是 insertupdatedelete 命令,其执行形式有所不同。 在前一种情况下,该命令从数据库中检索数据而不对其进行修改;在后一种情况下,它更新数据库而不检索数据。在两种情况下,执行任务都委托给一个需要两个参数的方法:
    • 用于连接数据库的连接字符串
    • 要在该连接上执行的命令 SQL

9.3.7. 执行 SELECT 查询

执行 SQL 命令需要以下步骤:

  1. 连接数据库
  2. 向数据库发送 SQL 命令
  3. 处理 SQL 命令的结果
  4. 关闭连接

步骤 2 和 3 会反复执行,而连接关闭仅在数据库处理结束时进行。打开的连接是 SGBD 的有限资源。 必须节约这些资源。因此,应始终设法限制开放连接的存活时间。在所研究的示例中,每次执行 SQL 命令后都会关闭连接。随后为下一个 SQL 命令打开新的连接。连接的打开/关闭操作成本较高。 为降低此成本,某些 SGBD 系统引入了“已建立连接池”的概念:在应用程序初始化时,会建立 N 个连接并分配至该池中。这些连接将保持打开状态直至应用程序结束。 当应用程序建立连接时,它会从池中获取 N 个已建立连接中的一个。当它关闭连接时,该连接会直接放回池中。该系统的优势在于对开发者而言是透明的:无需修改程序即可使用连接池。 连接池的配置取决于 SGBD。

我们首先关注 SQL 和 Select 命令的执行。示例程序中的 ExecuteSelect 方法如下:


// 执行 Select 查询
        static void ExecuteSelect(string connectionString, string requête) {
            // 处理可能出现的异常
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                    // 建立连接
                    connexion.Open();
                    // 执行 sqlCommand 并执行 SELECT 查询
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    SqlCeDataReader reader= sqlCommand.ExecuteReader();
                    // 显示结果
                    AfficheReader(reader);
                }
            } catch (Exception ex) {
                // 错误消息
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // 显示读取器
        static void AfficheReader(IDataReader reader) {
...
        }
  • 第 2 行:该方法接收两个参数:
    • 连接字符串 [connectionString],用于连接数据库
    • 要在该连接上执行的命令 SQL Select [requête]
  • 第4行:任何数据库操作都可能引发需要处理的异常。这一点在此尤为重要,因为用户提供的SQL命令可能存在语法错误。我们需要能够向用户反馈这一情况。因此,所有代码都应置于try/catch块内
  • 第5行:此处包含多项内容:
    • 使用连接字符串 [connectionString] 初始化了数据库连接。此时连接尚未建立,将在第 7 行建立。
    • using (Ressource) {...} 语句是一种语法便利机制,可确保在退出由 using 控制的代码块时,释放资源 Ressource(此处指连接)。
    • 连接属于专有类型:SqlCeConnection,专用于 SGBD SQL Server Compact。
  • 第7行:连接已建立。此时将使用连接字符串中的参数。
  • 第 9 行:通过专有对象 SqlCeCommand 发出 SQL 命令。第 9 行使用两项信息初始化该对象:要使用的连接以及要在其上发出的 SQL 命令。 对象 SqlCeCommand 既可用于执行 Select 命令,也可用于执行 UpdateInsertDelete 命令。 其属性和方法已在第 9.3.3 节中介绍。
  • 第 10 行:通过对象 SQL 的方法 Select 执行命令 ExecuteReader,该方法返回一个 IDataReader 对象,其方法和属性已在第 9.3.3 节中介绍。
  • 第12行:结果的显示由以下AfficheReader方法负责:

        // 显示阅读器
        static void AfficheReader(IDataReader reader) {
            using (reader) {
                // 结果分析
                // -- 列
                StringBuilder ligne = new StringBuilder();
                int i;
                for (i = 0; i < reader.FieldCount - 1; i++) {
                    ligne.Append(reader.GetName(i)).Append(",");
                }
                ligne.Append(reader.GetName(i));
                Console.WriteLine("\n{0}\n{1}\n{2}\n", "".PadLeft(ligne.Length, '-'), ligne, "".PadLeft(ligne.Length, '-'));
                // -- 数据
                while (reader.Read()) {
                    // 当前行处理
                    ligne = new StringBuilder();
                    for (i = 0; i < reader.FieldCount; i++) {
                        ligne.Append(reader[i].ToString()).Append(" ");
                    }
                    Console.WriteLine(ligne);
                }
            }
}
  • 第 2 行:该方法接收一个 IDataReader 对象。请注意,此处我们使用的是接口而非特定类。
  • 第 3 行:使用 using 子句来自动管理 IDataReader 对象的关闭。
  • 第8-10行:显示Select结果表的列名。这些是查询“select col1, col2, ... from table ...”中的coli
  • 第14-21行:遍历结果表,并显示表中每一行的值。
  • 第 18 行:由于不知道查询的表,因此无法确定结果中第 i 列的类型。因此无法使用语法 reader.GetXXX(i)(其中 XXX 是第 i 列的类型),因为该类型未知。 因此,需使用语法 reader.Item[i].ToString(),以字符串形式获取第 i 列的表示。 语法 reader.Item[i].ToString() 可以简写为 reader[i].ToString()。

9.3.8. 执行更新命令:INSERT、UPDATE、DELETE

方法 ExecuteUpdate 的代码如下:


        // 执行更新请求
        static void ExecuteUpdate(string connectionString, string requête) {
            // 处理可能出现的异常
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                    // 建立连接
                    connexion.Open();
                    // 执行 sqlCommand 并附带更新请求
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // 显示结果
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                // 错误消息
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
}

我们曾提到,执行查询命令 Select 与执行更新命令 Update 并无区别, InsertDelete 的区别仅在于所使用的 SqlCeCommand 对象的方法:ExecuteReader 对应 Select, ExecuteNonQuery 对应 UpdateInsertDelete。我们仅对上述代码中的最后一种方法进行说明:

  • 第 10 行: 由对象 SqlCeCommand 的方法 ExecuteNonQuery 执行命令 UpdateInsertDelete。 如果执行成功,该方法将返回更新(update)、插入(insert)或删除(delete)的行数。
  • 第 12 行:该行数将显示在屏幕上

建议读者参阅第 9.3.2 节中的该代码执行示例。

9.4. 其他连接器 ADO.NET

我们研究的代码是专有的:它依赖于 [System.Data.SqlServerCe] 命名空间,该命名空间专用于 SGBD SQL Server Compact。 现在我们将使用不同的 .NET 连接器构建相同的程序,并观察有何变化。

9.4.1. SQL Server 2005 连接器

将采用以下架构:

SQL Server 2005 的安装说明详见附录第 1.1 节。

我们在与之前相同的解决方案中创建第二个项目,然后创建 SQL Server 2005 数据库。在执行以下操作之前,必须先启动 SGBD SQL Server 2005:

  • [1]:在当前解决方案中创建新项目,并将其设为当前项目。
  • [2]:创建新连接
  • [3]:选择连接类型
  • [4]:选择 SGBD SQL 服务器
  • [5]:上一步选择的结果
  • [6]:使用按钮 [Browse] 指定创建 SQL Server 2005 数据库的位置。该数据库封装在一个 .mdf 文件中。
  • [7]:选择新项目的根目录,并将数据库命名为 [dbarticles.mdf]。
  • [8]:使用 Windows 身份验证。
  • [9]:确认信息页面
  • [11]:SQL Server 数据库
  • [12]:创建一个表。该表将与之前构建的 SQL Server Compact 数据库完全相同。
  • [13]:字段 [id]
  • [14]:字段 [id] 的类型为 Identity
  • [15,16]:字段 [id] 是主键
  • [17]:表中的其他字段
  • [18]:在保存表格时(Ctrl+S),将其命名为 [articles]。

接下来我们需要向表中插入数据:

我们将数据库添加到项目中:

该项目的引用如下:

配置文件 [App.config] 如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
  <add name="connectString1" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True;" />
     <add name="connectString2" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;Connect Timeout=30;" />
 </connectionStrings>
</configuration>
  • 第 4 行:连接数据库 [dbarticles.mdf] 的连接字符串,采用 Windows 身份验证
  • 第5行:连接数据库[dbarticles.mdf]的连接字符串,采用SQL Server身份验证。 [sa,msde] 是 SQL Server 服务器管理员的 (登录名, 密码) 对,如第 1.1 节所定义。

程序 [Program.cs] 的演变过程如下:


using System.Data.SqlClient;
...

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {
...
            // 处理配置文件 [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["connectString2"].ConnectionString;
            } catch (Exception e) {
...
            }
...
            // 读取并执行通过键盘输入的命令 SQL
...
        }

        // 执行更新请求
        static void ExecuteUpdate(string connectionString, string requête) {
            // 处理可能出现的异常
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                    // 建立连接
                    connexion.Open();
                    // 执行 sqlCommand 并发送更新请求
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // 显示结果
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
....
            }
        }

        // 执行 Select 查询
        static void ExecuteSelect(string connectionString, string requête) {
            // 处理可能出现的异常
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                    // 建立连接
                    connexion.Open();
                    // 执行 sqlCommand 并执行 SELECT 查询
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    SqlDataReader reader = sqlCommand.ExecuteReader();
                    // 处理结果
...
                }
            } catch (Exception ex) {
...
            }
        }
    }
}
  • 第 1 行:命名空间 [System.Data.SqlClient] 包含用于管理 SQL Server 2005 数据库的类
  • 第 24 行:连接类型为 SQLConnection
  • 第 28 行:封装 SQL 语句的对象类型为 SQLCommand
  • 第 47 行:封装 SQL Select 命令结果的对象类型为 SQLDataReader

该代码与 SGBD SQL Server Compact 所用的代码完全相同,仅类名不同。 要执行它,可以使用(第 11 行)在 [App.config] 中定义的两个连接字符串中的任意一个。

9.4.2. 连接器 MySQL5

将采用以下架构:

MySQL5 的安装说明详见附录第 1.2 节,Ado.Net 连接器的安装说明详见第 1.2.5 节。

我们在与之前相同的解决方案中创建第三个项目,并为其添加所需的引用:

  • [1]:新项目
  • [2]:向其添加引用
  • [3]: DLL、[MySQL.Data]、Ado.Net、MySql5 以及 [System.Configuration]、[4]。

现在我们创建数据库 [dbarticles] 及其表 [articles]。 必须启动 SGBD 和 MySQL5。此外,还需启动客户端 [Query Browser](参见第 1.2.3 节)。

  • [1]: 在 [Query Browser] 中,右键单击 [Schemata] [2] 区域,以创建 [3] 这个新模式(该术语指代数据库)。
  • [4]:该数据库将命名为 [dbarticles]。在 [5] 中可以看到它。目前该数据库尚无表。我们将执行以下脚本 SQL:
/* 选择当前数据库 */
USE dbarticles;
/* 创建商品表 */
CREATE TABLE ARTICLES (
    ID            INTEGER PRIMARY KEY AUTO_INCREMENT,
    NOM           VARCHAR(20) NOT NULL,
    PRIX          DOUBLE PRECISION NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);
/* 向表中插入数据 */
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article1', 100, 10, 1);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article2', 200, 20, 2);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article3', 300, 30, 3);
/* 添加约束 */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
  • 第 1 行:数据库 [dbarticles] 成为当前数据库。后续的 SQL 命令将在该数据库上执行。
  • 第 4-10 行:定义表 [ARTICLES]。请注意,SQL 是 MySQL 的所有者。 列类型以及主键的自动生成(属性 AUTO_INCREMENT)与 SGBD 和 SQL(Server Compact 和 Express 版本)中的情况有所不同。
  • 第 12-14 行:插入三行
  • 第 16-21 行:为列添加完整性约束。

该脚本在 [MySQL Query Browser] 中执行:

  • 在 [MySQL Query Browser] [6] 中,加载脚本 [7]。在 [8] 中可见该脚本。在 [9] 中,该脚本被执行。
  • 在 [10] 阶段,已创建表 [articles]。 双击该表。此时将弹出窗口 [11],其中包含查询 [12],该查询已准备好由 [13] 执行。 在 [14] 中,显示了执行结果。确实得到了预期的三行数据。请注意,[ID] 字段的值是自动生成的(该字段的 AUTO_INCREMENT 属性)。

现在数据库已准备就绪,我们可以回到 Visual Studio 继续应用程序的开发。

在 [1] 中,包含程序 [Program.cs] 和配置文件 [App.config]。该配置文件内容如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
 </connectionStrings>
</configuration>

第4行,连接字符串的各项内容如下:

  • Server:运行 SGBD、MySQL 的机器名称,此处为 localhost、c.a.d。即程序将被执行的机器。
  • Database:所管理的数据库名称,此处为 dbarticles
  • Uid:用户登录名,此处为 root
  • Pwd:其密码,此处为 root。这两项信息指代第 1.2 节中创建的管理员。

程序 [Program.cs] 与先前版本基本相同,仅在以下细节上有所不同:

espace de noms
MySql.Data.MySqlClient
classe Connection
MySqlConnection
classe Command
MySqlCommand
classe DataReader
MySqlDataReader

该程序使用了文件 [App.config] 中名为 dbArticlesMySql5 的连接字符串。运行结果如下:

Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

9.4.3. 连接器 ODBC

将采用以下架构:

ODBC连接器的优势在于,它们为使用它们的应用程序提供了一个标准接口。 因此,新应用程序只需使用单一代码,即可与所有配备 ODBC、c.a.d 连接器的 SGBD 进行交互,包括绝大多数 SGBD。 ODBC连接器的性能不如那些能够充分利用特定SGBD所有特性的“专有”连接器。 作为回报,应用程序获得了极大的灵活性:无需更改代码即可更换 SGBD。

我们研究了一个示例,其中应用程序根据给定的连接字符串,使用 MySQL5 数据库或 SQL Server Express 数据库。在下文中,我们假设:

  • SGBD、SQL Server Express 和 MySQL5 已启动
  • ODBC 驱动程序已安装在机器上(参见第 1.2.6 节)。SQL Server 2005 的驱动程序默认已存在。
  • 所使用的数据库为:MySQL5 数据库采用第 9.4.2 节所述配置,SQL Server Express 数据库采用第 9.4.1 节所述配置。

新的 Visual Studio 项目如下:

如上所述,第 9.4.1 节中创建的 SQL Server [dbarticles.mdf] 数据库已复制到项目文件夹中。

配置文件 [App.config] 如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;" />
     <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
 </connectionStrings>
</configuration>
  • 第4行:源连接字符串 ODBC MySQL5。这是之前已分析过的字符串,其中新增了一个 Driver 参数,用于指定要使用的驱动程序 ODBC。
  • 第 5 行:源 ODBC SQL Server Express 的连接字符串。这是之前示例中已使用的字符串,其中添加了 Driver 参数。

程序 [Program.cs] 与先前版本基本相同,仅在以下细节上有所不同:

espace de noms
System.Data.Odbc
classe Connection
OdbcConnection
classe Command
OdbcCommand
classe DataReader
OdbcDataReader

该程序使用文件 [App.config] 中定义的两个连接字符串之一。运行结果如下:

使用连接字符串 [dbArticlesOdbcSqlServer2005]:

Chaîne de connexion à la base : [Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 500,0000 10 5
2 pompe 10,0000 10 2
3 arc 610,0000 4 1
4 flèches - lot de 6 100,0000 12 20
5 combinaison de plongée 300,0000 8 2
6 Bouteilles d'oxygène 120,0000 10 5

使用连接字符串 [dbArticlesOdbcMySql5]:

Chaîne de connexion à la base : [Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

9.4.4. 连接器 OLE DB

将采用以下架构:

与 ODBC 连接器一样,OLE、DB(对象链接与嵌入 DataBase)连接器向使用它们的应用程序提供标准接口。 ODBC驱动程序支持访问数据库。而OLE和DB驱动程序的数据源更为多样:包括数据库、邮件系统、通讯录等。 只要软件开发商决定,任何数据源都可以成为 Ole 驱动程序 DB 的对象。因此,我们可以以标准方式访问各种各样的数据。

我们研究一个示例:应用程序根据给定的连接字符串,调用 ACCESS 数据库或 SQL Server Express 数据库。 下文中,我们假设 SGBD SQL Server Express 已启动,且所用数据库与前例相同。

新的 Visual Studio 项目如下:

  • 在 [1] 中:连接器 OLE 和 DB 所需的命名空间为 [System.Data.OleDb],该命名空间存在于上述引用 [System.Data] 中。 数据库 SQL Server [dbarticles.mdf] 是从前一个项目复制而来的。数据库 [dbarticles.mdb] 是使用 Access 创建的。
  • 在 [2] 中:与 SQL 服务器数据库一样,ACCESS 数据库具有 [Copy to Output Directory=Copy Always] 属性,以便其自动复制到项目的运行文件夹中。

ACCESS [dbarticles.mdb] 数据库如下:

在 [1] 中,包含表 [articles] 的结构,而在 [2] 中包含其内容。

配置文件 [App.config] 如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;"/>
     <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
 </connectionStrings>
</configuration>
  • 第 4 行:源连接字符串 OLE DB ACCESS。 其中包含 Provider 参数,该参数定义了要使用的驱动程序 OLE DB 以及数据库路径
  • 第 5 行:OLE DB Server Express 源的连接字符串。

程序 [Program.cs] 与先前版本基本相同,仅在以下细节上有所不同:

espace de noms
System.Data.OleDb
classe Connection
OleDbConnection
classe Command
OleDbCommand
classe DataReader
OleDbDataReader

该程序使用文件 [App.config] 中定义的两个连接字符串之一。使用连接字符串 [dbArticlesOleDbAccess] 运行时,结果如下:

Chaîne de connexion à la base : [Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 1202 5 2
2 arc 5000 10 2
3 canoé 1502 12 6
4 fusil 3000 10 2
5 skis nautiques 1800 5 2
6 essai3 3 3 3
7 cachalot 200000 1 0
8 léopard 500000 1 1
9 panthère 800000 1 1

9.4.5. 通用连接器

将采用以下架构:

与 ODBC、OLE 和 DB 连接器一样,通用连接器为使用它的应用程序提供了一个标准接口,同时在不牺牲灵活性的前提下提升了性能。 实际上,通用连接器基于 SGBD 的专有连接器。应用程序使用通用连接器的类。这些类充当应用程序与专有连接器之间的中介。

如上所述,当应用程序请求连接通用连接器时,该连接器会返回一个 IDbConnection 实例, 该连接接口在第9.3.3节中已有描述,具体MySQLConnection或SQLConnection类实现,具体取决于所接收请求的性质。 通常说通用连接器具有工厂类:通过工厂类请求其创建对象并返回对象引用(指针)。因此得名(factory=工厂,即对象生产工厂)。

目前尚无适用于所有 SGBD 的通用连接器(2008年4月)。若要查询某台机器上已安装的连接器,可使用以下程序:


using System;
using System.Data;
using System.Data.Common;

namespace Chap7 {
    class Providers {
        public static void Main() {
            DataTable dt = DbProviderFactories.GetFactoryClasses();
            foreach (DataColumn col in dt.Columns) {
                Console.Write("{0}|", col.ColumnName);
            }
            Console.WriteLine("\n".PadRight(40, '-'));
            foreach (DataRow row in dt.Rows) {
                foreach (object item in row.ItemArray) {
                    Console.Write("{0}|", item);
                }
                Console.WriteLine("\n".PadRight(40, '-'));
            }
        }
    }
}
  • 第8行:静态方法[DbProviderFactories.GetFactoryClasses()]返回已安装的通用连接器列表,该列表以内存中数据库表的形式存储(DataTable)。
  • 第9-11行:显示表dt的列名:
    • dt.Columns 是该表的列列表。列 C 的类型为 DataColumn
    • [DataColumn]。ColumnName是该列的名称
  • 第13-18行:显示表的行 dt
    • dt.Rows 是该表的行列表。行 L 的类型为 DataRow
    • [DataRow]。ItemArray 是一个对象数组,其中每个对象代表该行的一列

在我的机器上执行的结果如下:

Name|Description|InvariantName|AssemblyQualifiedName|
---------------------------------------
Odbc Data Provider|.Net Framework Data Provider for Odbc|System.Data.Odbc|System.Data.Odbc.OdbcFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OleDb Data Provider|.Net Framework Data Provider for OleDb|System.Data.OleDb|System.Data.OleDb.OleDbFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OracleClient Data Provider|.Net Framework Data Provider for Oracle|System.Data.OracleClient|System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
SqlClient Data Provider|.Net Framework Data Provider for SqlServer|System.Data.SqlClient|System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
Microsoft SQL Server Compact Data Provider|.NET Framework Data Provider for Microsoft SQL Server Compact|System.Data.SqlServerCe.3.5|System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=3.5.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91|
---------------------------------------
MySQL Data Provider|.Net Framework Data Provider for MySQL|MySql.Data.MySqlClient|MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=5.2.1.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d|
  • 第 1 行:该表有四列。其中前三列对我们来说最为有用。

下图显示了以下通用连接器:

名称
标识符
Odbc Data Provider
System.Data.Odbc
OleDb Data Provider
System.Data.OleDb
OracleClient Data Provider
System.Data.OracleClient
SqlClient Data Provider
System.Data.SqlClient
Microsoft SQL Server Compact Data Provider
System.Data.SqlServerCe.3.5
MySQL Data Provider
MySql.Data.MySqlClient

在 C# 程序中,可以通过其标识符访问通用连接器。

我们研究一个示例,其中应用程序利用了迄今为止我们构建的各种数据库。该应用程序将接收两个参数:

  • 第一个参数指定所使用的 SGBD 类型,以便调用正确的类库
  • 第二个参数通过连接字符串指定要管理的数据库。

新的 Visual Studio 项目如下:

  • 在 [1] 中:通用连接器所需的命名空间为 [System.Data.common],该命名空间位于引用 [System.Data] 中。

配置文件 [App.config] 如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
        <add name="dbArticlesSqlServer" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
        <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;Option=3;" />
        <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;Persist Security Info=True"/>
    </connectionStrings>
    <appSettings>
        <add key="factorySqlServerCe" value="System.Data.SqlServerCe.3.5"/>
        <add key="factoryMySql" value="MySql.Data.MySqlClient"/>
        <add key="factorySqlServer" value="System.Data.SqlClient"/>
        <add key="factoryOdbc" value="System.Data.Odbc"/>
        <add key="factoryOleDb" value="System.Data.OleDb"/>
    </appSettings>
</configuration>
  • 第 3-11 行:所用各数据库的连接字符串。
  • 第 13-17 行:要使用的通用连接器名称

程序 [Program.cs] 如下:


...
using System.Data.Common;

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {

            // 控制台应用程序——执行通过键盘输入的 SQL 查询
            // 连接的数据库其连接字符串及关联的 SGBD 连接器的名称均来自配置文件

            // 参数验证
            if (args.Length != 2) {
                Console.WriteLine("Syntaxe : pg factory connectionString");
                return;
            }

            // 配置文件处理
            string factory = null;
            string connectionString = null;
            DbProviderFactory connecteur = null;
            try {
                // 初始化
                factory = ConfigurationManager.AppSettings[args[0]];
                // 连接字符串
                connectionString = ConfigurationManager.ConnectionStrings[args[1]].ConnectionString;
                // 获取用于 SGBD 的通用连接器
                connecteur = DbProviderFactories.GetFactory(factory);
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // 显示
            Console.WriteLine("Provider factory : [{0}]\n", factory);
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

...
                // 执行查询
                if (champs[0] == "select") {
                    ExecuteSelect(connecteur,connectionString, requête);
                } else
                    ExecuteUpdate(connecteur, connectionString, requête);
            }
        }

        // 执行更新请求
        static void ExecuteUpdate(DbProviderFactory connecteur, string connectionString, string requête) {
            // 处理可能出现的异常
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // 连接配置
                    connexion.ConnectionString = connectionString;
                    // 建立连接
                    connexion.Open();
                    // 配置命令 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // 执行请求
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // 显示结果
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                // 错误消息
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // 执行 Select 查询
        static void ExecuteSelect(DbProviderFactory connecteur, string connectionString, string requête) {
            // 处理可能出现的异常
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // 连接配置
                    connexion.ConnectionString = connectionString;
                    // 建立连接
                    connexion.Open();
                    // 配置命令 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // 执行查询
                    DbDataReader reader = sqlCommand.ExecuteReader();
                    // 显示结果
...
                }
            } catch (Exception ex) {
                // 错误消息
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
    }
}
  • 第12-14行:应用程序接收两个参数:通用连接器的名称以及数据库连接字符串,这些参数以[App.config]文件中的键值形式提供。
  • 第 23、25 行:从 [App.config] 中获取通用连接器的名称以及连接字符串
  • 第27行:泛型连接器被实例化。从这一刻起,它便与特定的SGBD相关联。
  • 第39-43行:将键盘输入的命令SQL的执行委托给两个方法,并向其传递:
    • 待执行的查询
    • 标识将执行该查询的数据库的连接字符串
    • 用于标识与管理该数据库的 SGBD 进行交互的通用连接器的信息。
  • 第 50-54 行:通过通用连接器的 CreateConnection 方法(第 50 行)建立连接,然后使用待管理数据库的连接字符串进行配置(第 52 行)。随后打开该连接(第 54 行)。
  • 第 56-58 行:使用通用连接器的 CreateCommand 方法创建执行 SQL 命令所需的 Command 对象。 随后,使用待执行的 SQL 命令的文本(第 57 行)以及执行该命令的连接(第 58 行)对其进行配置。
  • 第 60 行:执行更新命令 SQL
  • 第 74-87 行:包含类似的代码。 新内容出现在第84行。通过执行命令Select获得的对象Reader属于类型DbDataReader,其用法与我们之前遇到的对象OleDbDataReaderOdbcDataReader 等对象,

以下是几个执行示例。

以 MySQL5 数据库为例:

打开项目 [1] 的属性页面,并选择 [Debug] [2] 选项卡。 在 [3] 中,[App.config] 第 14 行连接器的键。 在 [4] 中,[App.config] 第 6 行连接字符串的键。执行结果如下:

Provider factory : [MySql.Data.MySqlClient]
Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

使用 SQL Server Compact 数据库:

在 [1] 中,这是 [App.config] 第 13 行连接器的键。 在 [2] 中,这是 [App.config] 第 4 行连接字符串的键。执行结果如下:

Provider factory : [System.Data.SqlServerCe.3.5]
Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

请读者尝试测试其他数据库。

9.4.6. 该选择哪种连接器?

让我们回到基于数据库的应用程序架构:

我们已经了解过多种类型的连接器 ADO.NET:

  • 专有连接器性能最佳,但会使 [dao] 层依赖于专有类。更改 SGBD 意味着需要更改 [dao] 层。
  • ODBC、OLE 或 DB 连接器支持在不更改 [dao] 层的情况下处理多种数据库。其性能低于专有连接器。
  • 通用连接器基于专有连接器构建,同时向 [dao] 层提供标准接口。

因此,通用连接器似乎是理想的连接器。但在实际应用中,通用连接器无法将 SGBD 的所有特殊性完全隐藏在标准接口背后。 在下一节中,我们将探讨参数化查询的概念。在 SQL Server 中,一个参数化查询的形式如下:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

在 MySQL5 中,相同的查询将写为:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

因此,两者在语法上存在差异。第 9.3.3 节中描述的 IDbCommand 接口的参数相关属性如下:

Parameters
已配置的 SQL 命令的参数列表。命令 update articles set prix=prix*1.1 where id=@id 包含参数 @id。

属性 Parameters 的类型为 IDataParameterCollection,即一个接口。它表示命令 SQL CommandText 的所有参数集合。 属性 Parameters 具有方法 Add,用于添加类型为 IDataParameter 的参数,该类型同样是一个接口。该接口具有以下属性:

  • ParameterName:参数名称
  • DbType:参数的类型 SQL
  • Value:参数的赋值
  • ...

类型 IDataParameter 非常适合 SQL 订单的参数

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

,因为其中包含命名参数。可以使用属性 ParameterName

类型 IDataParameter 不适用于订单 SQL

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

因为参数没有命名。此时将根据参数在集合 [IDbCommand.Parameters] 中添加的顺序进行处理。 在此示例中,需按 nomprixstockactuelstockminimum 的顺序插入这 4 个参数。 在带命名参数的请求中,参数添加的顺序并不重要。最终,开发人员在初始化带参数请求的参数时,无法完全忽略其使用的 SGBD。这是通用连接器当前的局限性之一。

有些 frameworks 能够突破这些限制,并为 [dao] 层带来新功能:

框架是一组旨在简化特定应用程序架构设计的类库集合。目前有多种框架可用于编写既高效又对 SGBD 变更不敏感的 [dao] 层:

  • Spring.Net [http://www.springframework.net/](本文档中已介绍)提供了与所研究的通用连接器功能相当的解决方案,且不存在其局限性,同时还具备多种简化数据访问的便利功能。该框架提供Java版本。
  • iBatis.Net [http://ibatis.apache.org] 比 Spring.Net 更早出现且功能更丰富。该组件提供 Java 版本。
  • NHibernate [http://www.hibernate.org/] 是 Java 领域广为人知的 Java 版 Hibernate 的移植版本。 NHibernate 允许 [dao] 层在不发出 SQL 命令的情况下与 SGBD 进行交互。 [dao]层处理Hibernate对象。一种名为HBL的查询语言(Hibernate Query Language)可用于查询由Hibernate管理的对象。正是这些对象会发出SQL命令。 Hibernate 能够适配 SQL 专有命令,这些命令属于 SGBD。
  • LINQ(INtegrated查询语言),已集成到3.5版中,并在C# 2008中可用。 LINQ 沿袭了 NHibernate 的脚步,但目前(2008 年 5 月)仅支持 SGBD SQL 服务器。 这一情况预计会随着时间的推移而改变。LINQ 比 NHibernate 更进一步:其查询语言支持以标准方式查询三种不同类型的数据源:
    • 对象集合(LINQ to Objects
    • XML 文件(LINQ to Xml
    • 数据库(LINQ to SQL

本文档将不涉及这些框架。但在专业应用中,强烈建议使用它们。

9.5. 带参数的查询

我们在上一节中提到了带参数的查询。在此,我们将通过 SGBD SQL Server Compact 的示例进行说明。该项目如下

  • 在 [1] 项目中。 仅使用了 [App.config]、[Article.cs] 和 [Parametres.cs]。还需注意 SQL Server 基础以及 [dbarticles.sdf]。
  • 在 [2] 中,该项目配置为执行 [Parametres.cs]
  • 在 [3] 中,项目的引用

配置文件 [App.config] 定义了数据库连接字符串:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
    </connectionStrings>
</configuration>

文件 [Article.cs] 定义了一个类 [Article]。 将使用对象 Article 来封装数据库 [dbarticles.sdf] 中表 ARTICLES 某行中的信息:


namespace Chap7 {
    class Article {
        // 属性
        public int Id { get; set; }
        public string Nom { get; set; }
        public decimal Prix { get; set; }
        public int StockActuel { get; set; }
        public int StockMinimum { get; set; }

        // 构造函数
        public Article() { 
        }

        public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
            Id = id;
            Nom = nom;
            Prix = prix;
            StockActuel = stockActuel;
            StockMinimum = stockMinimum;
        }

    }
}

应用程序 [Parametres.cs] 实现了以下带参数的查询:


using System;
using System.Data.SqlServerCe;
using System.Text;
using System.Data;
using System.Configuration;

namespace Chap7 {
    class Parametres {
        static void Main(string[] args) {

            // 配置文件处理
            string connectionString = null;
            try {
                // 连接字符串
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // 视图
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // 创建物料表
            Article[] articles = new Article[5];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i-1] = new Article(0, "article" + i, i * 100, i * 10, i);
            }

            // 处理可能出现的异常
            try {

                // 从数据库中删除现有商品
                ExecuteUpdate(connectionString, "delete from articles");

                // 显示表中的商品
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

                // 将商品表插入数据库
                InsertArticles(connectionString, articles);

                // 显示表中的商品
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                // 错误信息
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // 插入商品表
        static void InsertArticles(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // 建立连接
                connexion.Open();
                // 命令配置
                string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
                SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                sqlCommand.Parameters.Add("@nom",SqlDbType.NVarChar,30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                // 订单编译
                sqlCommand.Prepare();
                // 插入行
                for (int i = 0; i < articles.Length; i++) {
                    // 参数初始化
                    sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
                    sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
                    sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
                    sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
                    // 执行查询
                    sqlCommand.ExecuteNonQuery();
                }
            }
        }

        // 执行更新请求
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }

        // 执行 Select 查询
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // 显示读取器
        static void AfficheReader(IDataReader reader) {
...
    }
}

与之前所见内容相比,新内容是第 51-75 行中的 [InsertArticles] 过程:

  • 第 51 行:该过程接收两个参数:
    • 连接字符串 connectionString,用于使该过程连接到数据库
    • 一个对象数组 Article,该数组需添加到数据库中的表 Articles
  • 第 56 行:插入对象 [Article] 的查询。该查询有四个参数:
    • @nom:商品名称
    • @prix:其价格
    • @sa:其当前库存
    • @sm:其最低库存

此带参数查询的语法是 SQL Server Compact 的专有语法。我们在上一段中看到,使用 MySQL5 时,语法如下:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

在 SQL Server Compact 中,每个参数前都必须加上 @ 字符。参数名称可以自由定义。

  • 第58-61行:定义这4个参数各自的特征,并将其逐一添加到对象SqlCeCommand的参数列表中,该对象封装了即将执行的订单SQL。

此处使用的是具有六个签名的方法 [SqlCeCommand].Parameters.Add。我们使用其中的两个:

Add(string parameterName, SQLDbType type)

添加并配置名为 parameterName 的参数。该名称必须是已配置的带参数查询中的名称之一:(@名称, ...)。type 表示该参数所涉及列的类型 SQL。 可用的类型包括以下几种:

类型 SQL
C# 类型
注释
BigInt
Int64
 
DateTime
DateTime
 
Decimal
十进制
 
Float
双精度
 
Int
Int32
 
Money
十进制
 
NChar
字符串
固定长度字符串
NVarChar
字符串
可变长度字符串
Real
单值
 

Add(string parameterName, SQLDbType type, int size)

第三个参数 size 用于设定列的大小。此信息仅对某些类型有用,例如 SQL,NVarChar 等。

  • 第 63 行:编译带参数的查询。这也被称为“预编译”,因此该方法得名。此操作并非必需,其目的是为了提高性能。 当 SGBD 执行 SQL 命令时,会在执行前进行一定的优化工作。参数化查询旨在使用不同的参数多次执行。而查询的文本本身不会改变。 因此,优化工作只需进行一次。某些 SGBD 具有“预处理”或“编译”参数化查询的能力。 此时会为该查询定义一个执行计划。这就是我们所说的优化阶段。编译完成后,查询将反复执行,每次使用新的有效参数,但执行计划保持不变。

编译并非参数化查询的唯一优势。让我们重新审视之前研究的查询:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

我们可能希望通过程序构建查询文本:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('"+nom+"',"+prix+","+sa+","+sm+")";

如上所示,若 (名称,价格,库存,单位) 的值为 ("商品1",100,10,1),则前面的查询将变为:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('article1',100,10,1)";

现在,如果 (名称,价格,sa,sm) 的值为 ("商品1",100,10,1),则前面的查询变为:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('l'article1',100,10,1)";

并因名称 l'article1 中的撇号而导致语法错误。如果 nom 源自用户输入,这意味着我们需要检查输入内容是否包含撇号,若包含则需将其转义。 这种处理依赖于 SGBD。预编译查询的优势在于它能自动完成这项工作。仅凭这一便利性,就足以证明使用预编译查询的合理性。

  • 第 65-73 行:表中的项目逐一插入
  • 第 67-70 行:查询的四个参数各自通过其 Value 属性获取值。
  • 第 72 行:现已完整的插入查询按常规方式执行。

以下是一个执行示例:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------


------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

117 article1 100 10 1
118 article2 200 20 2
119 article3 300 30 3
120 article4 400 40 4
121 article5 500 50 5
  • 第 3 行:删除表中所有行后的提示信息
  • 第 5-7 行:显示表已清空
  • 第10-18行:显示插入5个条目后的表

9.6. Transactions

9.6.1. 概述

事务是一系列以“原子性”方式执行的 SQL 命令:

  • 要么所有操作都成功
  • 要么其中一项失败,那么之前的所有操作都会被撤销

最终,事务中的操作要么全部成功应用,要么一个也没应用。当用户自己控制事务时,他可以通过 COMMIT 命令确认事务,或通过 ROLLBACK 命令取消事务。

在之前的示例中,我们并未使用事务。但实际上存在事务,因为在 SGBD 中,SQL 命令始终在事务内执行。 如果客户 .NET 未自行启动显式事务,则 SGBD 将使用隐式事务。此时通常有两种情况:

  1. 每个单独的 SQL 命令都属于一个事务,该事务由 SGBD 在命令执行前发起,并在命令执行后关闭。 这被称为自动提交模式。因此,整个过程就好像客户端 .NET 为每个 SQL 命令分别执行事务一样。
  2. SGBD 未处于自动提交模式,并在客户 .NET 于事务外发出的第一个订单 SQL 时启动了一个隐式事务,并允许客户关闭该事务。 因此,客户端 .NET 发出的所有 SQL 命令都属于该隐式事务。 该事务可能因各种事件而结束:客户端关闭连接、开始新事务等,但此时便处于依赖 SGBD 的状态。应避免使用此模式。

默认模式通常由 SGBD 的配置决定。某些 SGBD 默认处于自动提交模式,而其他则不是。SQLServer Compact 默认处于自动提交模式。

不同用户的 SQL 命令会在并行运行的事务中同时执行。一个事务执行的操作可能会影响另一个事务的操作。不同用户的事务之间有四个隔离级别:

  • 未提交读
  • 已提交读
  • 可重复读
  • 可串行化

未提交读

此隔离级别也称为“脏读”。以下是一个在此模式下可能发生的情况示例:

  1. 用户 U1 在表 T 上开始一个事务
  2. 用户 U2 在同一张表 T 上开始一个事务
  3. 用户 U1 修改了表 T 中的行,但尚未提交
  4. 用户 U2 “看到”了这些修改,并根据所见内容做出决策
  5. 用户通过 ROLLBACK 撤销了其事务

可以看出,在步骤4中,用户U2是基于后来被证明是错误的数据做出的决策。

已提交读取

这种隔离级别可以避免上述问题。在此模式下,步骤4中的用户U2将无法“看到”用户U1对表T所做的修改。 只有在 U1 将其事务提交为 COMMIT 之后,他才会看到这些修改。

在此模式下(也称为“不可重复读”),仍可能遇到以下情况:

  1. 用户 U1 在表 T 上开始一个事务
  2. 用户 U2 在同一张表 T 上开始一个事务
  3. 用户 U2 执行 SELECT 操作,以获取满足特定条件的 T 表行中 C 列的平均值
  4. 用户 U1 修改(UPDATE)表 T 中列 C 的某些值,并提交(COMMIT)
  5. 用户 U2 再次执行与步骤 3 相同的 SELECT 操作。他将发现,由于 U1 所做的修改,C 列的平均值已经发生了变化。

此时,用户 U2 只能看到由 U1 “提交”的修改。但当他在同一事务中执行时,两个相同的操作(步骤 3 和 5)却产生了不同的结果。 “不可重复读”(Unrepeatable Read)一词正是指这种情况。对于希望获得表 T 稳定快照的人来说,这种情况非常令人困扰。

可重复读

在此隔离级别下,只要用户保持在同一事务中,其对数据库的读取结果必然一致。用户操作的是一张“快照”,其他事务(即使已提交)所做的修改永远不会反映到该快照中。 只有当用户自己通过 COMMIT 或 ROLLBACK 结束事务时,才会看到这些修改。

然而,这种隔离模式尚不完善。在执行上述操作 3 之后,用户 U2 查询的行会被锁定。 在操作4中,用户U1将无法修改(UPDATE)这些行中C列的值。但他可以添加新行(INSERT)。 如果新增的某些行满足步骤3中测试的条件,由于这些新增行的存在,步骤5得出的平均值将与步骤3中得出的不同。这些行有时被称为“幽灵行”

为解决这一新问题,需切换至“Serializable”隔离级别。

Serializable

在此隔离级别下,事务之间完全相互隔离。它确保两个同时进行的事务的结果,与它们依次执行时得到的结果相同。 为了实现这一结果,在操作4中,当用户U1试图添加会改变用户U1的SELECT操作结果的行时,系统将阻止该操作。 系统将显示一条错误消息,告知其无法插入数据。只有当用户 U2 提交其事务后,该操作才可执行。

并非所有事务隔离级别都可在所有事务中使用。 默认隔离级别通常为已提交读(Committed Read)。当客户端创建显式事务时,可以显式指定事务所需的隔离级别。

9.6.2. 事务管理 API

连接实现了第 9.3.3 节中介绍的 IDbConnection 接口。该接口具有以下方法:

BeginTransaction
M
启动事务。

该方法有两种签名:

  1. IDbTransaction BeginTransaction():启动事务并返回用于控制该事务的 IDbTransaction 对象
  2. IDbTransaction BeginTransaction(IsolationLevel level):进一步指定事务所需的隔离级别。level的取值来自以下枚举:
ReadUncommitted
事务可以读取由其他事务写入但尚未提交的数据——应避免
ReadCommitted
事务无法读取其他事务写入但尚未提交的数据。然而,事务中连续两次读取的数据可能会发生变化(不可重复读取),因为其他事务可能在此期间对其进行了修改(读取的行未被锁定——仅更新后的行会被锁定)。 此外,其他事务可能已添加了行(幽灵行),这些行将在第二次读取中被纳入。
RepeatableRead
事务读取的行与更新的行一样会被锁定。这可以防止其他事务对其进行修改。但这并不能阻止行被插入。
Serializable
事务操作的表会被锁定,从而阻止其他事务插入新行。一切都仿佛该事务是唯一的。这会降低性能,因为事务不再并行处理。
Snapshot
事务处理的是在时间点 T 创建的数据副本。当事务为只读时使用。与 serializable 产生相同的结果,同时避免了其开销。

事务启动后,由类型为 IDbTransaction 的对象进行控制,这是一个接口,我们将使用其以下属性 P 和方法 M:

名称
类型
角色
Connection
P
连接 IDbConnection,该连接支持事务
Commit
M
验证事务——事务中发出的 SQL 命令的结果被复制到数据库中。
Rollback
M
取消交易 - 交易中发出的 SQL 命令的结果不会复制到数据库中。

9.6.3. 示例程序

我们继续使用前一个项目,现在关注程序 [Transactions.cs]:

  • 在 [1] 中,该项目
  • 在 [2] 中,该项目配置为执行 [Transactions.cs]

[Transactions.cs]的代码如下:


using System;
using System.Configuration;
using System.Data;
using System.Data.SqlServerCe;
using System.Text;

namespace Chap7 {
    class Transactions {
        static void Main(string[] args) {

            // 配置文件处理
            string connectionString = null;
            try {
                // 连接字符串
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // 显示
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // 创建一个包含2个同名商品的表格
            Article[] articles = new Article[2];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i - 1] = new Article(0, "article", i * 100, i * 10, i);
            }
            // 处理可能出现的异常
            try {
                Console.WriteLine("Insertion sans transaction...");
                // 先在不进行事务的情况下将商品表插入数据库
                ExecuteUpdate(connectionString, "delete from articles");
                try {
                    InsertArticlesOutOfTransaction(connectionString, articles);
                } catch (Exception ex) {
                    // 错误消息
                    Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
                }
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

                // 再次执行相同操作,但本次在事务中进行
                Console.WriteLine("\n\nInsertion dans une transaction...");
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                // 错误消息
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // 在不使用事务的情况下插入商品表
        static void InsertArticlesOutOfTransaction(string connectionString, Article[] articles) {
....
        }

        // 在事务中插入商品表
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
....
        }

        // 执行更新查询
        static void ExecuteUpdate(string connectionString, string requête) {
....
        }

        // 执行 Select 查询
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // 显示阅读器
        static void AfficheReader(IDataReader reader) {
...
            }
        }
    }
}
  • 第 12-19 行:从 [App.config] 中读取 SQLServer 数据库连接字符串
  • 第 25-28 行:创建了一个包含两个对象的数组 Article。这两个对象的名称均为“article”。 然而,数据库 [dbarticles.sdf] 的 [nom] 列上设置了唯一性约束(参见第 9.3.1 节)。因此,这两个名为“article”的条目不能同时存在于该数据库中。 两个名为“article”的条目被添加到表 articles 中。 因此将会出现一个问题:c.a.d。该异常由 SGBD 抛出,并通过其连接器 ADO.NET 传递。为了展示事务的作用,这两个商品将分别插入到两个不同的环境中:
    • 首先是在完全脱离事务的情况下。 需注意,在此情况下,SQLServer Compact 处于自动提交模式,c.a.d 会将每个 SQL 命令插入到隐式事务中。第一条记录将被插入,而第二条则不会。
    • 随后,系统将这两个插入操作封装在一个显式事务中。由于第二个插入操作将失败,因此第一个插入操作也将被回滚。最终,没有任何插入操作会被执行。
  • 第 33 行:清空表 articles
  • 第 35 行:在没有显式事务的情况下插入两个条目。由于已知第二个插入操作会引发异常,因此通过 try/catch 进行处理
  • 第 46 行:显示表 articles
  • 第44-46行:重复执行相同的操作序列,但此次使用显式事务进行插入。此处遇到的异常由方法InsertArticlesInTransaction进行处理。
  • 第54-56行:方法InsertArticlesOutOfTransaction即为先前所研究的程序[Parametres.cs]中的方法InsertArticles。
  • 第 64-66 行:方法 ExecuteUpdate 与前文所述相同。 SQL 命令是在隐式事务中执行的。此处之所以可行,是因为我们知道在此情况下,SQLServer Compact 处于自动提交模式。
  • 第 69-71 行:方法 ExecuteSelect 亦同。

方法 InsertArticlesInTransaction 如下:


// 在事务中插入商品表
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // 建立连接
                connexion.Open();
                // 命令配置
                string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
                SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                // 命令编译
                sqlCommand.Prepare();
                // 事务
                SqlCeTransaction transaction = null;
                try {
                    // 开始事务
                    transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
                    // 必须在此事务中执行命令 SQL
                    sqlCommand.Transaction = transaction;
                    // 插入行
                    for (int i = 0; i < articles.Length; i++) {
                        // 初始化参数
                        sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
                        sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
                        sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
                        sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
                        // 执行查询
                        sqlCommand.ExecuteNonQuery();
                    }
                    // 提交事务
                    transaction.Commit();
                    Console.WriteLine("transaction validée...");
                } catch {
                    // 回滚事务
                    if (transaction != null)transaction.Rollback();
                    Console.WriteLine("transaction invalidée...");
                }
            }
        }

我们仅详细说明其与先前研究的程序 [Parametres.cs] 中的方法 InsertArticles 的区别:

  • 第 16 行:声明了一个 SqlCeTransaction 事务。
  • 第17、35行:用于处理第二次插入操作结束后可能引发的异常的try/catch语句
  • 第 19 行:创建事务。该事务属于当前连接。
  • 第 21 行:将已配置的 SQL 命令放入事务中
  • 第 23-31 行:执行插入操作
  • 第 33 行:一切顺利——事务已提交——插入操作将最终写入数据库。
  • 第37行:出现问题。如果事务已存在,则将其回滚。

执行结果如下:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Insertion sans transaction...
Il y a eu 0 ligne(s) modifiée(s)
Erreur d'accès à la base de données (A duplicate value cannot be inserted into a unique index. [ Table name = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ])

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

126 article 100 10 1


Insertion dans une transaction...
Il y a eu 1 ligne(s) modifiée(s)
transaction invalidée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------
  • 第 4 行:由 ExecuteUpdate("delete from articles") 显示——表中没有数据
  • 第5行:由第二次插入操作引发的异常。该消息表明未验证约束UQ__ARTICLES__0000000000000010。可通过查看数据库属性获取更多信息:
  • 在 Visual Studio 的 [Database Explorer] 视图中,[1] 已创建了连接 [2] 至数据库 [dbarticles.sdf]。 该连接的索引为 UQ__ARTICLES__0000000000000010。右键单击该索引,即可访问其属性(Index properties)
  • 在 [3,4] 中,可以看到索引 UQ__ARTICLES__0000000000000010 对应于列 [NOM] 上的唯一性约束
  • 第7-11行:两次插入操作后显示的articles表。该表不为空:已插入第一篇文章。
  • 第15行:由ExecuteUpdate("delete from articles")显示——表中原本有一条记录
  • 第16行:事务失败时由InsertArticlesInTransaction显示的消息。
  • 第 18-20 行:表明未执行任何插入操作。该事务中的 Rollback 撤销了第一次插入。

9.7. 方法 ExecuteScalar

9.7.1. 在第 9.3.3所述的 IDbCommand 接口的方法中,包含以下方法:

ExecuteScalar
M
用于执行 SQL 命令,而 Select 仅返回单一结果,例如:select count(*) from articles

此处展示该方法的一个使用示例。让我们回到项目:

  • 在 [1] 中,该项目
  • 在 [2] 中,该项目配置为执行 [ExecuteScalar.cs]

程序 [ExecuteScalar.cs] 内容如下:


...
namespace Chap7 {
    class Scalar {
        static void Main(string[] args) {

            // 配置文件处理
            string connectionString = null;
...

            // 显示
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // 创建包含5个项目的表格
            Article[] articles = new Article[5];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i - 1] = new Article(0, "article" + i, i * 100, i * 10, i);
            }

            // 处理可能出现的异常
            try {
                // 将商品表插入事务
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
                // 计算商品价格的平均值
                decimal prixMoyen = (decimal)ExecuteScalar(connectionString, "select avg(prix) from articles");
                Console.WriteLine("Prix moyen des articles={0}", prixMoyen);
                // 或商品数量
                int nbArticles = (int)ExecuteScalar(connectionString, "select count(id) from articles");
                Console.WriteLine("Nombre d'articles={0}", nbArticles);
            } catch (Exception ex) {
                // 错误消息
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // 将商品表插入事务
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
...
        }


        // 执行更新查询
        static object ExecuteScalar(string connectionString, string requête) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // 建立连接
                connexion.Open();
                // 执行查询
                return new SqlCeCommand(requête, connexion).ExecuteScalar();
            }
        }

        // 执行更新查询
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }

        // 执行 Select 查询
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // 显示阅读器
        static void AfficheReader(IDataReader reader) {
...
        }
    }
}
  • 第14-17行:创建一个包含5个项目的数组
  • 第22行:清空表articles
  • 第23行:将5个商品填入该表
  • 第 24 行:显示该表
  • 第26行:查询商品的平均价格
  • 第29行:查询商品数量
  • 第 49 行:使用方法 [IDbCommand].ExecuteScalar() 计算上述各项值。

执行结果如下:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)
transaction validée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

145 article1 100 10 1
146 article2 200 20 2
147 article3 300 30 3
148 article4 400 40 4
149 article5 500 50 5
Prix moyen des articles=300
Nombre d'articles=5

第 15 和 16 行显示了方法 ExecuteScalar 返回的两个值。

9.8. 示例应用程序 - 版本 7

我们继续使用示例应用程序 IMPOTS。上一节(7.6)已对该应用程序的最新版本进行了分析。该应用程序是一个三层架构,具体如下:

  • [ui]层是一个图形用户界面[A],而[dao]层从文本文件[B]中获取数据。
  • 图层的实例化及其在应用程序中的集成由 Spring 负责。

我们修改 [dao] 层,使其从数据库中获取数据。

9.8.1. 数据库

前文提到的文本文件 [B] 的内容被放入数据库 MySQL5 中。以下是具体操作步骤:

  • [1]:已启动 MySQL Administrator
  • [2,3]:在 [Schemata] 区域,右键单击并选择 [Create Schema] 选项以创建新数据库
  • [4]:该数据库将命名为 [bdimpots]
  • [5]:该数据库已添加到[Schemata]区域的数据库中。
  • [6,7]:右键单击该表,选择 [Create New Table] 选项以创建新表
  • [8]:该表将命名为 [tranches]。它将包含 [id, limite, coeffR, coeffN] 列。
  • [9,10]:[id]是类型为INTEGER的主键,并具有属性AUTO_INCREMENT [10]: 在添加行时,将由 SGBD 负责填充此列。
  • [limite, coeffR, coeffN] 列的类型为 DOUBLE。
  • [11,12]:新表将显示在数据库的[Schema Tables]选项卡中。
  • [13,14]:用于向表中插入数据
  • [15]:已启动 [Query Browser]
  • [16]:已为列 [limite, coeffR, coeffN] 输入并验证了数据。列 [id] 已由 SGBD 填充。 已通过 [17] 进行验证。
  • 仍在 [Query Browser] [18] 中,执行 [20] 查询 [19]。 该查询创建了一个用户 'admimpots',密码为 'mdpimpots',并授予其对数据库 bdimpots 中所有对象的全部权限(grant all privileges)(即 bdimpots.*)。 这将使我们能够使用用户 [admimpots] 而不是管理员 [root] 来操作数据库 [bdimpots]。

9.8.2. Visual Studio 解决方案

我们将遵循示例应用程序第 5 版中探讨的方法(参见第 6.4 节)。我们将逐步构建以下 Visual Studio 解决方案:

  • 在 [1] 中:解决方案 ImpotsV7 由三个项目组成,分别对应应用程序的三个层
  • 在 [2] 中:[dao] 层的项目 [dao],该层将开始使用数据库
  • 在 [3] 中:[metier] 层的 [metier] 项目。此处沿用了第 6.4.4 节中描述的第 5 版中的 [metier] 层。
  • 在 [4] 中:[ui] 层的 [ui] 项目。此处我们沿用了第 7.6 节中描述的第 6 版 [ui] 层。

我们依托现有成果,复用两个已编写好的层:[ui]层和[metier]层。这得益于所选的分层架构。 不过,我们仍需 [ui] 和 [metier] 层的源代码。因为仅使用 DLL 层是不够的。 在第5版中,当创建[metier]层的DLL时,它依赖于[dao]层的DLL。 该依赖关系已被硬编码到 [metier] 层中的 DLL 中(DLL 的名称、[dao] 层、版本、身份令牌等)。因此,第 5 版 [ImpotsV5-metier.dll] 中的 DLL 仅支持与 DLL 和 [ImpotsV5-dao.dll] 配合使用,因为它是与这些组件一起编译的。 如果将 [dao] 层中的 DLL 进行更改,则必须重新编译 [metier] 层,以便为其生成新的 DLL。 [ui]层的情况也是如此。 因此,[ui] 和 [metier] 层将不会被修改,但会重新编译,以便与新层 [dao] 中的 DLL 协同工作。

9.8.3. [dao] 层

项目参考(参见项目中的 [1])

  • nunit.framework:用于测试NUnit
  • System.Configuration:用于处理配置文件 [App.config]
  • System.Data:因为需要调用数据库。

实体(参见项目中的 [2])

类 [TrancheImpot] 和 [ImpotException] 属于旧版本。

[dao] 层(参见项目中的 [3])

接口 [IImpotDao] 未发生变更:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // 税率区间
        TrancheImpot[] TranchesImpot{get;}
    }
}

该接口的实现类 [DataBaseImpot] 如下:


using System;
using System.Collections.Generic;
using System.Data.Common;
using Entites;

namespace Dao {
    public class DataBaseImpot : IImpotDao {
        // 税率档次
        private TrancheImpot[] tranchesImpot;
        public TrancheImpot[] TranchesImpot { get { return tranchesImpot; } }

        // 制造商
        public DataBaseImpot(string factory, string connectionString, string requête) {
            // 工厂:目标 SGBD 的工厂
            // connectionString:税率区间的数据库连接字符串
            // 处理可能出现的异常
            try {
                // 获取用于 SGBD 的通用连接器
                DbProviderFactory connecteur = DbProviderFactories.GetFactory(factory);
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // 连接配置
                    connexion.ConnectionString = connectionString;
                    // 建立连接
                    connexion.Open();
                    // 配置命令 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // 执行请求
                    List<TrancheImpot> listTrancheImpot = new List<TrancheImpot>();
                    using (DbDataReader reader = sqlCommand.ExecuteReader()) {
                        while (reader.Read()) {
                            // 创建新的税率区间
                            listTrancheImpot.Add(new TrancheImpot() { Limite = reader.GetDecimal(0), CoeffR = reader.GetDecimal(1), CoeffN = reader.GetDecimal(2) });
                        }
                    }
                    // 将税率区间放入其实例中
                    tranchesImpot = listTrancheImpot.ToArray();
                }
            } catch (Exception ex) {
                // 将异常封装在类型 ImpotException 中
                throw new ImpotException("Erreur de lecture des tranches d'impôt", ex) { Code = 101 };
            }

        }
    }
}
  • 第 7 行:类 [DataBaseImpot] 实现了接口 [IImpotDao]。
  • 第 10 行:接口方法 [TranchesImpot] 的实现。它仅返回第 9 行中税率区间数组的引用。该数组将由类的构造函数构建。
  • 第13行:构造函数。它使用一个通用连接器(参见第9.4.5节)来访问税率区间数据库。该构造函数接收三个参数:
    • 用于向其请求连接数据库、发出 SQL 命令以及处理 Select 结果的“工厂”名称。
    • 用于连接数据库的连接字符串
    • 用于获取税率区间的 SQL Select 命令。
  • 第19行:向“工厂”请求连接器
  • 第 20 行:使用该连接器建立连接。连接已建立但尚未启用
  • 第22行:初始化连接字符串。现在可以建立连接了。
  • 第 24 行:建立连接
  • 第26行:向连接器请求一个[DbCommand]对象,用于执行SQL命令
  • 第27行:指定要执行的命令为SQL
  • 第 28 行:指定执行该命令的连接
  • 第30行:创建一个名为[listTrancheImpot]的空列表,其中包含类型为[TrancheImpot]的对象。
  • 第 31 行:执行 SQL Select 命令
  • 第 32-35 行:处理 Select 操作生成的 [DbDataReader] 对象。Select 操作结果表中的每一行都用于实例化一个 [TrancheImpot] 类型的对象,并将该对象添加到 [listTrancheImpot] 列表中。
  • 第 38 行:类型为 [TrancheImpot] 的对象列表被转移到第 9 行的数组中。
  • 第 40-43 行:任何可能发生的异常都被封装在 [ImpotException] 类型中,并被分配错误代码 101(任意)。

测试[Test1](参见项目中的[4])

类 [Test1] 仅负责在屏幕上显示税率区间。该类与第 5 版(第 6.4.3 节)中已使用的类相同,唯一区别在于第 14 行中实例化 [dao] 层的语句。


using System;
using Dao;
using Entites;
using System.Configuration;

namespace Tests {
    class Test1 {
        static void Main() {

            // 创建层 [dao]
            IImpotDao dao = null;
            try {
                // 创建 [dao] 层
                dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
            } catch (ImpotException e) {
                // 显示错误
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // 程序停止
                Environment.Exit(1);
            }
            // 显示税率区间
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
        }
    }
}

第 14 行调用以下配置文件 [App.config]:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbImpotsMySql5" connectionString="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;" />
    </connectionStrings>
    <appSettings>
        <add key="requete" value="select limite, coeffr, coeffn from tranches"/>
        <add key="factoryMySql5" value="MySql.Data.MySqlClient"/>
    </appSettings>
</configuration>
  • 第 4 行:连接数据库 MySQL5 的连接字符串。请注意,将由用户 [admimpots] 建立连接。
  • 第 8 行:用于处理 SGBD 的“工厂”MySQL5
  • 第7行:用于获取税档的SQL Select查询。

该项目已配置为执行 [Test1.cs]:

Image

测试执行结果如下:

1
2
3
4
5
6
7
4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

测试 NUnit [NUnit1](参见项目中的 [4])

单元测试 [NUnit1] 与第 5 版(第 6.4.3 节)中已使用的测试相同,仅在第 16 行实例化 [dao] 层时有所不同。


using System;
using System.Configuration;
using Dao;
using Entites;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1 : AssertionHelper{
        // 待测试的图层 [dao]
        private IImpotDao dao;

        // 构建器
        public NUnit1() {
            // 初始化层 [dao]
            dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
        }

        // 测试
        [Test]
        public void ShowTranchesImpot(){
            // 显示税率区间
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
            // 一些测试
            Expect(tranchesImpot.Length,EqualTo(7));
            Expect(tranchesImpot[2].Limite,EqualTo(14753).Within(1e-6));
            Expect(tranchesImpot[2].CoeffR, EqualTo(0.191).Within(1e-6));
            Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92).Within(1e-6));
        }
    }
}

要执行此单元测试,项目类型必须为 [Class Library]:

  • 在 [1] 中:项目类型已更改
  • 为 [2]:生成的 DLL 将命名为 [ImpotsV7-dao.dll]
  • 在 [3] 中:生成项目(F6)后,[dao/bin/Release] 文件夹中包含 DLL 和 [ImpotsV7-dao.dll]。 该文件夹还包含配置文件 [App.config],其已重命名为 [nom DLL].config。这是 Visual Studio 中的标准操作。

随后,DLL 和 [ImpotsV7-dao.dll] 被加载到 NUnit 框架中并执行:

  • 在 [1] 中:测试已通过。我们现在认为 [dao] 层已投入运行。其 DLL 包含项目中的所有类,包括测试类。这些测试类已无用武之地。 我们将重建 DLL 以排除其中的测试类。
  • 生成 [2]:将 [tests] 文件夹从项目中移除
  • 生成 [3]:新项目。该项目由 F6 重新生成,以生成新的 DLL。 应用程序中的 [metier] 和 [ui] 层将使用此 DLL。

9.8.4. [metier] 层

  • 在 [1] 中,项目 [metier] 已成为解决方案的当前活动项目
  • 在 [2] 中:项目引用。请注意,DLL 引用了先前创建的 [dao] 层。此添加引用的流程已在第 5 版第 6.4.4 节中描述。
  • 在 [3] 中:图层 [metier]。这是第 5 版中的图层,在第 6.4.4 节中已有描述。

项目 [metier] 已配置为生成 DLL:

  • [1]:该项目属于“类库”类型
  • [2]:生成该项目将产生 DLL、[ImpotsV7-metier.dll] 和 [3]。

项目已生成(F6)。

9.8.5. 图层 [ui]

  • 在 [1] 中,项目 [ui] 已成为解决方案的当前项目
  • 在 [2] 中:项目引用。请注意 DLL 中对 [dao] 和 [metier] 层的引用。
  • 在 [3] 中:[ui] 层。这是第 7.6 节中描述的第 6 版。
  • 在 [4] 中,配置文件 [App.config] 与第 6 版中的配置文件类似。它们唯一的区别在于 Spring 实例化 [dao] 层的方式:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
                <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
                <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • 第 11-25 行:Spring 配置
  • 第 15-24 行:由 Spring 实例化的对象
  • 第 16-20 行:[dao] 层的实例化
  • 第 16 行:[dao] 层由位于 DLL 中的 [Dao.DataBaseImpot] 类实例化 [ImpotsV7-Dao]
  • 第17-19行:需提供给[Dao.DataBaseImpot]类构造函数的三个参数(所用SGBD的工厂、连接字符串、SQL请求)
  • 第21-23行:实例化[metier]层。此配置与第6版相同。

测试

项目 [ui] 的配置如下:

  • [1]:该项目类型为“Windows 应用程序”
  • [2]:项目生成将产生可执行文件 [ImpotsV7-ui.exe]

[3] 提供了运行示例。

9.8.6. 更改数据库

上文中的[dao]层是使用通用连接器和MySQL5基础层编写的。在此,我们将切换至SQL Server Compact基础层,以说明仅配置会发生变化。

的SQL Server Compact数据库将如下所示:

  • [1]:位于 Visual Studio [2] 视图中的 [dbimpots.sdf] 数据库。该数据库创建时未设置密码。
  • [3]:包含数据的表 [data]。 我们特意为该表及其列选择了与 MySQL5 数据库中不同的名称,以此再次强调将此类细节写入配置文件而非代码中的重要性。
  • [4]:[id] 列是主键,并具有 Identity 属性:由 SGBD 为其赋值。
  • [5]:[data]表的内容。
  • [6]:数据库 [dbimpots.sdf] 已放置在项目 [ui] 的文件夹中,并已集成到该项目中。
  • [7]:数据库 [dbimpots.sdf] 将被复制到项目的运行文件夹中。

新数据库的配置文件 [App.config] 如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <!--
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
                <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
                <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
            </object>
            -->
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
                <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
                <constructor-arg index="2" value="select data1, data2, data3 from data"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • 第 23-27 行:[dao] 层的配置,用于调用 [dbimpots.sdf] 数据库。

执行结果与之前相同。值得注意的是,使用通用连接器可使 [dao] 层不受 SGBD 变更的影响。然而,我们已看到该连接器并不适用于所有情况,特别是使用带参数查询的情形。 因此还有其他解决方案,例如前文提到的第三方数据访问框架(Spring、iBatis、NHibernate、LINQ 等)。

9.9. 深入了解……

  • LINQ 在许多著作中都有介绍,特别是本书:《C# 3.0 in a Nutshell》,作者 Joseph 和 Ben Albahari,O'Reilly 出版社,本文导言中已提及。
  • iBatis 被收录在以下书籍中:《iBatis in Action》,Clinton Begin 著,Manning 出版社
  • Manning出版社的《Nhibernate in Action》预计于2008年7月出版

Spring、iBatis、NHibernate的参考手册可在各框架的官方网站上获取。