Skip to content

7. 数据库访问

7.1. 概述

Windows 平台上有许多数据库。应用程序通过称为驱动程序(drivers)的程序来访问这些数据库。

在上图中,驱动程序提供两个接口:

  • 面向应用程序的 I1 接口
  • 面向数据库的 I2 接口

为了避免因迁移至不同的 B2 数据库而需要重写针对 B1 数据库编写的应用程序,我们对 I1 接口进行了标准化处理。 如果使用的是采用“标准化”驱动程序的数据库,则 B1 数据库将随附 P1 驱动程序, B2数据库将随P2驱动程序提供,且这两个驱动程序的I1接口将完全一致。因此无需重写应用程序。 例如,我们可以将数据库 ACCESS 迁移到数据库 MySQL,而无需修改应用程序。

目前有两种标准驱动程序:

  • ODBC 驱动程序(Open DataBase Connectivity)
  • OLE 和 DB 驱动程序(对象链接与嵌入 DataBase)

ODBC 驱动程序支持访问数据库。OLE 和 DB 驱动程序的数据源更为多样:数据库、邮件系统、通讯录等,没有限制。 只要开发人员决定,任何数据源均可通过 OLE 和 DB 驱动程序进行访问。其优势显而易见:能够以统一的方式访问多种多样的数据。

.NET 平台随附两种数据访问类:

  1. SQL 和 Server.NET 类
  2. OLE 和 Db.NET 类

第一类类可直接访问 Microsoft SGBD SQL 服务器,无需中间驱动程序。第二类类可访问 OLE DB 数据源。

Image

.NET 平台(2002年5月)随附三个驱动程序 OLE、DB,分别对应:SQL Server、Oracle 和 Microsoft Jet(Access)。 如果要操作一个拥有 ODBC 驱动程序但没有 OLE 和 DB 驱动程序的数据库,则无法进行操作。 因此,无法使用 SGBD 和 MySQL(截至 2002 年 5 月),因为它们不提供 OLE 和 DB 驱动程序。 不过,有一系列类可以访问 ODBC 数据源,即 odbc.net 类。这些类并非 SDK 的标准配置,需要从微软网站下载。 在接下来的示例中,我们将主要使用这些 ODBC 类,因为大多数 Windows 数据库都附带此类驱动程序。 例如,以下是在 Win 2000 机器上已安装的 ODBC 驱动程序列表(“开始”菜单/“设置”/“控制面板”/“管理工具”):

Image

选择“数据源”图标 ODBC

Image

7.2. 数据源的两种操作模式

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

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

联机模式下,应用程序

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

脱机模式下,应用程序

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

在这两种情况下,耗时的主要环节都是数据的操作和更新。假设这些更新是由用户手动录入完成的,该操作可能需要数十分钟。 在此期间,在联机模式下,与数据库的连接始终保持,修改会立即同步。在脱机模式下,数据更新期间不与数据库建立连接。修改仅在内存副本中进行,待全部完成后一次性同步到数据源。

这两种方法各有何优缺点?

  • 连接会消耗大量系统资源。如果存在大量并发连接,离线模式可将连接时长缩短至最低限度。拥有数千名用户的Web应用程序便是如此。
  • 脱机模式的缺点在于难以处理并发更新。用户 U1 在时间点 T1 获取数据并开始修改。 在时间点 T2,用户 U2 也访问了数据源并获取了相同的数据。与此同时,用户 U1 已修改了部分数据,但尚未将其提交回数据源。 因此,U2 处理的数据中包含部分错误数据。虽然 .NET 类提供了管理此问题的解决方案,但该问题并不容易解决。
  • 在联机模式下,多个用户同时更新数据通常不会造成问题。 由于与数据库的连接始终保持,这些并发更新由数据库自身进行管理。因此,当用户修改数据库中的一行数据时,Oracle 会立即锁定该行。该行将保持锁定状态,其他用户无法访问,直到修改者提交(commit)或回滚(rollback)其修改。
  • 如果数据需要在网络上传输,则应选择脱机模式。该模式允许将数据快照保存在一个称为数据集(dataset)的对象中,该对象本身即代表一个独立的数据库。该对象可在网络上的不同机器之间传输。

我们首先研究联机模式。

7.3. 连接模式下的数据访问

7.3.1. 示例数据库

我们考虑一个名为 ACCESS 的数据库,其实际名称为 articles.mdb,仅包含一个名为 ARTICLES 的表,结构如下:

名称
类型
代码
4位商品代码
名称
名称(字符串)
价格
其价格(实际价格)
stock_actuel
当前库存(整数)
stock_minimum
最低库存(整数),低于该数值时需补货

其初始内容如下:

Image

我们将通过 ODBC 示例程序和OLE DB 驱动程序,以展示这两种方法的相似性,同时也因为我们拥有针对 ACCESS 的这两类驱动程序。

我们还将使用一个包含相同唯一表 ARTICLES 的数据库 MySQL DBARTICLES, 内容相同,且通过驱动程序 ODBC 访问,以此说明:为利用数据库 ACCESS 而编写的应用程序,在切换至数据库 MySQL 时无需进行修改。 用户 admarticles 可使用密码 mdparticles 访问数据库 DBARTICLES。下图显示了数据库 MySQL 的内容:

C:\mysql\bin>mysql --database=dbarticles --user=admarticles --password=mdparticles
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3 to server version: 3.23.49-max-debug

Type 'help' for help.


mysql> show tables;
+----------------------+
| Tables_in_dbarticles |
+----------------------+
| articles             |
+----------------------+
1 row in set (0.01 sec)

mysql> select * from articles;
+------+--------------------------------+------+--------------+---------------+
| code | nom                            | prix | stock_actuel | stock_minimum |
+------+--------------------------------+------+--------------+---------------+
| a300 | vÚlo                           | 2500 |           10 |             5 |
| b300 | pompe                          |   56 |           62 |            45 |
| c300 | arc                            | 3500 |           10 |            20 |
| d300 | flÞches - lot de 6             |  780 |           12 |            20 |
| e300 | combinaison de plongÚe         | 2800 |           34 |             7 |
| f300 | bouteilles d'oxygÞne           |  800 |           10 |             5 |
+------+--------------------------------+------+--------------+---------------+
6 rows in set (0.02 sec)

mysql> describe articles;
+---------------+-------------+------+-----+---------+-------+
| Field         | Type        | Null | Key | Default | Extra |
+---------------+-------------+------+-----+---------+-------+
| code          | text        | YES  |     | NULL    |       |
| nom           | text        | YES  |     | NULL    |       |
| prix          | double      | YES  |     | NULL    |       |
| stock_actuel  | smallint(6) | YES  |     | NULL    |       |
| stock_minimum | smallint(6) | YES  |     | NULL    |       |
+---------------+-------------+------+-----+---------+-------+
5 rows in set (0.00 sec)

mysql> exit
Bye

要将数据库 ACCESS 定义为数据源 ODBC,请按以下步骤操作:

  • 如上所述,启动数据源管理器 ODBC,并选择“用户”选项卡 DSN(DSN=数据源名称)

Image

  • 通过按钮 Add 添加数据源,指定该数据源可通过驱动程序 Access 访问,并执行 Terminer

Image

  • 将数据源命名为 articles-access,填写任意描述,并使用 Sélectionner 按钮指定数据库中的 .mdb 文件。最后执行 OK

Image

新数据源随即会显示在用户数据源列表中:

Image

若要将数据库 MySQL DBARTICLES 设置为数据源 ODBC,请按以下步骤操作:

  • 按照上文所述启用数据源管理器 ODBC,并选择“用户”选项卡 DSN。 使用 Add 添加一个新的数据源,并选择 ODBC 驱动程序(来自 MySQL)。

Image

  • 执行 Terminer。随后将显示 MySQL 数据源的配置页面:

54321

Image

  • 在 (1) 中为我们的数据源 ODBC 命名
  • 在 (2) 中,指定 MySQL 服务器所在的机器。 此处我们填写 localhost,表示该服务器与我们的应用程序位于同一台机器上。如果 MySQL 服务器位于远程机器 M 上,则应在此处填写该机器的名称,这样我们的应用程序无需任何修改即可使用远程数据库。
  • 在 (3) 中填写数据库名称。此处数据库名为 DBARTICLES。
  • 在 (4) 中填写登录名 admarticles,在 (5) 中填写密码 mdparticles

7.3.2. 使用驱动程序 ODBC

在采用连接模式的数据库应用程序中,通常包含以下步骤:

  1. 连接数据库
  2. 向数据库发送查询 SQL
  3. 接收并处理这些查询的结果
  4. 关闭连接

步骤 2 和 3 会反复执行,而连接关闭仅在数据库操作结束时进行。这是一个相对经典的流程,如果您曾以交互方式操作过数据库,可能对此已很熟悉。 无论通过 ODBC 驱动程序还是 OLE DB 驱动程序访问数据库,这些步骤均保持一致。 下面我们以管理数据源的 .NET 类为例进行说明。 该程序名为 liste,其参数为数据源 DSN 的名称,该数据源包含表 ODBC。随后,它将显示该表的内容:

dos>liste
syntaxe : pg dsnArticles

dos>liste articles-access

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

a300 vélo                           2500 10 5
b300 pompe                          56 62 45
c300 arc                            3500 10 20
d300 flèches - lot de 6             780 12 20
e300 combinaison de plongée         2800 34 7
f300 bouteilles d'oxygène           800 10 5

dos>liste mysql-artices
Erreur d'exploitation de la base de données (ERROR [IM002] [Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified)

dos>liste mysql-articles

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

a300 vélo                           2500 10 5
b300 pompe                          56 62 45
c300 arc                            3500 10 20
d300 flèches - lot de 6             780 12 20
e300 combinaison de plongée         2800 34 7
f300 bouteilles d'oxygène           800 10 5

从上述结果中,我们可以看到该程序同时列出了数据库 ACCESS 和 MySQL 的内容。现在让我们来研究一下该程序的代码:


' 选项
Option Explicit On 
Option Strict On

' 命名空间
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports Microsoft.VisualBasic

Module db1
    Sub main(ByVal args As String())
        ' 控制台应用程序
        ' 显示数据库 DSN 中表 ARTICLES 的内容 
        ' 其名称作为参数传递
        Const syntaxe As String = "syntaxe : pg dsnArticles"
        Const tabArticles As String = "articles"        ' la table des articles

        ' 参数验证
        ' 是否有 1 个参数
        If args.Length <> 1 Then
            ' 错误消息
            Console.Error.WriteLine(syntaxe)
            ' 结束
            Environment.Exit(1)
        End If

        ' 获取参数
        Dim dsnArticles As String = args(0)                ' la base DSN
        ' 准备连接数据库
        Dim articlesConn As OdbcConnection = Nothing        ' la connexion
        Dim myReader As OdbcDataReader = Nothing            ' le lecteur de données

        ' 尝试访问数据库
        Try
            ' 数据库连接字符串
            Dim connectString As String = "DSN=" + dsnArticles + ";"
            articlesConn = New OdbcConnection(connectString)
            articlesConn.Open()

            ' 执行命令 SQL
            Dim sqlText As String = "select * from " + tabArticles
            Dim myOdbcCommand As New OdbcCommand(sqlText)
            myOdbcCommand.Connection = articlesConn
            myReader = myOdbcCommand.ExecuteReader()

            ' 查询检索到的表
            ' 显示列
            Dim ligne As String = ""
            Dim i As Integer
            For i = 0 To (myReader.FieldCount - 1) - 1
                ligne += myReader.GetName(i) + ","
            Next i
            ligne += myReader.GetName(i)
            Console.Out.WriteLine((ControlChars.Lf + "".PadLeft(ligne.Length, "-"c) + ControlChars.Lf + ligne + ControlChars.Lf + "".PadLeft(ligne.Length, "-"c) + ControlChars.Lf))

            ' 显示数据
            While myReader.Read()
                ' 处理当前行
                ligne = ""
                For i = 0 To myReader.FieldCount - 1
                    ligne += myReader(i).ToString + " "
                Next i
                Console.WriteLine(ligne)
            End While
        Catch ex As Exception
            Console.Error.WriteLine(("Erreur d'exploitation de la base de données " + ex.Message + ")"))
            Environment.Exit(2)
        Finally
            ' 关闭读取器
            myReader.Close()
            ' 关闭连接
            articlesConn.Close()
        End Try
    End Sub
End Module

源管理类 ODBC 位于命名空间 Microsoft.Data.Odbc 中,因此我们需要导入该命名空间。此外,还有一些类位于命名空间 System.Data 中。


Imports System.Data
Imports Microsoft.Data.Odbc

程序使用的命名空间位于不同的程序集之中。使用以下命令编译程序:

dos>vbc /r:microsoft.data.odbc.dll /r:microsoft.visualbasic.dll /r:system.dll /r:system.data.dll db1.vb

7.3.2.1. 连接阶段

连接 ODBC 使用类 OdbcConnection。该类的构造函数接受一个称为连接字符串的参数。这是一个字符串,定义了建立数据库连接所需的所有参数。这些参数可能非常多,因此字符串可能很复杂。 该字符串的格式为 "param1=valeur1;param2=valeur2;...;paramj=valeurj;"。以下是 paramj 可能包含的几个参数:

uid
将访问数据库的用户名
password
该用户的密码
dsn
数据库名称(如有)DSN
data source
所访问的数据库名称
...
 

如果使用数据源管理器 ODBC 将数据源定义为 ODBC,则这些参数已指定并保存。 此时只需传递参数 DSN,该参数将返回数据源的名称 DSN。此处即采用此方法:


        ' 准备连接数据库
        Dim articlesConn As OdbcConnection = Nothing        ' la connexion
        Dim myReader As OdbcDataReader = Nothing        ' le lecteur de données
        Try
            ' 正在尝试访问数据库
            ' 数据库连接字符串
            Dim connectString As String = "DSN=" + dsnArticles + ";"
            articlesConn = New OdbcConnection(connectString)
            articlesConn.Open()

构建完对象 OdbcConnection 后,使用方法 Open 建立连接。与数据库上的其他操作一样,此连接建立过程也可能失败。因此,访问数据库的全部代码都包含在 try-catch 中。 建立连接后,即可向数据库发出 SQL 查询。

7.3.2.2. 发送 SQL 查询

要发出 SQL 查询,我们需要一个 Command 对象,更准确地说,这里需要一个 OdbcCommand 对象,因为我们使用的是 ODBC 数据源。 OdbcCommand 类有多个构造函数:

  • OdbcCommand():创建一个空的 Command 对象。要使用它,后续需要指定各种属性:
    • CommandText:待执行的 SQL 查询文本
    • Connection:表示将执行查询的数据库连接的 OdbcConnection 对象
    • CommandType:SQL 查询的类型。可能有三种值
  1. CommandType.Text:属性 CommandText 包含 SQL 查询的文本(默认值)
  2. CommandType.StoredProcedure:属性 CommandText 包含数据库中一个存储过程的名称
  3. CommandType.TableDirect:属性 CommandText 包含表 T 的名称。等同于 select * from T。仅适用于驱动程序 OLE DB。
  • OdbcCommand(string sqlText):参数 sqlText 将被赋值给属性 CommandText。 这是待执行的 SQL 查询文本。连接信息需在 Connection 属性中指定。
  • OdbcCommand(string sqlText, OdbcConnection 连接) : 参数 sqlText 将赋值给属性 CommandText,参数 connexion 将赋值给属性 Connection

要发出 SQL 请求,有两种方法:

  • OdbcdataReader ExecuteReader(): 将 CommandText 中的查询 SELECT 发送至连接 Connection,并构建一个 OdbcDataReader 对象,从而访问 select 结果表中的所有行
  • int ExecuteNOnQuery():发送更新请求(INSERT、 UPDATE、DELETE)从 CommandText 发送至连接 Connection,并返回受此更新影响的行数。

在本例中,建立数据库连接后,我们发出查询 SQL SELECT 以获取表 ARTICLES 的内容:


            ' 正在执行命令 SQL
            Dim sqlText As String = "select * from " + tabArticles
            Dim myOdbcCommand As New OdbcCommand(sqlText)
            myOdbcCommand.Connection = articlesConn
            myReader = myOdbcCommand.ExecuteReader()

查询请求通常是以下类型的请求:

    select col1, col2,... from table1, table2,...
    where condition
    order by expression
    ...

只有第一行的关键字是必填的,其余均为可选。还有其他未在此列出的关键字。

  1. 系统会与位于 from 关键字后面的所有表进行连接
  2. 仅保留位于 select 关键字后面的列
  3. 仅保留满足 where 关键字条件的行
  4. 根据 order by 关键字表达式排序后的结果行即构成查询结果。

select的查询结果是一个表。如果参考前面的ARTICLES表,并希望获取当前库存低于最低阈值的商品名称,则应编写如下语句:

    select nom from articles where stock_actuel<stock_minimum

若需按商品名称的字母顺序排列,则应输入:

    select nom from articles where stock_actuel<stock_minimum order by nom

7.3.2.3. 查询结果 SELECT 的利用

在脱机模式下,查询 SELECT 的结果是一个 DataReader 对象,此处为 OdbcDataReader 对象。该对象可用于依次获取结果中的所有行,并获取这些结果列的相关信息。 让我们来看看该类的几个属性和方法:

FieldCount
表的列数
Item
Item(i) 表示结果当前行中的第 i 列
XXX GetXXX(i)
当前行第 i 列的值,以 XXX 类型呈现(Int16、Int32、Int64、Double、String、Boolean 等)
string GetName(i)
第 i 列的名称
Close()
关闭对象 OdbcdataReader 并释放相关资源
bool Read()
在结果表中向前移动一行。若无法实现则返回false。新行将成为读取器的当前行。

select 结果的处理通常是类似于文本文件的顺序处理:只能在表中向前移动,不能向后移动:


While myReader.Read()
    ' 获取了一行数据 - 进行处理
    ....
    ' 下一行
end while

这些说明足以理解我们示例中的以下代码:


            ' 处理检索到的表
            ' 显示列
            Dim ligne As String = ""
            Dim i As Integer
            For i = 0 To (myReader.FieldCount - 1) - 1
                ligne += myReader.GetName(i) + ","
            Next i
            ligne += myReader.GetName(i)
            Console.Out.WriteLine((ControlChars.Lf + "".PadLeft(ligne.Length, "-"c) + ControlChars.Lf + ligne + ControlChars.Lf + "".PadLeft(ligne.Length, "-"c) + ControlChars.Lf))

            ' 显示数据
            While myReader.Read()
                ' 处理当前行
                ligne = ""
                For i = 0 To myReader.FieldCount - 1
                    ligne += myReader(i).ToString + " "
                Next i
                Console.WriteLine(ligne)
            End While

唯一的难点在于将当前行各列的值进行拼接的语句:


                For i = 0 To myReader.FieldCount - 1
                    ligne += myReader(i).ToString + " "
                Next i

表达式 *行+=myReader(i).ToString 被转换为 *行+=myReader.Item(i).ToString。ToString(),其中 Item(i) 是当前行第 i 列的值。

7.3.2.4. 资源释放

OdbcReaderOdbcConnection 都拥有一个 Close() 方法,该方法会释放与被关闭对象相关的资源。

                ' 关闭读取器
                myReader.Close()
                ' 关闭连接
                articlesConn.Close()

7.3.3. 使用 OLE 驱动程序 DB

我们继续使用相同的示例,这次通过 OLE DB 驱动程序访问数据库。 .NET 平台为 ACCESS 数据库提供了此类驱动程序。因此,我们将使用与之前相同的 articles.mdb 数据库。我们在此旨在说明:即使类发生变化,概念依然不变:

  • 连接由一个 OleDbConnection 对象表示
  • 通过 OleDbCommand 对象发出 SQL 查询
  • 若该请求为 SELECT 子句,则将返回 OleDbDataReader 对象以访问结果表中的行

这些类位于命名空间 System.Data.OleDb 中。上述程序可轻松修改以支持 OLE 和 DB 数据库:

  • 将所有 OdbcXX 替换为 OleDbXX
  • 修改连接字符串。对于无需登录名/密码的 ACCESS 数据库,连接字符串应为 Provider=Microsoft.JET.OLEDB.4.0;Data Source=[fichier.mdb]。 该字符串中可配置的部分是待使用的文件名 ACCESS。我们将修改程序,使其能够接受该文件名作为参数。
  • 现在要导入的命名空间是 System.Data.OleDb

我们的程序变为如下所示:


' 选项
Option Explicit On 
Option Strict On

' 命名空间
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports Microsoft.VisualBasic
Imports System.Data.OleDb

Module db2
    Public Sub Main(ByVal args() As String)

        ' 控制台应用程序
        ' 显示数据库 DSN 中表 ARRTICLES 的内容 
        ' 其名称作为参数传递
        Const syntaxe As String = "syntaxe : pg base_access_articles"
        Const tabArticles As String = "articles"        ' la table des articles

        ' 参数验证
        ' 是否有 1 个参数
        If args.Length <> 1 Then
            ' 错误消息
            Console.Error.WriteLine(syntaxe)
            ' 结束
            Environment.Exit(1)
        End If

        ' 获取参数
        Dim dbArticles As String = args(0)        ' la base de données

        ' 准备连接数据库
        Dim articlesConn As OleDbConnection = Nothing        ' la connexion
        Dim myReader As OleDbDataReader = Nothing        ' le lecteur de données

        ' 尝试访问数据库

        Try
            ' 数据库连接字符串
            Dim connectString As String = "Provider=Microsoft.JET.OLEDB.4.0;Data Source=" + dbArticles + ";"
            articlesConn = New OleDbConnection(connectString)
            articlesConn.Open()

            ' 执行命令 SQL
            Dim sqlText As String = "select * from " + tabArticles
            Dim myOleDbCommand As New OleDbCommand(sqlText)
            myOleDbCommand.Connection = articlesConn
            myReader = myOleDbCommand.ExecuteReader()

            ' 查询检索到的表
            ' 显示列
            Dim ligne As String = ""
            Dim i As Integer
            For i = 0 To (myReader.FieldCount - 1) - 1
                ligne += myReader.GetName(i) + ","
            Next i
            ligne += myReader.GetName(i)
            Console.Out.WriteLine((ControlChars.Lf + "".PadLeft(ligne.Length, "-"c) + ControlChars.Lf + ligne + ControlChars.Lf + "".PadLeft(ligne.Length, "-"c) + ControlChars.Lf))
            ' 显示数据
            While myReader.Read()
                ' 处理当前行
                ligne = ""
                For i = 0 To myReader.FieldCount - 1
                    ligne += myReader(i).ToString + " "
                Next i
                Console.WriteLine(ligne)
            End While
        Catch ex As Exception
            Console.Error.WriteLine(("Erreur d'exploitation de la base de données (" + ex.Message + ")"))
            Environment.Exit(2)
        Finally
            ' 关闭读取器
                myReader.Close()
                ' 关闭连接
                articlesConn.Close()
        End Try
        ' 结束
        Environment.Exit(0)
    End Sub
End Module

所得结果:

dos>vbc liste.vb

E:\data\serge\MSNET\vb.net\adonet\6>dir
07/05/2002  15:09                2 325 liste.CS
07/05/2002  15:09                4 608 liste.exe
20/08/2001  11:54               86 016 ARTICLES.MDB

dos>liste articles.mdb

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

a300 vélo                           2500 10 5
b300 pompe                          56 62 45
c300 arc                            3500 10 20
d300 flèches - lot de 6             780 12 20
e300 combinaison de plongée         2800 34 7
f300 bouteilles d'oxygène           800 10 5

7.3.4. 更新表

前面的示例仅列出了表的内容。我们将修改商品数据库管理程序,使其能够修改该数据库。该程序名为 sql。将其作为参数传递给要管理的商品数据库名称 DSN。 用户直接在键盘上输入 SQL 命令,程序会执行这些命令,如下所示,这是在 MySQL 物料数据库上获得的结果:

dos>vbc /r:microsoft.data.odbc.dll sql.vb

dos>sql mysql-articles

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

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

a300 vélo                           2500 10 5
b300 pompe                          56 62 45
c300 arc                            3500 10 20
d300 flèches - lot de 6             780 12 20
e300 combinaison de plongée         2800 34 7
f300 bouteilles d'oxygène           800 10 5

Requête SQL (fin pour arrêter) : select * from articles where stock_actuel<stock_minimum

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

c300 arc                            3500 10 20
d300 flèches - lot de 6             780 12 20

Requête SQL (fin pour arrêter) : insert into articles values ("1","1",1,1,1)
Il y a eu 1 ligne(s) modifiée(s)

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

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

a300 vélo                           2500 10 5
b300 pompe                          56 62 45
c300 arc                            3500 10 20
d300 flèches - lot de 6             780 12 20
e300 combinaison de plongée         2800 34 7
f300 bouteilles d'oxygène           800 10 5
1 1 1 1 1

Requête SQL (fin pour arrêter) : update articles set nom="2" where nom="1"
Il y a eu 1 ligne(s) modifiée(s)

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

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

a300 vélo                           2500 10 5
b300 pompe                          56 62 45
c300 arc                            3500 10 20
d300 flèches - lot de 6             780 12 20
e300 combinaison de plongée         2800 34 7
f300 bouteilles d'oxygène           800 10 5
1 2 1 1 1

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

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

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

a300 vélo                           2500 10 5
b300 pompe                          56 62 45
c300 arc                            3500 10 20
d300 flèches - lot de 6             780 12 20
e300 combinaison de plongée         2800 34 7
f300 bouteilles d'oxygène           800 10 5

Requête SQL (fin pour arrêter) : select * from articles order by nom asc

----------------------------------------
code,nom,prix,stock_actuel,stock_minimum
----------------------------------------

c300 arc                            3500 10 20
f300 bouteilles d'oxygène           800 10 5
e300 combinaison de plongée         2800 34 7
d300 flèches - lot de 6             780 12 20
b300 pompe                          56 62 45
a300 vélo                           2500 10 5

Requête SQL (fin pour arrêter) : fin

日程安排如下:


' 选项
Option Explicit On 
Option Strict On

' 命名空间
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Data.OleDb
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic

Module db3
    Public Sub Main(ByVal args() As String)

        ' 控制台应用程序
        ' 在 
        ' ARTICLES 数据库中的一张表,该数据库的名称作为参数传递
        Const syntaxe As String = "syntaxe : pg dsnArticles"

        ' 参数验证
        ' 是否有2个参数
        If args.Length <> 1 Then
            ' 错误信息
            Console.Error.WriteLine(syntaxe)
            ' 结束
            Environment.Exit(1)
        End If        'if
        ' 获取参数
        Dim dsnArticles As String = args(0)
        ' 数据库连接字符串
        Dim connectString As String = "DSN=" + dsnArticles + ";"

        ' 准备连接数据库
        Dim articlesConn As OdbcConnection = Nothing
        Dim sqlCommand As OdbcCommand = Nothing
        Try
            ' 尝试访问数据库
            articlesConn = New OdbcConnection(connectString)
            articlesConn.Open()
            ' 创建一个命令对象
            sqlCommand = New OdbcCommand("", articlesConn)
            'try
        Catch ex As Exception
            ' 错误消息
            Console.Error.WriteLine(("Erreur d'exploitation de la base de données (" + ex.Message + ")"))
            ' 释放资源
            Try
                articlesConn.Close()
            Catch
            End Try
            Environment.Exit(2)
        End Try        'catch
        ' 构建已接受的 SQL 命令字典
        Dim commandesSQL() As String = {"select", "insert", "update", "delete"}
        Dim dicoCommandes As New Hashtable
        Dim i As Integer
        For i = 0 To commandesSQL.Length - 1
            dicoCommandes.Add(commandesSQL(i), True)
        Next i

        ' 读取并执行通过键盘输入的命令 SQL
        Dim requête As String = Nothing        ' texte de la requête SQL
        Dim champs() As String        ' les champs de la requête    
        Dim modèle As New Regex("\s+")
        ' 键盘输入命令的读取-执行循环
        While True
            ' 初始阶段无错误
            Dim erreur As Boolean = False
            ' 请求
            Console.Out.Write(ControlChars.Lf + "Requête SQL (fin pour arrêter) : ")
            requête = Console.In.ReadLine().Trim().ToLower()
            ' 结束了吗?
            If requête = "fin" Then
                Exit While
            End If
            ' 将请求拆分为字段
            champs = modèle.Split(requête)
            ' 请求有效?
            If champs.Length = 0 Or Not dicoCommandes.ContainsKey(champs(0)) Then
                ' 错误消息
                Console.Error.WriteLine("Requête invalide. Utilisez select, insert, update, delete")
                ' 下一个请求
                erreur = True
            End If
            If Not erreur Then
                ' 准备 Command 对象以执行请求
                sqlCommand.CommandText = requête
                ' 正在执行请求
                Try
                    If champs(0) = "select" Then
                        executeSelect(sqlCommand)
                    Else
                        executeUpdate(sqlCommand)
                    End If
                Catch ex As Exception
                    ' 错误消息
                    Console.Error.WriteLine(("Erreur d'exploitation de la base de données (" + ex.Message + ")"))
                End Try
            End If
        End While
        ' 释放资源
        Try
            articlesConn.Close()
        Catch
        End Try
        Environment.Exit(0)
    End Sub

    ' 执行更新请求
    Sub executeUpdate(ByVal sqlCommand As OdbcCommand)
        ' 执行 sqlCommand,更新请求
        Dim nbLignes As Integer = sqlCommand.ExecuteNonQuery()
        ' 显示
        Console.Out.WriteLine(("Il y a eu " & nbLignes & " ligne(s) modifiée(s)"))
    End Sub

    ' 执行 Select 查询
    Sub executeSelect(ByVal sqlCommand As OdbcCommand)
        ' 执行 sqlCommand,Select 查询
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' 对检索到的表进行处理
        ' 显示列
        Dim ligne As String = ""
        Dim i As Integer
        For i = 0 To (myReader.FieldCount - 1) - 1
            ligne += myReader.GetName(i) + ","
        Next i
        ligne += myReader.GetName(i)
        Console.Out.WriteLine((ControlChars.Lf + "".PadLeft(ligne.Length, "-"c) + ControlChars.Lf + ligne + ControlChars.Lf + "".PadLeft(ligne.Length, "-"c) + ControlChars.Lf))
        ' 显示数据
        While myReader.Read()
            ' 处理当前行
            ligne = ""
            For i = 0 To myReader.FieldCount - 1
                ligne += myReader(i).ToString + " "
            Next i
            ' 显示
            Console.WriteLine(ligne)
        End While
        ' 释放资源
        myReader.Close()
    End Sub
End Module

此处仅对与前一个程序相比的新内容进行说明:

  • 我们构建了一个支持的SQL命令词典:

        ' 构建已接受的 SQL 命令字典
        Dim commandesSQL() As String = {"select", "insert", "update", "delete"}
        Dim dicoCommandes As New Hashtable
        Dim i As Integer
        For i = 0 To commandesSQL.Length - 1
            dicoCommandes.Add(commandesSQL(i), True)
        Next i

这使我们能够轻松验证输入的查询中的第一个单词(字段[0])是否属于四条被接受的命令之一:


            ' 查询有效吗?
            If champs.Length = 0 Or Not dicoCommandes.ContainsKey(champs(0)) Then
                ' 错误消息
                Console.Error.WriteLine("Requête invalide. Utilisez select, insert, update, delete")
                ' 下一个查询
                erreur = True
            End If            'if
  • 此前,该查询已使用类 RegEx 中的方法 Split 拆分为字段:

        Dim modèle As New Regex("\s+")
....
            ' 将查询拆分为字段
            champs = modèle.Split(requête)

查询中的单词之间可以使用任意数量的空格分隔。

  • 执行查询 select 所使用的方法与执行更新查询(insertupdatedelete)所使用的方法不同。 因此,必须进行测试,并针对这两种情况分别执行不同的函数:

                ' 准备 Command 对象以执行查询
                sqlCommand.CommandText = requête
                ' 执行查询
                Try
                    If champs(0) = "select" Then
                        executeSelect(sqlCommand)
                    Else
                        executeUpdate(sqlCommand)
                    End If                    'try
                Catch ex As Exception
                    ' 错误消息
                    Console.Error.WriteLine(("Erreur d'exploitation de la base de données (" + ex.Message + ")"))
                End Try

执行查询 SQL 可能会引发异常,此处对此异常进行了处理。

  • 函数 executeSelect 综合了前面示例中的所有内容。
  • 函数 executeUpdate 使用类 OdbcCommand 中的方法 ExecuteNonQuery,该方法返回受该命令影响的行数。

7.3.5. IMPOTS

我们继续使用上一章中构建的 impôt 对象:


' 选项
Option Strict On
Option Explicit On 

' 命名空间
Imports System

Public Class impôt
    ' 计算税款所需的数据
    ' 来自外部来源
    Private limites(), coeffR(), coeffN() As Decimal

    ' 生成器
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' 验证这 3 个表是否大小相同
        Dim OK As Boolean = LIMITES.Length = COEFFR.Length And LIMITES.Length = COEFFN.Length
        If Not OK Then
            Throw New Exception("Les 3 tableaux fournis n'ont pas la même taille(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
        End If
        ' 没问题
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub

    ' 计算税额
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' 计算份额数
        Dim nbParts As Decimal
        If marié Then
            nbParts = CDec(nbEnfants) / 2 + 2
        Else
            nbParts = CDec(nbEnfants) / 2 + 1
        End If
        If nbEnfants >= 3 Then
            nbParts += 0.5D
        End If
        ' 计算应税收入及家庭系数
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' 税额计算
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' 返回结果
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

我们为其添加了一个新的构造函数,用于从数据库 ODBC 初始化数组 limitescoeffRcoeffN


Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
...


    ' 生成器 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' 根据
        ' 基于数据库 ODBC DSNimpots 中 Timpots 表的内容
        ' colLimites、colCoeffR、colCoeffN 是该表的三个列
        '可能会引发异常
        Dim connectString As String = "DSN=" + DSNimpots + ";"        ' chaîne de connexion à la base
        Dim impotsConn As OdbcConnection = Nothing        ' la connexion
        Dim sqlCommand As OdbcCommand = Nothing        ' la commande SQL
        ' 查询 SELECT
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' 用于检索数据的表
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' 尝试访问数据库
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' 正在创建一个命令对象
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' 正在执行查询
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' 对检索到的表进行处理
        While myReader.Read()
            ' 将当前行数据放入数组
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' 释放资源
        myReader.Close()
        impotsConn.Close()

        ' 将动态数组转换为静态数组
        Me.limites = New Decimal(tLimites.Count) {}
        Me.coeffR = New Decimal(tLimites.Count) {}
        Me.coeffN = New Decimal(tLimites.Count) {}
        Dim i As Integer
        For i = 0 To tLimites.Count - 1
            limites(i) = Decimal.Parse(tLimites(i).ToString())
            coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
            coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
        Next i
    End Sub

测试程序如下:它接收作为参数的参数,并将这些参数传递给类impôt的构造函数。在构建impôt对象后,它会计算应缴税额:


Option Explicit On 
Option Strict On

' 命名空间
Imports System
Imports Microsoft.VisualBasic

' 测试页面
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' 交互式税款计算程序
        ' 用户通过键盘输入三项数据:已婚 nbEnfants 工资
        ' 程序随后显示应缴税额
        Const syntaxe1 As String = "pg DSNimpots tabImpots colLimites colCoeffR colCoeffN"
        Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

        ' 验证程序参数
        If arguments.Length <> 5 Then
            ' 错误信息
            Console.Error.WriteLine(syntaxe1)
            ' 结束
            Environment.Exit(1)
        End If        'if
        ' 获取参数
        Dim DSNimpots As String = arguments(0)
        Dim tabImpots As String = arguments(1)
        Dim colLimites As String = arguments(2)
        Dim colCoeffR As String = arguments(3)
        Dim colCoeffN As String = arguments(4)

        ' 创建税款对象
        Dim objImpôt As impôt = Nothing
        Try
            objImpôt = New impôt(DSNimpots, tabImpots, colLimites, colCoeffR, colCoeffN)
        Catch ex As Exception
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
            Environment.Exit(2)
        End Try

        ' 无限循环
        While True
            ' 初始时无错误
            Dim erreur As Boolean = False

            ' 请求税费计算参数
            Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
            Dim paramètres As String = Console.In.ReadLine().Trim()

            ' 需要采取什么措施?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If

            ' 验证输入行中的参数数量
            Dim args As String() = paramètres.Split(Nothing)
            Dim nbParamètres As Integer = args.Length
            If nbParamètres <> 3 Then
                Console.Error.WriteLine(syntaxe2)
                erreur = True
            End If
            Dim marié As String
            Dim nbEnfants As Integer
            Dim salaire As Integer
            If Not erreur Then
                ' 参数有效性验证
                ' 已婚
                marié = args(0).ToLower()
                If marié <> "o" And marié <> "n" Then
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                    erreur = True
                End If
                ' nbEnfants
                nbEnfants = 0
                Try
                    nbEnfants = Integer.Parse(args(1))
                    If nbEnfants < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
                    erreur = True
                End Try
                ' 工资
                salaire = 0
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
                    erreur = True
                End Try
            End If
            If Not erreur Then
                ' 参数正确 - 计算税款
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

所使用的数据库是名为 DSN mysql-impots 的 MySQL 数据库:

C:\mysql\bin>mysql --database=impots --user=admimpots --password=mdpimpots
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 5 to server version: 3.23.49-max-debug

Type 'help' for help.

mysql> show tables;
+------------------+
| Tables_in_impots |
+------------------+
| timpots          |
+------------------+

mysql> select * from timpots;
+---------+--------+---------+
| limites | coeffR | coeffN  |
+---------+--------+---------+
|   12620 |      0 |       0 |
|   13190 |   0.05 |     631 |
|   15640 |    0.1 |  1290.5 |
|   24740 |   0.15 |  2072.5 |
|   31810 |    0.2 |  3309.5 |
|   39970 |   0.25 |    4900 |
|   48360 |    0.3 |    6898 |
|   55790 |   0.35 |  9316.5 |
|   92970 |    0.4 |   12106 |
|  127860 |   0.45 |   16754 |
|  151250 |    0.5 | 23147.5 |
|  172040 |   0.55 |   30710 |
|  195000 |    0.6 |   39312 |
|       0 |   0.65 |   49062 |
+---------+--------+---------+

运行测试程序得到以下结果:

dos>D:\data\devel\vbnet\poly\chap6\impots>vbc /r:system.data.dll /r:microsoft.data.odbc.dll /r:system.dll /t:library impots.vb

dos>vbc /r:impots.dll testimpots.vb

dos>test mysql-impots timpots limites coeffr coeffn
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22506 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 2 200000
impôt=33388 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 3 200000
impôt=16400 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 3 300000
impôt=50082 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 3 200000
impôt=22506 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :