Skip to content

2. 第 2 部分

2.1. Introduction

首先,我们将回顾第一部分的内容,特别是所采用的三层架构。在提出的解决方案中,[dao]层是一个测试层:数据源由一个[ArrayList]对象实现。 本文将重点探讨[dao]层,并介绍当数据位于SGBD中时,该层的多种可能实现方案。

使用的工具

  • Firebird SGBD——参见附录第 3.5 节。
  • SGBD MSDE(Microsoft Data Engine)——参见附录第3.12节。
  • IBExpert,个人版,用于图形化管理 SGBD Firebird —— 参见附录第 3.6 节。
  • EMS MS SQL Manager 用于图形化管理 SGBD MSDE - 参见附录第 3.14 节。
  • Ibatis SqlMap 用于 SGBD 的数据访问层 - 参见第 2.5.6.2 节。

按照初级-中级-高级的难度等级划分,本文档属于[intermédiaire-avancé]部分。理解本文档需要具备一定的先决条件。其中部分内容可通过我撰写的其他文档获取。若涉及此类情况,我会予以注明。当然,这仅供参考,读者也可使用自己喜欢的文档。

  • VB.net 语言:[Introduction au langage VB.NET par l'exemple ]
  • VB.net 中的 Web 编程:[Développement WEB avec ASP.NET 1.1 ]
  • Spring 框架的应用:IoC
  • Ibatis 文档 SqlMap:[http://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Firebird 文档:[http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf]
  • Spring.net 文档:[http://www.springframework.net/documentation.html]

2.2. webarticles 应用程序 - 回顾

本文将介绍第 1 部分中研究的简化电子商务 Web 应用程序的组成部分。该应用程序允许 Web 用户:

  • 浏览来自数据库的商品列表
  • 将部分商品放入电子购物车
  • 确认购物车。此确认操作仅用于在数据库中更新所购商品的库存。

2.2.1. 应用程序的视图

向用户展示的视图包括:

  • 视图 [ERREURS],用于报告应用程序中的任何错误

Image

2.2.2. 应用程序的总体架构

第 1 部分构建的应用程序采用三层架构:

  • 通过使用接口,使这三层相互独立
  • 各层之间的集成通过Spring实现
  • 每层均采用独立的命名空间:web(Web层)、domain(业务层)和dao(数据访问层)。

该应用程序遵循MVC(模型-视图-控制器)架构。若参照上文的分层图,MVC架构的整合方式如下:

客户端请求的处理流程如下:

  1. 客户端向控制器发送请求。此处的控制器是一个承担特殊角色的 .aspx 页面。它处理所有客户端的请求,是应用程序的入口,即 MVC 架构中的“C”。
  2. 控制器处理该请求。为此,它可能需要业务层的协助,即MVC架构中的M模型。
  3. 控制器从业务层收到响应。客户端的请求已得到处理。这可能导致多种可能的响应。一个典型的例子是
    • 如果请求无法正确处理,则显示错误页面
    • 否则则返回确认页面
  4. 控制器选择要发送给客户端的响应(即视图)。该响应通常是一个包含动态元素的页面。控制器将这些元素提供给视图。
  5. 视图被发送给客户端。这就是MVC中的“V”。

2.2.3. 模型

MVC中的模型M由以下元素组成:

  1. 业务类
  2. 数据访问类
  3. 数据库

2.2.3.1. 数据库

该数据库仅包含一个名为 ARTICLES 的表,该表由以下 SQL 命令生成:

CREATE TABLE ARTICLES (
    ID            INTEGER NOT NULL,
    NOM           VARCHAR(20) NOT NULL,
    PRIX          NUMERIC(15,2) NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);
/* 限制条件 */
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);
/* 主键 */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
唯一标识商品的主键
nom
商品名称
prix
其价格
stockactuel
当前库存
stockminimum
库存低于该数值时需下达补货订单

2.2.3.2. 模型的命名空间

模型 M 以两个命名空间的形式提供:

  • istia.st.articles.dao:包含 [dao] 层的数据访问类
  • istia.st.articles.domain:包含 [domain] 层面的业务类

每个命名空间都包含在一个专属的“程序集”文件中:

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]:访问 [dao] 层的接口。这是 [domain] 层唯一可见的接口,它看不到其他接口。
- [Article]:定义文章的类
- [ArticlesDaoArrayList]:实现 [IArticlesDao] 接口的类,其中包含 [ArrayList]
数据访问层——完全位于Web应用程序三层架构的[dao]层中
webarticles-domain
- [IArticlesDomain]:访问 [domain] 层的接口。这是 Web 层唯一可见的接口,它看不到其他接口。
- [AchatsArticles]:一个实现 [IArticlesDomain] 的类
- [Achat]:表示客户购买记录的类
- [Panier]:表示客户所有购买记录的类
代表网络购物模型- 完全位于Web应用程序三层架构的[domain]层中

2.2.4. [webarticles] 应用程序的部署与测试

2.2.4.1. 部署

我们将本文第 1 部分中开发的应用程序部署到一个名为 [runtime] 的文件夹中:

 

注释

文件夹 [runtime] 包含三个文件和两个子文件夹:

  • 控制器文件 [global.asax] 和 [main.aspx]
  • 配置文件 [web.config]
  • 文件夹 [bin],其中包含:
    • 三个层级的 DLL 文件(分别为 [webarticles-dao.dll]、[webarticles-domain.dll]、[webarticles-web.dll])
    • Spring所需文件[Spring-Core.*]、[log4net.dll]
  • 包含各视图呈现代码的文件夹 [vues]。
  • 无需保留 .vb 代码文件,因为其编译版本已包含在 DLL 中。

2.2.4.2. 测试

我们将 Web 服务器 [Cassini] 配置如下:

Image

配置如下:

物理路径:D:\data\serge\travail\2004-2005\aspnet\webarticles-010405\runtime\

虚拟路径:/webarticles

使用浏览器访问 URL [http://localhost/webarticles/main.aspx]

Image

需要说明的是,[dao]层由一个类实现,该类将文章存储在[ArrayList]对象中。该类创建了一个包含四个文章的初始列表。根据上图所示,我们使用菜单中的链接进行操作。以下是其中的一些操作。 左侧列代表客户请求,右侧列代表给客户的响应。

2.2.5. [dao] 层的重新审视

在我们对 [dao] 层的首个实现中,数据访问接口 [IArticlesDao] 通过一个将文章存储在 [ArrayList] 对象中的类来实现。 这使我们无需过多关注该层,并证明了只有其接口才是关键,而非具体实现。由此,我们成功构建了一个可运行的Web应用程序。该应用程序包含三个层:[web]、[domain]和[dao]。 本文将提供 [dao] 层的不同实现方案。 每种实现方案均可替代当前的 [dao] 层,且无需对 [domain] 和 [web] 层进行任何修改。之所以能实现这种灵活性,是因为:

  • [domain] 层不是针对具体类,而是针对 [IArticlesDao] 接口
  • 借助 Spring,我们能够向 [domain] 层隐藏 [IArticlesDao] 接口的实现类名称。

2.2.5.1. [dao] 层的组成部分

回顾 [dao] 层中将在新实现中保留的某些元素:

  • - [IArticlesDao]:访问 [dao] 层的接口
  • - [Article]:定义文章的类

2.2.5.2. [Article] 类

定义文章的类如下:

Imports System

Namespace istia.st.articles.dao

    Public Class Article

         ' 私有字段
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

         ' 商品编号
        Public Property id() As Integer
            Get
                Return _id
            End Get
            Set(ByVal Value As Integer)
                If Value <= 0 Then
                    Throw New Exception("Le champ id [" + Value.ToString + "] est invalide")
                End If
                Me._id = Value
            End Set
        End Property

         ' 商品名称
        Public Property nom() As String
            Get
                Return _nom
            End Get
            Set(ByVal Value As String)
                If Value Is Nothing OrElse Value.Trim.Equals("") Then
                    Throw New Exception("Le champ nom [" + Value + "] est invalide")
                End If
                Me._nom = Value
            End Set
        End Property

         ' 商品价格
        Public Property prix() As Double
            Get
                Return _prix
            End Get
            Set(ByVal Value As Double)
                If Value < 0 Then
                    Throw New Exception("Le champ prix [" + Value.ToString + "] est invalide")
                End If
                Me._prix = Value
            End Set
        End Property

         ' 当前库存
        Public Property stockactuel() As Integer
            Get
                Return _stockactuel
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Le champ stockActuel [" + Value.ToString + "] est invalide")
                End If
                Me._stockactuel = Value
            End Set
        End Property

         ' 商品最低库存
        Public Property stockminimum() As Integer
            Get
                Return _stockminimum
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Le champ stockMinimum [" + Value.ToString + "] est invalide")
                End If
                Me._stockminimum = Value
            End Set
        End Property

         ' 默认制造商
        Public Sub New()
        End Sub

         ' 带属性的制造商
        Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
            Me.id = id
            Me.nom = nom
            Me.prix = prix
            Me.stockactuel = stockactuel
            Me.stockminimum = stockminimum
        End Sub

         ' 商品识别方法
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

该类提供:

  1. 一个构造函数,用于设置文章的5项信息:[id, nom, prix, stockactuel, stockminimum]
  2. 用于读取和写入这5项信息的公共属性。
  3. 对插入到商品中的数据进行验证。若数据有误,将抛出异常。
  4. 方法 toString,用于获取商品的字符串值。这在应用程序调试中通常非常有用。

2.2.5.3. 接口 [IArticlesDao]

接口 [IArticlesDao] 定义如下:

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
         ' 所有商品列表
        Function getAllArticles() As IList
         ' 添加商品
        Function ajouteArticle(ByVal unArticle As Article) As Integer
         ' 删除商品
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
         ' 修改商品
        Function modifieArticle(ByVal unArticle As Article) As Integer
         ' 搜索商品
        Function getArticleById(ByVal idArticle As Integer) As Article
         ' 删除所有商品
        Sub clearAllArticles()
         ' 更改商品库存
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

接口中各方法的作用如下:

getAllArticles
返回数据源中的所有文章
clearAllArticles
清空数据源
getArticleById
返回由编号标识的对象 [Article]
ajouteArticle
允许向数据源添加一条记录
modifieArticle
允许修改数据源中的商品
supprimerArticle
允许从数据源中删除商品
changerStockArticle
允许修改数据源中某商品的库存

该接口向客户端程序提供了一系列仅由其签名定义的方法。它不涉及这些方法的实际实现方式。这为应用程序带来了灵活性。客户端程序调用的是接口,而非其具体的实现。

具体实现的选择是通过 Spring 配置文件来完成的。

2.3. 实现类 [ArticlesDaoPlainODBC]

我们提供了一个新的 [dao] 层实现,该实现假设数据位于 ODBC 源中。 众所周知,在 Windows 系统下,市面上几乎所有的 SGBD 都配备了 ODBC 驱动程序。该方案的优势在于,可以在应用程序完全不知情的情况下切换 SGBD。 其缺点在于,仅利用所有SGBD共性特征的ODBC驱动程序,通常其性能会低于专门为充分发挥特定SGBD全部潜力而编写的驱动程序。 可参阅第 3.7 节,了解 ODBC 源代码的创建示例。

2.3.1. 代码

2.3.1.1. 骨架

[ArticlesDaoPlainODBC] 类以如下方式实现了 [IArticlesDao] 接口:

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

Namespace istia.st.articles.dao

    Public Class ArticlesDaoPlainODBC
        Implements istia.st.articles.dao.IArticlesDao

         ' 私有字段
        Private connexion As OdbcConnection = Nothing
        Private DSN As String
        Private insertCommand As OdbcCommand
        Private updatecommand As OdbcCommand
        Private deleteSomeCommand As OdbcCommand
        Private selectSomeCommand As OdbcCommand
        Private updateStockCommand As OdbcCommand
        Private deleteAllCommand As OdbcCommand
        Private selectAllCommand As OdbcCommand

         ' 制造商
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
             ' DSN:源名称 ODBC
             ' uid:用户标识
             ' 密码:其密码
...
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
...
        End Function

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
...
        End Function

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
...
        End Sub

        Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
...
        End Function

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
...
        End Function

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
...
        End Function

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
....
        End Function

        Private Function executeQuery(ByVal query As OdbcCommand) As IList
             ' 执行一个请求 SELECT 
....
        End Function

        Private Function executeUpdate(ByVal sqlCommand As OdbcCommand) As Integer
....
    End Class

End Namespace

注释

  • 第 3 行,导入包含用于访问 ODBC 源文件的 .NET 类命名空间
  • 第 11 行 - 将 ODBC 数据源的连接信息存储起来
  • 第 12 行 - 存储数据源的名称 DSN
  • 第 13-19 行 - 类型为 [OdbcCommand] 的私有变量,用于定义类中各种方法所使用的查询 SQL
  • 第22-27行 - 构造函数。它接收用于构建[OdbcConnection]对象的参数,该对象将代码与数据源ODBC关联起来
  • 第29-31行 - 添加商品的方法
  • 第 33-35 行 - 更改商品库存的方法
  • 第 37-39 行 - 用于从数据源 ODBC 中删除所有商品的方法
  • 第 41-43 行 - 获取数据源 ODBC 中所有商品列表的方法
  • 第 45-47 行 - 用于获取特定商品的方法
  • 第 49-51 行 - 根据商品编号修改该商品特定字段的方法
  • 第 53-55 行 - 根据商品编号删除商品的方法
  • 第 57-60 行 - 实用方法,用于对数据源执行 [SELECT] 并返回结果
  • 第 62-64 行 - 实用方法,用于在数据源上执行 [INSERT, UPDATE, DELETE] 并返回结果

2.3.1.2. 构造函数

         ' 生成器
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
             ' DSN:源名称ODBC
             ' uid:用户标识
             ' password:密码

             '获取作为参数传递的数据库名称
            Me.DSN = DSN
            Dim connectString As String = String.Format("DSN={0};UID={1};PASSWORD={2}", DSN, uid, password)
             '建立连接
            connexion = New OdbcConnection(connectString)
             ' 准备查询SQL
            insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
            updatecommand = New OdbcCommand("update ARTICLES set nom=?, prix=?, stockactuel=?, stockminimum=? where id=?", connexion)
            deleteSomeCommand = New OdbcCommand("delete from ARTICLES where id=?", connexion)
            selectSomeCommand = New OdbcCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=?", connexion)
            updateStockCommand = New OdbcCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)
            selectAllCommand = New OdbcCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New OdbcCommand("delete from ARTICLES", connexion)
        End Sub

注释

  • 第 2 行 - 构造函数接收连接 ODBC 数据源所需的三个信息:数据源名称 DSN、用于连接的用户名以及相应的密码。
  • 第 8 行 - 保存数据源名称 DSN,以便在错误消息中显示。
  • 第 9 行 - 实例化对象 [OdbcConnection]。实例化的连接不等于已建立的连接。由方法 [open] 负责建立连接。
  • 第12-19行——在对象[OdbcCommand]中准备查询SQL。这样可以避免每次需要时都重新构建它们。查询中的形式参数?将在执行查询时被实际值替换。

2.3.1.3. 方法 executeQuery

        Private Function executeQuery(ByVal query As OdbcCommand) As IList
             ' 执行查询SELECT 
             ' 声明允许访问结果表所有行的对象
            Dim myReader As OdbcDataReader = Nothing
            Try
                 '建立连接至 BDD
                connexion.Open()
                 '执行查询
                myReader = query.ExecuteReader()
                 '声明一个项目列表以便后续返回
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     '根据读取器的值准备一个商品
                    unArticle = New Article
                    unArticle.id = myReader.GetInt32(0)
                    unArticle.nom = myReader.GetString(1)
                    unArticle.prix = myReader.GetDouble(2)
                    unArticle.stockactuel = myReader.GetInt32(3)
                    unArticle.stockminimum = myReader.GetInt32(4)
                     '将商品添加到列表中
                    articles.Add(unArticle)
                End While
                 '返回结果
                Return articles
            Finally
                 '释放资源
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

注释

  • 方法 [executeQuery] 是一个实用方法,用于:
    • 在数据源上执行 [SELECT id, nom, prix, stockactuel, stockminimum from ARTICLES ...] 查询
    • 将结果以 [Article] 对象列表的形式返回
  • 第 1 行 - 该方法的唯一参数是 [OdbcCommand] 对象,其中包含待执行的 [Select] 查询。
  • 第 7 行 - 连接已打开。无论是否发生错误,该连接都将在第 29 行关闭。
  • 第 9 行 - 实例化了用于处理 [Select] 结果所需的 [OdbcDataReader] 对象
  • 第13-23行 - [Select]的每行结果都被放入一个[Article]对象中,该对象将与其他条目一起加入到[ArrayList]对象中
  • 第 25 行返回条目列表
  • 未处理任何异常。调用此方法的代码需自行处理异常。

2.3.1.4. 方法 executeUpdate

        Private Function executeUpdate(ByVal sqlCommand As OdbcCommand) As Integer
             ' 执行更新请求
            Try
                 '建立与 BDD 的连接
                connexion.Open()
                 '正在执行查询
                Return sqlCommand.ExecuteNonQuery()
            Finally
                 ' 释放资源
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

注释

  • 该方法接收一个 [OdbcCommand] 对象,其中包含一个类型为 [Insert, Update, Delete] 的 SQL 请求。
  • 连接在第 5 行打开。无论是否发生异常,该连接都将在第 10 行关闭。
  • 更新请求在第 7 行执行。随即返回结果,即该请求修改的 ARTICLES 表中的行数。

2.3.1.5. 方法 ajouteArticle

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' 独占段
            SyncLock Me
                 ' 准备插入请求
                With insertCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", unArticle.id))
                    .Add(New OdbcParameter("nom", unArticle.nom))
                    .Add(New OdbcParameter("prix", unArticle.prix))
                    .Add(New OdbcParameter("stockactuel", unArticle.stockactuel))
                    .Add(New OdbcParameter("stockminimum", unArticle.stockminimum))
                End With
                Try
                     '执行该请求
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

注释

  • 第 1 行 - 该方法接收要添加到数据源 ODBC 中的条目。它返回受此操作影响的行数,即 c.a.d。1 或 0
  • 第 3 行和第 20 行 - 该方法是同步的。所有数据访问方法均如此。这意味着每次只有一个线程可以处理数据源。这可能过于保守。存在更好的替代方案,特别是将这些操作纳入事务中。 在此情况下,由 SGBD 负责管理并发访问。我们目前暂不打算引入事务概念。Spring 允许我们在 [domain] 层引入事务。我们或许会在另一篇文章中再探讨这一话题。
  • 第 5-12 行,为构造函数初始化的 [insertCommand] 对象的请求形式参数赋值。回顾一下该对象:
insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)

第7至11行提供了查询所需的5个参数值。

  • 第13-19行,执行该请求。若执行成功,则返回结果;否则,抛出一个带有明确错误信息的通用异常

2.3.1.6. 方法 modifieArticle

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' 独占段
            SyncLock Me
                 ' 正在准备更新查询
                With updatecommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("nom", unArticle.nom))
                    .Add(New OdbcParameter("prix", unArticle.prix))
                    .Add(New OdbcParameter("stockactuel", unArticle.stockactuel))
                    .Add(New OdbcParameter("stockminimum", unArticle.stockminimum))
                    .Add(New OdbcParameter("id", unArticle.id))
                End With
                 ' 正在执行
                Try
                     '正在执行插入请求
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

注释

  • 第 1 行 - 该方法从数据源 ODBC 接收待修改的条目。它返回受此操作影响的行数,c.a.d。1 或 0
  • 方法 [ajouteArticle] 的注释可在此处引用

2.3.1.7. 方法 supprimeArticle

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' 独占段
            SyncLock Me
                 ' 准备删除查询
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                 '正在执行
                Try
                     '正在执行删除查询
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la suppression de l'article [id={0}] : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

注释

  • 第 1 行 - 该方法从数据源 ODBC 接收待删除的商品编号。它返回受此操作影响的行数,c.a.d。1 或 0
  • 方法 [ajouteArticle] 的注释可在此处引用

2.3.1.8. 方法 getAllArticles

            Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' 独占段
            SyncLock Me
                Try
                     '正在执行SELECT查询
                    Dim articles As IList = executeQuery(selectAllCommand)
                     '返回列表
                    Return articles
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de l'obtention des articles [select id,nom,prix,stockactuel,stockminimum from articles]: {0}", ex.Message))
                End Try
            End SyncLock
        End Function

注释

  • 第 1 行 - 该方法不接收任何参数。它返回数据源中所有条目的列表 ODBC
  • 在第 6 行,向方法 [executeQuery] 发送了查询 [Select] 以获取所有商品
  • 第8行返回获取到的列表
  • 第 9-12 行,处理可能出现的异常

2.3.1.9. 方法 getArticleById

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' 独占段
            SyncLock Me
                 ' 正在准备 select 查询
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                 '正在执行
                Try
                     '正在执行查询
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     '检测是否找到文章
                    If articles.Count = 0 Then Return Nothing
                     '返回商品
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

注释

  • 第1行——该方法接收所需商品编号作为参数。若在源文件ODBC中找到该商品,则返回该商品;否则返回引用[nothing]。
  • 查询该商品的请求 [Select] 在第 5-8 行初始化
  • 该查询在第12行执行——返回商品列表
  • 若该列表为空,则在第14行返回引用[nothing]
  • 否则,在第16行返回列表中的唯一商品
  • 第17-20行,处理可能出现的异常

2.3.1.10. 方法 clearAllArticles

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' 独占段
            SyncLock Me
                Try
                     '执行删除请求
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     '请求错误
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

注释

  • 第1行——该方法不接收任何参数,也不返回任何内容
  • 第 6 行 - 执行删除所有文章的请求
  • 第 7-10 行,处理可能出现的异常

2.3.1.11. 方法 changerStockArticle

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' 独占段
            SyncLock Me
                 ' 正在准备库存更新请求
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("mvt1", mouvement))
                    .Add(New OdbcParameter("id", idArticle))
                    .Add(New OdbcParameter("mvt2", mouvement))
                End With
                 '正在执行
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     '请求错误
                    Throw New Exception(String.Format("Erreur lors du changement de stock [idArticle={0}, mouvement={1}] : [{2}]", idArticle, mouvement, ex.Message))
                End Try
            End SyncLock
        End Function

注释

  • 第1行 - 该方法接收需修改库存的商品编号及其库存增量(正数或负数)作为参数。它返回由操作c.a.d修改的行数。0或1。
  • 第5-10行,初始化查询[updateStockCommand]。回顾查询SQL的文本:
            updateStockCommand = New OdbcCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)

请注意,库存仅在修改后仍大于等于0时才会被修改。

  • 第13行执行了商品库存更新请求,并返回了结果
  • 第14-18行,处理可能出现的异常

2.3.2. 生成 [dao] 层的程序集

此新版本 [dao] 层的 Visual Studio 项目结构如下:

Image

该项目配置为生成一个名为 [webarticles-dao.dll] 的 DLL:

2.3.3. [dao] 层的 Nunit 测试

2.3.3.1. 创建 的ODBC-Firebird数据源

为了测试我们的新层 [dao],我们需要一个 ODBC 数据源,因此需要一个数据库。 我们使用 Firebird(第 3.5 节)。通过 IBExpert(第 3.6 节),我们创建了以下商品数据库:

该数据库的管理员将是用户 [SYSDBA],密码为 [masterkey]。我们创建一些文章:

Image

现在,我们创建以下 Firebird 源 ODBC(参见第 3.7 节):

生成的源文件 ODBC 具有以下特征:

  • 名称 DSN:odbc-firebird-articles
  • 连接标识:SYSDBA
  • 关联密码:masterkey

2.3.3.2. 测试类 NUnit

我们已经为最初构建的 [dao] 层编写了一个测试类。如果读者还记得,该类测试的并非某个具体类,而是 [IArticlesDao] 接口:

Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports System.Threading
Imports Spring.Objects.Factory.Xml
Imports System.IO

Namespace istia.st.articles.tests

    <TestFixture()> _
    Public Class NunitTestArticlesArrayList
         ' 待测试对象
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' 获取 Spring 对象生成器的实例
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
             ' 请求实例化 articlesdao 对象
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

....

可以看出,在属性方法 <Setup()> 中,我们要求 Spring 提供一个名为 [articlesdao] 的单例的引用,其类型为 [IArticlesDao],即接口的类型。 单例 [articlesdao] 由以下配置文件 [spring-config.xml] 定义:

<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">

<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao"/>
</objects>

让我们证明,初始测试类允许我们在无需修改或重新编译的情况下测试我们的新层 [dao]。

  • 在新层 [dao] 的 Visual Studio 文件夹中,创建文件夹 [tests] (如下图右侧所示),方法是将初始层 [dao] 测试项目中的 [bin] 文件夹复制过来(如下图左侧所示)。 如有需要,读者可参阅本文第一部分中关于 [dao] 层初始版本的测试项目。
  • 在文件夹 [tests] 中,用 DLL [webarticles-dao.dll] 替换来自旧层 [dao] 中的 DLL [webarticles-dao.dll] 替换为来自新层 [dao] 的 DLL [webarticles-dao.dll]
  • 修改配置文件 [spring-config.xml] 以实例化新类 [ArticlesDaoPlainODBC]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">

<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoPlainODBC, webarticles-dao">
        <constructor-arg index="0">
            <value>odbc-firebird-articles</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>SYSDBA</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>masterkey</value>
        </constructor-arg>
    </object>
</objects>

注释

  • 第 6 行,对象 [articlesdao] 现已关联到类 [ ArticlesDaoPlainODBC] 的一个实例
  • 该类有一个带三个参数的构造函数:
  • 源名称 DSN - 第 8 行
  • 用于访问数据库的身份 - 第 11 行
  • 与该身份关联的密码 - 第 14 行

这里我们采用了之前创建的 ODBC-Firebird 源中的信息。

2.3.3.3. 测试

现在我们可以进行测试了。借助应用程序 [Nunit-Gui], 我们加载上述文件夹中的 DLL 和 [test-webarticles-dao.dll],并执行测试 [testGetAllArticles]:

Image

观察上方的屏幕截图,我们可能会对测试类最初命名为 [NUnitTestArticlesDaoArrayList] 感到遗憾。这容易引起混淆。实际上,这里测试的是 [ArticlesDaoPlainODBC] 类。 截图显示,我们已正确检索到之前放入表 [ARTICLES] 中的条目。现在,让我们进行全部测试:

Image

在左侧窗口中,可以看到已测试方法的列表。每个方法名称前面的圆点颜色表示该方法的测试结果:成功(绿色)或失败(红色)。在屏幕上查看本文件的读者会发现,所有测试均已通过。

2.3.3.4. 结论

我们刚刚证明了:

  • 由于测试类 NUnit 引用的不是类而是接口;
  • 因为该接口的实例化类确切名称是在配置文件中而非代码中提供的;
  • 由于 Spring 负责实例化该类并将引用传递给测试代码;

因此,为初始 [dao] 层编写的测试代码,对于该层的新实现仍然有效。我们无需访问测试类的源代码。 我们仅使用了其编译版本,即在测试初始 [dao] 层时生成的版本。当需要将新的 [dao] 层集成到 [webarticles] 应用程序中时,我们将得出类似的结论。

2.3.4. 将新层[dao]集成到应用程序[webarticles]中

2.3.4.1. 集成测试

需要提醒的是,应用程序 [webarticles] 的初始版本已部署在以下 [runtime] 文件夹中:

 

建议读者参阅第 2.2.4 节,该节详细说明了 [webarticles] 应用程序的部署方式。我们对 [runtime] 文件夹的内容进行了以下修改:

  • 在文件夹 [bin] 中,旧层 [dao] 中的 DLL 被新层 [dao] 中的 DLL 所替换
  • 在 [runtime] 中,配置文件 [web.config] 被替换为一个考虑了 [dao] 层新实现类的文件:

新的配置文件 [web.config] 如下:

<?xml version="1.0" encoding="iso-8859-1" ?>
<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 type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
            <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoPlainODBC, webarticles-dao">
                <constructor-arg index="0">
                    <value>odbc-firebird-articles</value>
                </constructor-arg>
                <constructor-arg index="1">
                    <value>SYSDBA</value>
                </constructor-arg>
                <constructor-arg index="2">
                    <value>masterkey</value>
                </constructor-arg>
            </object>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

注释

  • 第 14-24 行将新类 [ArticlesDaoPlainODBC] 的一个实例与单例 [articlesDao] 关联起来。这是唯一的修改。我们在测试新层 [dao] 时已经遇到过这种情况。

我们已准备好进行测试。我们将按照第 2.2.4 节中的方法配置 Web 服务器 [Cassini]。我们将商品表 [Firebird] 初始化为以下值:

Image

请确保 Cassini Web 服务器以及 SGBD 和 [Firebird] 已启动。使用浏览器访问 URL [http://localhost/webarticles/main.aspx]:

Image

现在,让我们检查数据库 [Firebird] 中表 [ARTICLES] 的内容:

Image

商品 [parapluie] 和 [bottes] 已采购,其库存已按采购数量减少。商品 [chapeau] 无法采购,因为需求数量超过了库存数量。 建议读者进行进一步测试。

2.3.4.2. 结论

我们做了什么?

  • 我们恢复了旧版本的部署版本;
  • 我们将 [dao] 层中的 DLL 替换为新版本。 [web]和[domain]层中的DLL保持不变;
  • 我们修改了配置文件 [web.config],使其能够支持 [dao] 层的新实现类

这一切都十分简洁,并为 Web 应用程序提供了极大的扩展灵活性。这些重要特性源于两项架构选择:

  • 通过接口访问各层
  • 通过 Spring 进行层的集成与配置

我们现提供 [dao] 层的新实现。

2.4. 实现类 [ArticlesDaoSqlServer]

[dao] 层的第二个实现假设数据位于 SQL Server 数据库中。 微软提供了一个名为 MSDE 的 SGBD,它是 SQL Server 的精简版。关于如何获取并安装该版本,请参见附录第 3.12 节。

2.4.1. 代码

[ArticlesDaoSqlServer] 类与之前探讨的 [ArticlesDaoPlainODBC] 类非常相似。因此,我们仅说明相较于上一版本所做的更改:

  • 所需类位于命名空间 [System.Data.SqlClient] 中,而非 [System.Data.Odbc]
  • 类型为 [OdbcConnection] 的连接现改为类型 [SqlConnection]
  • [OdbcCommand] 对象现采用 [SqlCommand] 类型
  • 带参数的 SQL 查询语法发生变更。插入查询现变为:
insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)

而此前为:

insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
  • [ajouteArticle]方法现变为:
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' 独占段
            SyncLock Me
                 ' 准备插入查询
                With insertCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", unArticle.id))
                    .Add(New SqlParameter("@nom", unArticle.nom))
                    .Add(New SqlParameter("@prix", unArticle.prix))
                    .Add(New SqlParameter("@sa", unArticle.stockactuel))
                    .Add(New SqlParameter("@sm", unArticle.stockminimum))
                End With
                Try
                     '执行该查询
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function
  • 制造商也进行了修改:
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' 服务器:目标实例名称 SQL
             ' databaseName:目标数据库名称
             ' uid:用户标识
             ' password:其密码

             '获取作为参数传递的数据库名称
            Me.databaseName = databaseName
             '建立连接
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
             ' 准备查询 SQL
            insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
...
        End Sub

该构造函数现在支持四个参数:

             ' 服务器:目标实例名称 SQL
             ' databaseName:目标数据库名称
             ' uid:用户标识
             ' 密码:其密码

类 [ArticlesDaoSqlServer] 的完整代码如下:

Imports System
Imports System.Collections
Imports System.Data.SqlClient

Namespace istia.st.articles.dao

    Public Class ArticlesDaoSqlServer
        Implements istia.st.articles.dao.IArticlesDao

         ' 私有字段
        Private connexion As SqlConnection = Nothing
        Private databaseName As String
        Private insertCommand As SqlCommand
        Private updatecommand As SqlCommand
        Private deleteSomeCommand As SqlCommand
        Private selectSomeCommand As SqlCommand
        Private updateStockCommand As SqlCommand
        Private deleteAllCommand As SqlCommand
        Private selectAllCommand As SqlCommand

         ' 创建者
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' 服务器:目标实例名称 SQL
             ' databaseName:目标数据库名称
             ' uid:用户标识
             ' password:其密码

             '获取作为参数传递的数据库名称
            Me.databaseName = databaseName
             '建立连接
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
             ' 准备查询SQL
            insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
            updatecommand = New SqlCommand("update ARTICLES set nom=@nom, prix=@prix, stockactuel=@sa, stockminimum=@sm where id=@id", connexion)
            deleteSomeCommand = New SqlCommand("delete from ARTICLES where id=@id", connexion)
            selectSomeCommand = New SqlCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=@id", connexion)
            updateStockCommand = New SqlCommand("update ARTICLES set stockactuel=stockactuel+@mvt where id=@id and (stockactuel+@mvt)>=0", connexion)
            selectAllCommand = New SqlCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New SqlCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' 独占段
            SyncLock Me
                 ' 准备插入查询
                With insertCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", unArticle.id))
                    .Add(New SqlParameter("@nom", unArticle.nom))
                    .Add(New SqlParameter("@prix", unArticle.prix))
                    .Add(New SqlParameter("@sa", unArticle.stockactuel))
                    .Add(New SqlParameter("@sm", unArticle.stockminimum))
                End With
                Try
                     '执行该查询
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' 独占段
            SyncLock Me
                 ' 正在准备库存更新查询
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@mvt", mouvement))
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 '正在执行
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     '请求错误
                    Throw New Exception(String.Format("Erreur lors du changement de stock [idArticle={0}, mouvement={1}] : [{2}]", idArticle, mouvement, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' 独占段
            SyncLock Me
                Try
                     '正在执行插入请求
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

        Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' 独占段
            SyncLock Me
                Try
                     '正在执行 SELECT 查询
                    Dim articles As IList = executeQuery(selectAllCommand)
                     '返回列表
                    Return articles
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de l'obtention des articles [select id,nom,prix,stockactuel,stockminimum from articles]: {0}", ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' 独占段
            SyncLock Me
                 ' 正在准备 select 查询
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 '正在执行
                Try
                     '正在执行查询
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     '检测是否找到文章
                    If articles.Count = 0 Then Return Nothing
                     '返回商品
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' 独占段
            SyncLock Me
                 ' 准备更新请求
                With updatecommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@nom", unArticle.nom))
                    .Add(New SqlParameter("@prix", unArticle.prix))
                    .Add(New SqlParameter("@sa", unArticle.stockactuel))
                    .Add(New SqlParameter("@sm", unArticle.stockminimum))
                    .Add(New SqlParameter("@id", unArticle.id))
                End With
                 ' 执行该查询
                Try
                     '正在执行插入查询
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' 独占段
            SyncLock Me
                 ' 准备删除查询
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 '正在执行
                Try
                     '正在执行删除查询
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la suppression de l'article [id={0}] : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Private Function executeQuery(ByVal query As SqlCommand) As IList
             ' 执行查询 SELECT 
             ' 声明对象以访问结果表中的所有行
            Dim myReader As SqlDataReader = Nothing
            Try
                 '建立连接至 BDD
                connexion.Open()
                 '执行查询
                myReader = query.ExecuteReader()
                 '声明一个项目列表以便后续返回
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     '根据读取器的值准备一个商品
                    unArticle = New Article
                    unArticle.id = myReader.GetInt32(0)
                    unArticle.nom = myReader.GetString(1)
                    unArticle.prix = myReader.GetDouble(2)
                    unArticle.stockactuel = myReader.GetInt32(3)
                    unArticle.stockminimum = myReader.GetInt32(4)
                     '将商品添加到列表中
                    articles.Add(unArticle)
                End While
                 '返回结果
                Return articles
            Finally
                 '释放资源
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

        Private Function executeUpdate(ByVal updateCommand As SqlCommand) As Integer
             ' 执行更新请求
            Try
                 '建立与 BDD 的连接
                connexion.Open()
                 '正在执行查询
                Return updateCommand.ExecuteNonQuery()
            Finally
                 ' 释放资源
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

请读者结合前文对类 [ArticlesDaoPlainODBC] 的说明来阅读此代码。

2.4.2. 生成 [dao] 层程序集

新的 Visual Studio 项目具有以下结构:

Image

该项目已配置为生成名为 [webarticles-dao.dll] 的 DLL:

2.4.3. [dao] 层的 Nunit 测试

2.4.3.1. 创建 数据源SQL Server

为了测试我们的新图层 [dao],我们需要一个数据源 SQL Server,因此还需要 SGBD SQL Server。 实际上,我们将使用 SGBD MSDE (MicroSoft 数据引擎)(第 3.12 节),该版本是 SQL 服务器的衍生版本,仅在支持的并发用户数量上有所限制。 使用 [EMS MS SQL Manager](第 3.14 段),我们在名为 [portable1_tahe\msde140405] 的 MSDE 实例中创建以下文章数据库:

Image

该数据库归用户 [mdparticles] 所有,密码为 [admarticles]。创建表 [ARTICLES] 的 Transact-SQL 命令如下:

CREATE TABLE [ARTICLES] (
  [id] int NOT NULL,
  [nom] varchar(20) COLLATE French_CI_AS NOT NULL,
  [prix] float(53) NOT NULL,
  [stockactuel] int NOT NULL,
  [stockminimum] int NOT NULL,
  CONSTRAINT [ARTICLES_uq] UNIQUE ([nom]),
  PRIMARY KEY ([id]),
  CONSTRAINT [ARTICLES_ck_id] CHECK ([id] > 0),
  CONSTRAINT [ARTICLES_ck_nom] CHECK ([nom] <> ''),
  CONSTRAINT [ARTICLES_ck_prix] CHECK ([prix] >= 0),
  CONSTRAINT [ARTICLES_ck_stockactuel] CHECK ([stockactuel] >= 0),
  CONSTRAINT [ARTICLES_ck_stockminimum] CHECK ([stockminimum] >= 0)
)
ON [PRIMARY]
GO

我们创建了一些文章:

Image

2.4.3.2. 测试类 NUnit

实现类 [ArticlesDaoSqlServer] 的 Nunit 测试类与类 [ArticlesDaoPlainODBC] 的测试类相同(参见第 2.3.3.2 节)。我们采用类似的方法来准备该类的 Nunit 测试:

  • 在 [dao-sqlserver] 项目的 Visual Studio 文件夹中,通过复制 [dao-odbc] 项目中的 [tests] 文件夹(左侧),创建 [tests] 文件夹(右侧):
  • 在项目 [dao-sqlserver] 的文件夹 [tests] 中, 我们将 DLL [webarticles-dao.dll] 替换为来自项目 [dao-sqlserver] 生成的 DLL [webarticles-dao.dll]
  • 我们修改配置文件 [spring-config.xml],以便实例化新类 [ArticlesDaoSqlServer]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
-->
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoSqlServer, webarticles-dao">
        <constructor-arg index="0">
            <value>portable1_tahe\msde140405</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>dbarticles</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>admarticles</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>mdparticles</value>
        </constructor-arg>
    </object>
</objects>

注释

  • 第 7 行,对象 [articlesdao] 现已关联到类 [ ArticlesDaoSqlServeur] 的一个实例
  • 该类有一个带四个参数的构造函数:
  • 所用实例 MSDE 的名称 - 第 9 行
  • 数据库名称 - 第12行
  • 用于访问数据库的身份 - 第 15 行
  • 与该身份关联的密码 - 第 18 行

这里我们采用了之前创建的源文件 MSDE 中的信息。

2.4.3.3. 测试

现在可以进行测试了。借助应用程序 [Nunit-Gui], 从上文的 [tests] 文件夹中加载 DLL 和 [test-webarticles-dao.dll],并执行 [testGetAllArticles] 测试:

Image

尽管测试类最初命名为 [NUnitTestArticlesDaoArrayList](该名称因我们使用了该类生成的 DLL 和 [tests-webarticles-dao.dll] 而保留), 但此处实际测试的确实是[ArticlesDaoSqlserver]类。屏幕截图显示,我们已正确检索到之前放入[ARTICLES]表中的条目。现在,让我们进行全部测试:

Image

在左侧窗口中,可以看到已测试的方法列表。每个方法名称前面的圆点颜色表示该方法的测试结果:成功(绿色)或失败(红色)。在屏幕上查看本文档的读者将看到所有测试均已通过。

2.4.4. 将新层 [dao] 集成到应用程序 [webarticles] 中

我们遵循第 2.3.4 节中所述的步骤。对文件夹 [runtime] 的内容进行以下修改:

  • 在 [bin] 文件夹中,将旧图层 [dao] 中的 DLL 被新图层 [dao] 中的 DLL 所替换,该新图层由类 [ArticlesDaoSqlServer] 实现
  • 在 [runtime] 中,配置文件 [web.config] 被替换为一个考虑了新实现类的文件:
<?xml version="1.0" encoding="iso-8859-1" ?>
<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 type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoSqlServer, webarticles-dao">
        <constructor-arg index="0">
            <value>portable1_tahe\msde140405</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>dbarticles</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>admarticles</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>mdparticles</value>
        </constructor-arg>
    </object>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

注释:

  • 第 15-33 行将新类 [ArticlesDaoSqlServer] 的一个实例与单例 [articlesDao] 关联起来。这是唯一的修改。我们在测试新层 [dao] 时已经遇到过这种情况

我们已准备好进行测试。我们将保持与之前相同的 [Cassini] Web 服务器配置。我们将 [MSDE] 文章表初始化为以下值:

Image

请确保 Cassini Web 服务器以及 SGBD MSDE(此处为实例 portable1_tahe\msde140405)已启动。 使用浏览器访问 URL [http://localhost/webarticles/main.aspx]:

Image

现在,让我们检查数据库 [MSDE] 中表 [ARTICLES] 的内容:

Image

商品 [ballon foot] 和 [raquette tennis] 已采购,其库存已按采购数量减少。商品 [rollers] 无法采购,因为需求数量超过了库存数量。 建议读者进行进一步测试。

2.4.5. 实现类 [ArticlesDaoOleDb]

2.4.5.1. 数据源 OleDb

[dao] 层的第三种实现假设数据位于可通过 OleDb 驱动程序访问的数据库中。OleDb 源的工作原理与 ODBC 源类似。 调用 OleDb 数据源的程序,是通过所有 OleDb 数据源共有的标准接口实现的。 更换 OleDb 源文件,实际上就是更换 OleDb 驱动程序。代码本身无需修改。

您可以通过 Visual Studio 查看计算机上可用的 OleDb 驱动程序:

  • 通过 [Affichage/Explorateur de serveurs] 调用服务器资源管理器:

Image

  • 要添加新连接,请右键单击 [Connexion de données] 并选择 [Ajouter une connexion] 选项。随后将弹出向导,用于定义连接的各项参数:

Image

  • [Fournisseur]面板列出了可用的OLEDB驱动程序。 我们将为新层 [dao] 使用驱动程序 [Microsoft Jet 4.0 OLE DB Provider],该驱动程序可访问数据库 ACCESS。
  • 暂时退出 Visual Studio,创建基础数据库 ACCESS( ),该数据库包含以下唯一表:

Image

  • 该表的结构如下:
id
数字 - 整数 - 主键
nom
文本 - 20个字符 -
prix
数字 - 双精度实数
stockactuel
数字 - 整数
stockminimum
数值 - 整数
  • 让我们回到 Visual Studio,并按照之前的说明创建一个新的连接:

Image

  • 我们选择驱动程序 [Microsoft Jet 4.0],然后转到面板 [Connexion]:

Image

  • 点击按钮 [1],指定刚刚创建的数据库 ACCESS,然后点击按钮 [Terminer] 完成连接的配置。创建的连接现在会显示在可用连接列表中:

Image

  • 双击表 [ARTICLES] 即可查看其内容:

Image

  • 此时即可对该表中的行进行添加、修改或删除操作。
  • 在服务器浏览器中选择新连接,即可访问其属性表:

Image

  • 了解连接字符串非常重要,它将用于连接数据库:
Provider=Microsoft.Jet.OLEDB.4.0;User ID=Admin;Data Source=D:\data\serge\databases\access\articles\articles.mdb;Mode=Share Deny None;Extended Properties="";Jet OLEDB:System database="";Jet OLEDB:Registry Path="";Jet OLEDB:Engine Type=5;Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:Create System Database=False;Jet OLEDB:Encrypt Database=False;Jet OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB:SFP=False
  • 在此连接字符串中,我们仅需关注以下部分:
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;

2.4.5.2. 类代码 [ArticlesDaoOleDb]

类 [ArticlesDaoOleDb] 与之前研究的类 [ArticlesDaoPlainODBC] 非常相似。因此,我们仅说明与上一版本相比的变更:

  • 所需类位于命名空间 [System.Data.OleDb] 中,而非 [System.Data.Odbc]
  • 类型为 [OdbcConnection] 的连接现改为类型 [OleDbConnection]
  • [OdbcCommand] 对象的类型现已更改为 [OleDbCommand]

该类的构造函数仅接受一个参数,即数据库连接字符串:

         ' 构造函数
        Public Sub New(ByVal connectString As String)
             ' connectString:源连接字符串 OleDb
             '实例化连接
            connexion = New OleDbConnection(connectString)
             ' 准备请求 SQL
...
        End Sub

[ArticlesDaoOleDb] 类的完整代码如下:

Imports System
Imports System.Collections
Imports System.Data.OleDb

Namespace istia.st.articles.dao

    Public Class ArticlesDaoOleDb
        Implements istia.st.articles.dao.IArticlesDao

         ' 私有字段
        Private connexion As OleDbConnection = Nothing
        Private insertCommand As OleDbCommand
        Private updatecommand As OleDbCommand
        Private deleteSomeCommand As OleDbCommand
        Private selectSomeCommand As OleDbCommand
        Private updateStockCommand As OleDbCommand
        Private deleteAllCommand As OleDbCommand
        Private selectAllCommand As OleDbCommand

         ' 构造函数
        Public Sub New(ByVal connectString As String)
             ' connectString:源连接字符串 OleDb
             '实例化连接
            connexion = New OleDbConnection(connectString)
             ' 准备请求 SQL
            insertCommand = New OleDbCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
            updatecommand = New OleDbCommand("update ARTICLES set nom=?, prix=?, stockactuel=?, stockminimum=? where id=?", connexion)
            deleteSomeCommand = New OleDbCommand("delete from ARTICLES where id=?", connexion)
            selectSomeCommand = New OleDbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=?", connexion)
            updateStockCommand = New OleDbCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)
            selectAllCommand = New OleDbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New OleDbCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' 独占段
            SyncLock Me
                 ' 准备插入请求
                With insertCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", unArticle.id))
                    .Add(New OleDbParameter("nom", unArticle.nom))
                    .Add(New OleDbParameter("prix", unArticle.prix))
                    .Add(New OleDbParameter("stockactuel", unArticle.stockactuel))
                    .Add(New OleDbParameter("stockminimum", unArticle.stockminimum))
                End With
                Try
                     '执行该查询
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' 独占段
            SyncLock Me
                 ' 正在准备库存更新查询
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("mvt1", mouvement))
                    .Add(New OleDbParameter("id", idArticle))
                    .Add(New OleDbParameter("mvt2", mouvement))
                End With
                 '正在执行
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     '请求错误
                    Throw New Exception(String.Format("Erreur lors du changement de stock [idArticle={0}, mouvement={1}] : [{2}]", idArticle, mouvement, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' 独占段
            SyncLock Me
                Try
                     '正在执行插入请求
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

        Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' 独占段
            SyncLock Me
                Try
                     '正在执行 SELECT 查询
                    Dim articles As IList = executeQuery(selectAllCommand)
                     '返回列表
                    Return articles
                Catch ex As Exception
                     '请求错误
                    Throw New Exception(String.Format("Erreur lors de l'obtention des articles [select id,nom,prix,stockactuel,stockminimum from articles]: {0}", ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' 独占段
            SyncLock Me
                 ' 正在准备 select 查询
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                 '正在执行
                Try
                     '正在执行查询
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     '检测是否找到文章
                    If articles.Count = 0 Then Return Nothing
                     '返回商品
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' 独占段
            SyncLock Me
                 ' 准备更新请求
                With updatecommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("nom", unArticle.nom))
                    .Add(New OleDbParameter("prix", unArticle.prix))
                    .Add(New OleDbParameter("stockactuel", unArticle.stockactuel))
                    .Add(New OleDbParameter("stockminimum", unArticle.stockactuel))
                    .Add(New OleDbParameter("id", unArticle.id))
                End With
                 ' 执行该查询
                Try
                     '正在执行插入查询
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' 独占段
            SyncLock Me
                 ' 准备删除查询
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                 '正在执行
                Try
                     '正在执行删除查询
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la suppression de l'article [id={0}] : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Private Function executeQuery(ByVal query As OleDbCommand) As IList
             ' 执行查询 SELECT 
             ' 声明对象以访问结果表中的所有行
            Dim myReader As OleDbDataReader = Nothing
            Try
                 '建立连接至 BDD
                connexion.Open()
                 '执行查询
                myReader = query.ExecuteReader()
                 '声明一个项目列表以便后续返回
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     '根据读取器的值准备一个商品
                    unArticle = New Article
                    unArticle.id = myReader.GetInt32(0)
                    unArticle.nom = myReader.GetString(1)
                    unArticle.prix = myReader.GetDouble(2)
                    unArticle.stockactuel = myReader.GetInt32(3)
                    unArticle.stockminimum = myReader.GetInt32(4)
                     '将商品添加到列表中
                    articles.Add(unArticle)
                End While
                 '返回结果
                Return articles
            Finally
                 '释放资源
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

        Private Function executeUpdate(ByVal sqlCommand As OleDbCommand) As Integer
             ' 执行更新请求
            Try
                 '建立与 BDD 的连接
                connexion.Open()
                 '正在执行查询
                Return sqlCommand.ExecuteNonQuery()
            Finally
                 ' 释放资源
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

请读者结合前文对类 [ArticlesDaoPlainODBC] 的说明来阅读此代码。

2.4.5.3. 生成 [dao] 层程序集

新的 Visual Studio 项目具有以下结构:

Image

该项目配置为生成名为 [webarticles-dao.dll] 的 DLL:

2.4.5.4. [dao] 层的 Nunit 测试

2.4.5.4.1. 测试类 NUnit

实现类 [ArticlesDaoOleDb] 的 Nunit 测试类与类 [ArticlesDaoPlainODBC] 的测试类相同(参见第 2.3.3.2 节)。我们采用类似的方法来准备该类的 Nunit 测试:

  • 在项目 [dao-oledb] 的 Visual Studio 文件夹中,通过复制项目 [dao-odbc] 中的 [tests] 文件夹(左侧),创建 [tests] 文件夹(右侧):
  • 在项目 [dao-oledb] 的文件夹 [tests] 中,我们将 DLL 和 [webarticles-dao.dll] 替换为 DLL 和 [webarticles-dao.dll](该文件源自项目 [dao-oledb] 的生成)
  • 我们修改配置文件 [spring-config.xml],以实例化新类 [ArticlesDaoOleDb]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
-->
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoOleDb, webarticles-dao">
        <constructor-arg index="0">
            <value>Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;</value>
        </constructor-arg>
    </object>
</objects>

注释

  • 第 7 行,对象 [articlesdao] 现已关联到类 [ ArticlesDaoOleDb] 的一个实例
  • 该类有一个单参数构造函数:连接 OleDb 数据库的连接字符串ACCESS - 第 9 行
2.4.5.4.2. Tests

我们已准备好进行测试。借助应用程序 [Nunit-Gui], 从上述文件夹中加载 DLL [test-webarticles-dao.dll] [tests],并执行测试 [testGetAllArticles]:

Image

尽管测试类最初命名为 [NUnitTestArticlesDaoArrayList],但此处实际测试的是 [ArticlesDaoOleDb] 类。 屏幕截图显示,我们已正确检索到之前放入表 [ARTICLES] 中的条目。现在,让我们执行全部测试:

Image

在屏幕上查看此文档的读者将看到所有测试均已通过(显示为绿色)。

2.4.5.5. 将新层 [dao] 集成到应用程序 [webarticles] 中

我们遵循第 2.3.4 节中所述的步骤。对文件夹 [runtime] 的内容进行以下修改:

  • 在文件夹 [bin] 中,旧层 [dao] 中的 DLL 被新层 [dao] 中的 DLL 所替换,该新层由类 [ArticlesDaoOleDb] 实现
  • 在 [runtime] 中,配置文件 [web.config] 被替换为一个考虑了新实现类的文件:
<?xml version="1.0" encoding="iso-8859-1" ?>
<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 type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
            <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoOleDb, webarticles-dao">
                <constructor-arg index="0">
                    <value>Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;</value>
                </constructor-arg>
            </object>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

注释

  • 第 14-18 行将新类 [ArticlesDaoOleDb] 的一个实例与单例 [articlesDao] 关联起来。这是唯一的修改。

我们保留与之前相同的 [Cassini] Web 服务器配置。我们将文章表初始化为以下值:

Image

请确保文章数据库未被 Visual Studio 或 ACCESS 等程序占用。通过浏览器访问 URL [http://localhost/webarticles/main.aspx]:

Image

现在,让我们使用 ACCESS 检查 [ARTICLES] 表的内容:

Image

商品 [pantalon] 和 [jupe] 已采购,其库存已按采购数量减少。商品 [manteau] 无法采购,因为需求数量超过了库存数量。 建议读者进行进一步测试。

2.5. 实现类 [ArticlesDaoFirebirdProvider]

2.5.1. Firebird-net-provider 访问提供程序

我们之前已使用过数据源 [Firebird],并通过驱动程序 ODBC 调用它。 尽管 ODBC 驱动程序为使用它们的代码带来了很高的可重用性,但其性能却不如专门为目标 SGBD 编写的驱动程序。 SGBD [Firebird] 可通过特定的类库使用,该类库可从 Firebird [http://firebird.sourceforge.net/] 网站下载。下载页面提供以下链接(2005年4月):

Image

[firebird-net-provider] 链接用于下载访问 Firebird 的 .NET 类。安装该软件包后,将生成类似于以下结构的文件夹:

Image

其中有两个元素值得关注:

  • [FirebirdSql.Data.Firebird.dll]:包含用于访问 SGBD Firebird 的 .NET 类的程序集
  • [FirebirdNETProviderSDK.chm]:这些类的文档

接下来,为了让 Visual Studio 项目能够使用这些类,我们将进行两项操作:

  • 将程序集 [FirebirdSql.Data.Firebird.dll] 放入项目的 [bin] 文件夹中
  • 将该程序集添加到项目的引用中

2.5.2. [ArticlesDaoFirebirdProvider]类的代码

[ArticlesDaoFirebirdProvider] 类与之前研究的 [ArticlesDaoSqlServer] 类非常相似。因此,我们仅说明与该版本相比所做的更改:

  • 所需类位于命名空间 [FirebirdSql.Data.Firebird] 中,而非 [System.Data.SqlClient]
  • 类型为 [SqlConnection] 的连接现改为类型 [FbConnection]
  • [SqlCommand] 对象现为 [FbCommand] 类型
  • [SqlParameter] 对象现为类型 [FbParameter]

该类的构造函数接受四个参数,并利用这些参数构建数据库连接字符串:

         ' 构造函数
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' 服务器:SGBD的主机名
             ' databaseName:数据库路径
             ' uid:登录用户的身份
             ' password:其密码
...
        End Sub

[ArticlesDaoFirebirdProvider] 类的完整代码如下:

Imports System
Imports System.Collections
Imports FirebirdSql.Data.Firebird

Namespace istia.st.articles.dao

    Public Class ArticlesDaoFirebirdProvider
        Implements istia.st.articles.dao.IArticlesDao

         ' 私有字段
        Private connexion As FbConnection = Nothing
        Private databasePath As String
        Private insertCommand As FbCommand
        Private updatecommand As FbCommand
        Private deleteSomeCommand As FbCommand
        Private selectSomeCommand As FbCommand
        Private updateStockCommand As FbCommand
        Private deleteAllCommand As FbCommand
        Private selectAllCommand As FbCommand

         ' 创建者
        Public Sub New(ByVal serveur As String, ByVal databasePath As String, ByVal uid As String, ByVal password As String)
             ' 服务器:SGBD Firebird 数据库的主机名
             ' databaseName:待处理数据库的路径
             ' uid:连接数据库的用户身份
             ' password:其密码

             '获取作为参数传递的数据库名称
            Me.databasePath = databasePath
             '建立连接
            Dim connectString As String = String.Format("DataSource={0};Database={1};User={2};Password={3}", serveur, databasePath, uid, password)
            connexion = New FbConnection(connectString)
             ' 准备查询SQL
            insertCommand = New FbCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
            updatecommand = New FbCommand("update ARTICLES set nom=@nom, prix=@prix, stockactuel=@sa, stockminimum=@sm where id=@id", connexion)
            deleteSomeCommand = New FbCommand("delete from ARTICLES where id=@id", connexion)
            selectSomeCommand = New FbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES where id=@id", connexion)
            updateStockCommand = New FbCommand("update ARTICLES set stockactuel=stockactuel+@mvt where id=@id and (stockactuel+@mvt)>=0", connexion)
            selectAllCommand = New FbCommand("select id, nom, prix, stockactuel, stockminimum from ARTICLES", connexion)
            deleteAllCommand = New FbCommand("delete from ARTICLES", connexion)
        End Sub

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' 专用部分
            SyncLock Me
                 ' 准备插入查询
                With insertCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", unArticle.id))
                    .Add(New FbParameter("@nom", unArticle.nom))
                    .Add(New FbParameter("@prix", unArticle.prix))
                    .Add(New FbParameter("@sa", unArticle.stockactuel))
                    .Add(New FbParameter("@sm", unArticle.stockminimum))
                End With
                Try
                     '执行该查询
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' 独占段
            SyncLock Me
                 ' 正在准备库存更新查询
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@mvt", mouvement))
                    .Add(New FbParameter("@id", idArticle))
                End With
                 '正在执行
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     '请求错误
                    Throw New Exception(String.Format("Erreur lors du changement de stock [idArticle={0}, mouvement={1}] : [{2}]", idArticle, mouvement, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' 独占段
            SyncLock Me
                Try
                     '正在执行插入请求
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

        Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' 独占段
            SyncLock Me
                Try
                     '正在执行SELECT查询
                    Dim articles As IList = executeQuery(selectAllCommand)
                     '返回列表
                    Return articles
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de l'obtention des articles [select id,nom,prix,stockactuel,stockminimum from articles]: {0}", ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' 独占段
            SyncLock Me
                 ' 正在准备 select 查询
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                 '正在执行
                Try
                     '正在执行查询
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     '检测是否找到文章
                    If articles.Count = 0 Then Return Nothing
                     '返回商品
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' 独占段
            SyncLock Me
                 ' 准备更新请求
                With updatecommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@nom", unArticle.nom))
                    .Add(New FbParameter("@prix", unArticle.prix))
                    .Add(New FbParameter("@sa", unArticle.stockactuel))
                    .Add(New FbParameter("@sm", unArticle.stockminimum))
                    .Add(New FbParameter("@id", unArticle.id))
                End With
                 ' 执行该查询
                Try
                     '正在执行插入查询
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' 独占段
            SyncLock Me
                 ' 准备删除查询
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                 '正在执行
                Try
                     '正在执行删除查询
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     '查询错误
                    Throw New Exception(String.Format("Erreur lors de la suppression de l'article [id={0}] : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

        Private Function executeQuery(ByVal query As FbCommand) As IList
             ' 执行查询 SELECT 
             ' 声明对象以访问结果表中的所有行
            Dim myReader As FbDataReader = Nothing
            Try
                 '建立连接至 BDD
                connexion.Open()
                 '执行查询
                myReader = query.ExecuteReader()
                 '声明商品列表以便后续返回
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     '正在根据阅读器的数值准备一篇文章
                    unArticle = New Article
                    unArticle.id = myReader.GetInt32(0)
                    unArticle.nom = myReader.GetString(1)
                    unArticle.prix = myReader.GetDouble(2)
                    unArticle.stockactuel = myReader.GetInt32(3)
                    unArticle.stockminimum = myReader.GetInt32(4)
                     '将文章添加到列表中
                    articles.Add(unArticle)
                End While
                 '返回结果
                Return articles
            Finally
                 ' 释放资源
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

        Private Function executeUpdate(ByVal updateCommand As FbCommand) As Integer
             ' 执行更新请求
            Try
                 '建立与 BDD 的连接
                connexion.Open()
                 '执行查询
                Return updateCommand.ExecuteNonQuery()
            Finally
                 ' 释放资源
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

请读者结合前文对类 [ArticlesDaoSqlServer] 的说明来阅读此代码。

2.5.3. 生成 [dao] 层程序集

新的 Visual Studio 项目具有以下结构:

Image

请注意项目引用中包含 [FirebirdSql.Data.Firebird.dll] 程序集。该 DLL 程序集已放置在项目的 [bin] 文件夹中。 该项目配置为生成一个名为 [webarticles-dao.dll] 的 DLL:

2.5.4. [dao] 层的 Nunit 测试

2.5.4.1. 测试类 NUnit

实现类 [ArticlesDaoFirebirdProvider] 的 Nunit 测试类与类 [ArticlesDaoPlainODBC] 的测试类相同(参见第 2.3.3.2 节)。 我们采用类似的方法来准备类 [ArticlesDaoFirebirdProvider] 的 Nunit 测试:

  • 在 [dao-firebird-provider] 项目的 Visual Studio 文件夹中,通过复制 [dao-odbc] 测试层项目(左侧)中的 [bin] 文件夹,创建 [tests] 文件夹(右侧):
  • 在文件夹 [tests] 中,我们将 DLL [webarticles-dao.dll] 替换为来自 [dao-firebird-provider] 项目生成的 DLL [webarticles-dao.dll]2HTMLP001136ZQX
  • 我们修改配置文件 [spring-config.xml],以实例化新类 [ArticlesDaoFirebirdProvider]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
-->
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoFirebirdProvider, webarticles-dao">
        <constructor-arg index="0">
            <value>localhost</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>D:\data\serge\databases\firebird\dbarticles2.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>sysdba</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>masterkey</value>
        </constructor-arg>
    </object>
</objects>

注释

  • 第 7 行,对象 [articlesdao] 现已关联到类 [ArticlesDaoFirebirdProvider] 的一个实例
  • 该类有一个带四个参数的构造函数
  • SGBD 的主机 - 第 9 行
  • Firebird 数据库的访问路径 - 第 12 行
  • 登录用户的用户名 - 第 15 行
  • 其密码 - 第 18 行

2.5.4.2. 测试

数据源中的 [ARTICLES] 表已填充以下条目(请使用 IBExpert):

Image

现在可以进行测试了。借助应用程序 [Nunit-Gui], 我们从上述文件夹 [tests] 中加载 DLL 和 [test-webarticles-dao.dll],并执行测试 [testGetAllArticles]:

Image

尽管测试类最初命名为 [NUnitTestArticlesDaoArrayList],但此处实际测试的是 [ArticlesDaoFirebirdProvider] 类。 屏幕截图显示,我们已正确检索到之前放入表 [ARTICLES] 中的条目。现在,让我们执行全部测试:

Image

在屏幕上查看此文档的读者会看到所有测试均已通过(显示为绿色)。但读者无法看到的是,这些测试的执行速度明显快于我们首次实现中通过驱动程序 ODBC 访问文章数据库时的速度。

2.5.5. 将新层[dao]集成到应用程序[webarticles]中

我们遵循此前已两次说明的方法,特别是第2.3.4节中的说明。我们对文件夹[runtime]的内容进行了以下修改:

  • 在文件夹 [bin] 中,将旧层 [dao] 中的 DLL 被新层 [dao] 中的 DLL 所替换,该新层由类 [ArticlesDaoFirebirdProvider] 实现。 我们还在此放置了 Firebird 所需的 DLL:

Image

  • 在 [runtime] 中,配置文件 [web.config] 被替换为一个考虑了新实现类的文件:
<?xml version="1.0" encoding="iso-8859-1" ?>
<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 type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
            <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoFirebirdProvider, webarticles-dao">
                <constructor-arg index="0">
                    <value>localhost</value>
                </constructor-arg>
                <constructor-arg index="1">
                    <value>D:\data\serge\databases\firebird\dbarticles2.gdb</value>
                </constructor-arg>
                <constructor-arg index="2">
                    <value>sysdba</value>
                </constructor-arg>
                <constructor-arg index="3">
                    <value>masterkey</value>
                </constructor-arg>
            </object>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

注释

  • 第 14-27 行将新类 [ArticlesDaoFirebirdProvider] 的一个实例与单例 [articlesDao] 关联起来。这是唯一的修改。

现在可以进行测试了。我们将 Web 服务器 [Cassini] 配置为与之前的测试相同。我们使用以下值初始化文章表:

Image

使用浏览器访问 URL [http://localhost/webarticles/main.aspx]:

Image

现在检查 [ARTICLES] 表的内容:

Image

商品 [crayon bille] 和 [ramette 50 feuilles] 已成功采购,其库存已按采购数量相应减少。商品 [stylo plume] 未能采购,因为需求数量超过了库存数量。 建议读者进行进一步测试。

2.5.6. 实现类 [ArticlesDaoSqlMap]

2.5.6.1. Ibatis 产品 SqlMap

我们为应用程序 [webarticles] 的 [dao] 层编写了四种不同的实现。 每次我们都能将新的 [dao] 层集成到 [webarticles] 应用程序中,而无需重新编译另外两个层 [web] 和 [domain]。 需要重申的是,这一成果得益于两项架构设计:

  • 通过接口访问各层
  • 通过 Spring 集成各层

我们希望更进一步。尽管各不相同,但我们对 [dao] 层的四种实现却有着惊人的相似之处。一旦写好了第一个实现,其余三个几乎就是通过复制粘贴并替换某些关键字而得出的。 而业务逻辑则未作任何修改。这不禁让人思考:是否有可能设计一种实现方案,让我们摆脱对不同数据访问模式的依赖?我们目前使用了四种方式:

  • 通过驱动程序 ODBC 访问数据源 ODBC
  • 直接访问 SQL Server 数据库
  • 通过 Ole Db 驱动程序访问 Ole Db 数据源
  • 直接访问 Firebird 数据库

Ibatis SqlMap [[http://www.ibatis.com/] 工具使得能够开发与数据源实际类型无关的数据访问层。数据访问通过以下方式实现:

  • 配置文件,其中包含定义数据源及所需操作的信息
  • 基于这些信息访问数据的类库

Ibatis SqlMap 工具最初是为 Java 平台开发的。其移植到 .NET 平台的时间较短,且似乎存在部分缺陷(此为个人观点,尚需深入验证)。 尽管如此,鉴于该工具在Java平台上已得到充分验证,介绍其.NET版本仍具有参考价值。

2.5.6.2. 在哪里可以找到 、IBATIS和SqlMap?

Firebird的主网站是[http://www.ibatis.com/]。下载页面提供了以下链接:

Image

我们选择链接 [Stable Binaries],它会将我们引导至 [SourceForge.net]。按照下载流程完成整个过程。最终会获得一个包含以下文件的压缩包:

Image

在某个使用 Ibatis SqlMap 的 Visual Studio 项目中,需要执行以下两项操作:

  • 将上述文件放入项目的 [bin] 文件夹中
  • 在项目中为上述每个文件添加引用

2.5.6.3. Ibatis SqlMap 的配置文件

将通过以下配置文件定义一个 [SqlMap] 数据源:

  1. providers.config:定义用于访问数据的类库
  2. sqlmap.config:定义要建立的连接的特性
  3. 映射文件:定义对数据执行的操作

这些文件的逻辑如下:

  • 要访问数据,我们需要建立连接。为了表示该连接,我们已经遇到过几个类:OdbcConnection、SqlConnection、OleDbConnection、FbConnection。 此外,我们还需要一个 [Command] 对象来发送 SQL 请求: OdbcCommand、SqlCommand、OleDbCommand、FbCommand。 等等。在文件 [providers.config] 中,我们定义了所需的所有类。
  • 文件 [sqlmap.config] 主要定义了连接包含数据的数据库的连接字符串。 通过实例化在 [providers.config] 中定义的 [Connection] 类来建立数据库连接,并将 [sqlmap.config] 中定义的连接字符串传递给该类的构造函数。
  • 映射文件定义:
    • 数据表行与类之间的关联.NET,其实例将包含这些行
    • 需执行的 SQL 操作。这些操作通过名称进行标识。 代码 .NET 通过名称执行这些操作,从而消除了代码 .NET 中所有 SQL 代码。

2.5.6.4. [dao-sqlmap] 项目的配置文件

让我们通过一个示例,来探讨 SqlMap 配置文件的具体性质。我们将假设数据源是第 2.3.3.1 节中提到的 ODBC Firebird 数据源。

2.5.6.4.1. providers.config

针对数据源 ODBC 的配置文件 [providers.config] 如下所示:

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

<providers>
    <clear/>
    <provider 
        name="Odbc1.1" 
        enabled="true" 
        assemblyName="System.Data, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
        connectionClass="System.Data.Odbc.OdbcConnection" 
        commandClass="System.Data.Odbc.OdbcCommand" 
        parameterClass="System.Data.Odbc.OdbcParameter" 
        parameterDbTypeClass="System.Data.Odbc.OdbcType" 
        parameterDbTypeProperty="OdbcType" 
        dataAdapterClass="System.Data.Odbc.OdbcDataAdapter" 
        commandBuilderClass="System.Data.Odbc.OdbcCommandBuilder" 
        usePositionalParameters = "true"
        useParameterPrefixInSql = "false"
        useParameterPrefixInParameter = "false"
        parameterPrefix = "@"
    />
</providers>

注释

  • [providers.config] 文件随 [SqlMap] 软件包一起发布。它提供了多个标准访问提供商(provider)。上面的代码直接来自该文件。
  • 一个 <provider> 有一个名称(第 6 行),名称可以是任意内容
  • 一个 <provider> 可以处于启用状态(如 [enabled=true]),也可以处于禁用状态(如 [enabled=false])。如果处于启用状态,则第 8 行引用的 DLL 必须可访问。 一个 [providers.config] 文件可以包含多个 <provider> 标签。
  • 第 8 行 - 包含第 9-15 行所定义类的程序集名称
  • 第 9 行 - 用于建立连接的类
  • 第 10 行 - 用于创建 [Command] 对象以发送 SQL 命令的类
  • 第 11 行 - 用于管理带参数的 SQL 命令参数的类
  • 第 12 行 - 用于枚举表字段可能数据类型的类
  • 第 13 行 - 包含该参数值类型的 [Parameter] 对象的属性名称
  • 第 14 行 - 用于根据数据源创建 [DataSet] 对象的 [Adapter] 类名称
  • 第 15 行 - 类 [CommandBuilder] 的名称,该类与对象 [Adapter] 关联,可基于其属性 [SelectCommand] 自动生成该对象的属性 [InsertCommand, DeleteCommand, UpdateCommand]
  • 第 16 至 19 行 - 定义了如何管理已配置的 SQL 命令。根据具体情况,需编写如下内容:
insert into ARTICLES(id,nom,prix,stockactuel,stockminimum) values (?,?,?,?,?)

或者

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

第一种情况涉及形式位置参数。这些参数的实际值必须按形式参数的顺序提供。第二种情况涉及命名参数。为该类参数赋值时需明确其名称,此时顺序已不重要。

  • 第16行 - 说明源文件ODBC使用位置参数
  • 第17-19行 - 涉及命名参数。此处没有命名参数。

这些信息使 SqlMap 能够知道,例如,它需要实例化哪个类来建立连接。在此处,该类为 [OdbcConnection](第 9 行)。

2.5.6.4.2. sqlmap.config

文件 [providers.config] 定义了用于访问源 ODBC 的类。它未指定任何源 ODBC。这是由文件 [sqlmap.config] 完成的:

<?xml version="1.0" encoding="utf-8" ?>
<sqlMapConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="Schemas\SqlMapConfig.xsd">
    <properties resource="properties.xml"/>
    <settings>
        <setting useStatementNamespaces="false" />
        <setting cacheModelsEnabled="false" />
    </settings>
     <!-- ==== 数据源 ========= -->    
    <database>
        <provider name="${provider}"/>
        <dataSource name="sqlmaparticles"  connectionString="${connectionString}"/>
        <transactionManager type="ADO/SWC" />
    </database>
    <sqlMaps>
        <sqlMap resource="articles.xml" />
    </sqlMaps>
</sqlMapConfig>

注释

  • 第 3 行 - 定义了一个属性文件 [properties.xml]。该文件定义了 (键, 值) 对。 键可以是任意名称。键 C 对应的值可通过在 [sqlmap.config] 中使用 ${C} 语法获取。以下是 [properties.xml] 文件,它将与前面的 [sqlmap.config] 文件关联:
1
2
3
4
5
<?xml version="1.0" encoding="utf-8" ?>
<settings>
    <add key="provider" value="Odbc1.1" />
    <add key="connectionString" value="DSN=odbc-firebird-articles;UID=SYSDBA;PASSWORD=masterkey" />
</settings>

第 3 行 - 定义了键 [provider]。其值为在 [providers.config] 中使用的 <provider> 标签名称

第 4 行 - 定义了键 [connectionString]。其值为用于连接 ODBC Firebird 数据源的连接字符串。

  • 第 4-7 行 - 配置参数:
    • 第 5 行 - SQL 的查询将通过名称进行标识,该名称本身可以属于某个命名空间。[useStatementNamespaces="false"] 表示不使用命名空间。
    • 第 6 行 - SqlMap 具有多种缓存策略,以尽量减少对数据源的访问。[cacheModelsEnabled="false"] 表示不使用任何缓存策略。
  • 第 9-13 行 - 定义数据源的特性:
    • 第 10 行 - 指定要使用的 [providers.config] 的 <provider> 名称
    • 第 11 行 - 数据源连接字符串
    • 第 12 行 - 事务管理器。此处我们未使用该功能,但因该行在标准分发文件中存在,故保留。
  • 第 14-16 行 - 列出了定义要在数据源上执行的 SQL 操作的文件列表。
  • 第 15 行 - 定义映射文件 [articles.xml]
2.5.6.4.3. articles.xml

该文件具有两项功能:

  • 定义数据源表的对象映射。在最简单的情况下,这相当于将一个类与表中的一行相关联。
  • 定义带参数的 SQL 操作并为其命名。

我们将使用以下文件 [articles.xml]:

<?xml version="1.0" encoding="iso-8859-1" ?>
<sqlMap namespace="Articles" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="SqlMap.xsd">
     <!-- resultMap -->
    <resultMaps>
        <resultMap id="article" class="istia.st.articles.dao.Article">
            <result property="id" column="ID" />
            <result property="nom" column="NOM" />
            <result property="prix" column="PRIX" />
            <result property="stockactuel" column="STOCKACTUEL" />
            <result property="stockminimum" column="STOCKMINIMUM" />
        </resultMap>
    </resultMaps>
     <!-- 查询 SQL -->
    <statements>
         <!-- 获取所有文章 -->
        <select id="getAllArticles" resultMap="article">
                select ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM FROM ARTICLES
            </select> 
         <!-- 删除所有商品-->
        <delete id="clearAllArticles">
                delete from ARTICLES
            </delete> 
         <!-- 插入商品 -->
        <insert id="insertArticle" parameterClass="istia.st.articles.dao.Article">
                insert into ARTICLES (id, nom, prix,stockactuel, stockminimum) values
                ( #(id#, #名称#, #价格#, #当前库存#, #最低库存#)
            </insert>
         <!-- 删除一个商品 -->
        <delete id="deleteArticle" parameterClass="int">
                delete FROM ARTICLES where ID= #值#
            </delete>
         <!-- 修改商品 -->
        <update id="modifyArticle" parameterClass="istia.st.articles.dao.Article">
                update ARTICLES set NOM= #名称# ,PRIX= #价格# ,STOCKACTUEL= #当前库存# ,STOCKMINIMUM= #最低库存#  ID= #id#
            </update>
         <!-- 搜索特定商品 -->
        <select id="getArticleById" resultMap="article" parameterClass="int">
                select ID, NOM, PRIX,    STOCKACTUEL, STOCKMINIMUM FROM ARTICLES where ID= #价格#
            </select>
         <!-- 更改商品库存 -->
        <update id="changerStockArticle" parameterClass="Hashtable">
                update ARTICLES set STOCKACTUEL=(STOCKACTUEL + #移动#) 其中 ID=#id#  ((STOCKACTUEL + #移动#) >=0)
      </update>
    </statements>
</sqlMap>

注释

  • 第 4-11 行——定义了数据源表 [ARTICLES] 中的一行与类 [istia.st.articles.dao.Article] 之间的映射关系。 表中的每一列(column)都与类 [Article] 的一个属性(property)相关联。 此映射使 [SqlMap] 能够构建 SQL 与 SELECT 操作的结果。 SELECT 的每行结果都将根据映射规则放置在 [Article] 对象中。
  • 第 5 行 - 映射由 <resultMap> 标签包围,并通过 [id="article"] 属性命名。关联的类由 [class="istia.st.articles.dao.Article"] 属性指定。
  • 第14-44行 - 定义所需的操作 SQL
  • 第 16-18 行 - 定义了一个名为 SELECT 的操作,其名称为 [getAllArticles]
    • 第 16 行 - 操作 SELECT 被命名为 [name= "getAllArticles "],且要使用的映射由属性 [resultMap="article"] 定义。因此,此处引用的是第 5-11 行的映射
    • 第 17 行 - 待执行的 SQL 命令的文本
  • 第20-22行 - 定义SQL-Delete命令[clearAllArticles],用于清空商品表。
  • 第24-27行 - 定义SQL-Insert命令[insertArticle],用于向商品表中添加新商品。这是一个由参数(#id#、#nom#、#prix#、#stockactuel#、#stockminimum#)驱动的查询。 这五个参数的值将来自作为参数传递的 [Article] 对象:[parameterClass="istia.st.articles.dao.Article"]。 该参数对象必须包含由参数化命令 SQL 引用的属性(id、nom、prix、stockactuel、stockminimum)。
  • 第29-31行 - 定义命令 SQL Delete [deleteArticle],用于删除已知编号为 #value# 的商品。该编号将作为参数传递:[parameterClass="int"]。这是一条通用规则。 当参数唯一时,在命令 SQL 的文本中通过关键字 #value# 引用该参数。
  • 第33-35行 - 定义SQL-Update命令 [modifyArticle],用于修改已知编号的商品。与命令 [insertArticle] 类似,所需的五项信息将来自对象 [istia.st.articles.dao.Article] 的属性。
  • 第37-39行 - 定义SQL-Select命令[getArticleById],用于根据已知的商品编号获取该商品的记录。
  • 第41-43行 - 定义SQL-Update命令[changerStockArticle],用于修改已知编号的商品的字段[stockactuel]。 所需的两项信息——商品编号 #id# 和库存增量 #mouvement#——将从字典 [parameterClass="Hashtable"] 中获取。该字典必须包含两个键:id mouvement。SQL 命令将使用与这两个键关联的值。
2.5.6.4.4. 配置文件的位置

我们将探讨两种不同情况:

  • 如果是 Nunit 测试,[SqlMap] 的配置文件将放置在被测二进制文件所在的同一文件夹中。
  • 对于 Web 应用程序,配置文件将放置在应用程序的根目录下。

2.5.6.5. API 属于 SqlMap

SqlMap 的类包含在 DLL 中,该文件通常放置在应用程序的 [bin] 文件夹中:

Image

使用 SqlMap 类库的应用程序必须导入 [IBatisNet.DataMapper] 命名空间:

Imports IBatisNet.DataMapper

所有 SQL 操作均通过 [Mapper] 类型的单例实现,该类属于 [IBatisNet.DataMapper ] 命名空间。单例的获取方式如下:

        Dim mappeur As SqlMapper = Mapper.Instance

要执行命令 SqlMap [getAllArticles],应编写如下代码:

                    dim articles as IList=mappeur.QueryForList("getAllArticles", Nothing)
  • 方法 [QueryForList] 可将命令 SELECT 的结果以列表形式输出
  • 第一个参数是要执行的命令 SQL 的名称(参见 articles.xml)
  • 第二个参数是要传递给 SQL 请求的参数。必须与 SqlMap 命令的 [parameterClass] 属性相对应。 在 [articles.xml] 中,我们有 [parameterClass=Nothing]。因此,此处传递一个空指针。
  • 结果类型为 IList。该列表中的对象由 SQL-select 命令的 [resultMap] 属性指定:[resultMap="article"]。“article”是一个映射名称:
<resultMap id="article" class="istia.st.articles.dao.Article">

与该映射关联的类是 [istia.st.articles.dao.Article]。最终,前面定义的变量 [articles] 是一个由 [ istia.st.articles.dao.Article] 对象组成的列表。 因此,我们仅用一条语句就获取了整个 [ARTICLES] 表。如果 [ARTICLES] 表为空,则会得到一个包含 0 个元素的 [IList] 对象。

要执行命令 SqlMap [getArticleById],应编写如下代码:

dim unArticle as Article=CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
  • 方法 [QueryForObject] 可获取仅返回一行结果的命令 SELECT 的结果
  • 第一个参数是要执行的命令名称 SqlMap
  • 第二个参数是要传递给 SQL 请求的参数。必须与命令 SqlMap 的 [parameterClass] 属性相对应。 在 [articles.xml] 中,我们有 [parameterClass="int"]。因此,这里传递一个整数,代表要查找的商品编号。
  • 结果类型为 Object。如果 SELECT 未返回任何行,则结果为空指针(nothing)。

要执行命令 SqlMap [insertArticle],应编写如下代码:

                    mappeur.Insert("insertArticle", unArticle)
  • 方法 [Insert] 用于执行命令 SQL INSERT
  • 第一个参数是要执行的命令名称 SqlMap
  • 第二个参数是要传递给该命令的参数。必须与命令 SqlMap 的 [parameterClass] 属性相对应。 在 [articles.xml] 中,我们有 [parameterClass="istia.st.articles.dao.Article"]。因此,这里传递的是类型为 [istia.st.articles.dao.Article] 的对象。

要执行命令 SqlMap [deleteArticle],应编写如下代码:

                    dim nbArticles as Integer=mappeur.Delete("deleteArticle", idArticle)
  • 方法 [Delete] 允许执行命令 SQL DELETE
  • 第一个参数是要执行的命令名称 SQL
  • 第二个参数是要传递给该命令的参数。必须与命令 SqlMap 的 [parameterClass] 属性相对应。 在 [articles.xml] 中,我们有 [parameterClass="int"]。因此,这里传递的是要删除的商品编号。
  • 方法 [Delete] 的结果是已删除的行数

同理,要执行命令 SqlMap [clearAllArticles],应输入:

                    dim nbArticles as Integer=mappeur.Delete("clearAllArticles", nothing)

要执行命令 SqlMap [modifyArticle],应输入:

                    dim nbArticles as Integer=mappeur.Update("modifyArticle", unArticle)
  • 方法 [Update] 用于执行命令 SQL UPDATE
  • 第一个参数是要执行的命令名称 SqlMap
  • 第二个参数是要传递给该命令的参数。必须与命令 SqlMap 的 [parameterClass] 属性相对应。 在 [articles.xml] 中,我们有 [parameterClass="istia.st.articles.dao.Article"]。因此,这里传递的是类型为 [istia.st.articles.dao.Article] 的对象。
  • 方法 [Update] 的返回结果是修改的行数。

同理,要执行命令 SqlMap [changerStockArticle],应编写如下代码:

                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' 更新
                    dim nbLignes as Integer= mappeur.Update("changerStockArticle", paramètres)
  • 第二个参数对应于命令 SqlMap 的属性 [parameterClass]。在 [articles.xml] 中,有 [parameterClass="Hashtable"]。 命令 SQL 配置为 [changerStockArticle],并使用参数 [id, mouvement]。因此,这里传递了一个包含这两个键的字典。

2.5.6.6. 类 [ArticlesDaoSqlMap] 的代码

根据上述说明,现在我们可以编写如下新的实现类 [ArticlesDaoSqlMap]:

Option Explicit On 
Option Strict On

Imports System
Imports IBatisNet.DataMapper
Imports System.Collections

Namespace istia.st.articles.dao

    Public Class ArticlesDaoSqlMap
        Implements IArticlesDao

         ' 私有字段
        Dim mappeur As SqlMapper = Mapper.Instance

         ' 所有文章列表
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
            SyncLock Me
                Try
                    Return mappeur.QueryForList("getAllArticles", Nothing)
                Catch ex As Exception
                    Throw New Exception("Echec de l'obtention de tous les articles : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' 添加文章
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            SyncLock Me
                Try
                     ' unArticle:待添加文章
                     ' 插入
                    mappeur.Insert("insertArticle", unArticle)
                    Return 1
                Catch ex As Exception
                    Throw New Exception("Echec de l'ajout de l'article [" + unArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' 删除商品
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                     ' id:待删除商品的ID
                     ' 删除
                    Return mappeur.Delete("deleteArticle", idArticle)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de la suppression de l'article d'id [" + idArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' 修改商品
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                     ' 更新
                    Return mappeur.Update("modifyArticle", unArticle)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de la mise à jour de l'article [" + unArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' 搜索文章
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            SyncLock Me
                Try
                     ' id:要搜索的文章ID
                    Return CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de la recherche de l'article d'id [" + idArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' 删除所有商品
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
            SyncLock Me
                Try
                    mappeur.Delete("clearAllArticles", Nothing)
                Catch ex As Exception
                    Throw New Exception("Erreur lors de l'effacement de la table des articles : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Sub

         ' 修改商品库存
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                     ' id:需调整库存的商品ID
                     ' 业务类型:库存移动
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' 更新
                    Return mappeur.Update("changerStockArticle", paramètres)
                Catch ex As Exception
                    Throw New Exception(String.Format("Erreur lors du changement de stock [{0},{1}] : {2}", idArticle, mouvement, ex.ToString))
                End Try
            End SyncLock
        End Function
    End Class
End Namespace

建议读者结合API和SqlMap的说明来阅读此代码。值得注意的是,使用[SqlMap]大大减少了需要编写的代码量。

2.5.6.7. 生成 [dao] 层的程序集

新的 Visual Studio 项目具有以下结构:

Image

请注意,项目引用中包含了 SqlMap 所需的“程序集”。这些 DLL 已被放置在项目的 [bin] 文件夹中。 该项目已配置为生成一个名为 [webarticles-dao.dll] 的 DLL:

2.5.6.8. [dao] 层的 Nunit 测试

2.5.6.8.1. 测试类 NUnit

实现类 [ArticlesDaoSqlMap] 的 Nunit 测试类与类 [ArticlesDaoPlainODBC] 的测试类相同(参见第 2.3.3.2 节)。 我们采用类似的方法来准备类 [ArticlesDaoSqlMap] 的 Nunit 测试:

  • 在 [dao-sqlmap] 项目的 Visual Studio 文件夹中,通过复制 [dao-odbc] 项目中的 [tests] 文件夹(左侧),创建 [test1] 文件夹(右侧):
  • 在文件夹 [tests] 中,我们将 DLL 和 [webarticles-dao.dll] 替换为来自项目 [dao-sqlmap] 生成的 DLL 和 [webarticles-dao.dll]2HTMLP001419ZQX项目生成中。
  • 我们为 SqlMap 添加了必要的 DLL,以及已研究的配置文件 [providers.config, sqlmap.config, properties.xml, articles.xml]。
  • 我们修改配置文件 [spring-config.xml],以实例化新类 [ArticlesDaoSqlMap]:
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
-->
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoSqlMap, webarticles-dao"/>
</objects>

注释

  • 第 7 行,对象 [articlesdao] 现已与类 [ArticlesDaoSqlMap] 的实例相关联
  • 该类没有构造函数。将使用默认构造函数。
2.5.6.8.2. Tests

Firebird 数据源中的 [ARTICLES] 表已填充以下条目:

Image

我们已准备好进行测试。借助应用程序 [Nunit-Gui], 我们加载上述文件夹中的 DLL 和 [test-webarticles-dao.dll],并执行 [testGetAllArticles] 测试:

Image

尽管测试类最初命名为 [NUnitTestArticlesDaoArrayList],但此处实际测试的是 [ArticlesDaoSqlMap] 类。 屏幕截图显示,我们已正确检索到之前放入表 [ARTICLES] 中的条目。现在,让我们执行全部测试:

Image

在屏幕上查看此文档的读者会发现,部分测试已通过(绿色),但也有部分测试失败(红色)。 失败的测试包括 [testArticleAbsent] 和 [testChangerStockArticle]。经过长时间的排查,这些失败的原因似乎如下:

  • 在 [testArticleAbsent] 中,要求修改一个不存在的文章。 为此使用了方法 [modifieArticle],该方法会将修改的行数设为 0 或 1。此处应为 0,但实际却抛出了类型为 [IBatisNet.Common.Exceptions.ConcurrentException] 的异常。
  • 在 [changerStockArticle] 中,又出现了一次 [update] 类型的操作。这涉及将库存减少的数量大于当前库存。 为此,我们使用了方法 [changerStockArticle],该方法会将修改的行数设为 0 或 1。 命令 SQL 的编写旨在避免执行更新操作(参见命令 SQL "changerStockArticle" 在 articles.xml 中),该操作会导致库存变为负数。 在此,我们预期 [changerStockArticle] 方法的结果应为 0。然而,再次出现了 [IBatisNet.Common.Exceptions.ConcurrentException] 类型的异常。

可能的错误原因有很多:

  1. [ArticlesDaoSqlMap]类的代码存在错误。这种可能性确实存在。不过,该类是从Java类移植而来的,而该Java类在SqlMap的Java版本中运行正常。
  2. SqlMap的.NET版本存在缺陷
  3. Firebird 的 ODBC 驱动程序存在缺陷
  4. ……

鉴于尚无定论,我们将通过拦截著名的 [IBatisNet.Common.Exceptions.ConcurrentException] 异常来绕过这一障碍。[ArticlesDaoSqlMap] 类的更新代码如下:

....
Namespace istia.st.articles.dao

    Public Class ArticlesDaoSqlMap
        Implements IArticlesDao

         ' 私有字段
        Dim mappeur As SqlMapper = Mapper.Instance

         ' 所有商品列表
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
...
        End Function

         ' 添加商品
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
...
        End Function

         ' 删除商品
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                     ' id:要删除的文章ID
                     ' 删除
                    Return mappeur.Delete("deleteArticle", idArticle)
                Catch ex As Exception
                    If ex.GetType.Equals(GetType(IBatisNet.Common.Exceptions.ConcurrentException)) Then Return 0
                    Throw New Exception("Erreur lors de la suppression de l'article d'id [" + idArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' 修改文章
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                     ' 更新
                    Return mappeur.Update("modifyArticle", unArticle)
                Catch ex As Exception
                    If ex.GetType.Equals(GetType(IBatisNet.Common.Exceptions.ConcurrentException)) Then Return 0
                    Throw New Exception("Erreur lors de la mise à jour de l'article [" + unArticle.ToString + "] : [" + ex.ToString + "]")
                End Try
            End SyncLock
        End Function

         ' 搜索文章
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
...
        End Function

         ' 删除所有商品
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
....
        End Sub

         ' 更改商品库存
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                     ' id:要修改库存的商品ID
                     ' 业务类型:库存变动
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' 更新
                    Return mappeur.Update("changerStockArticle", paramètres)
                Catch ex As Exception
                    If ex.GetType.Equals(GetType(IBatisNet.Common.Exceptions.ConcurrentException)) Then Return 0
                    Throw New Exception(String.Format("Erreur lors du changement de stock [{0},{1}] : {2}", idArticle, mouvement, ex.ToString))
                End Try
            End SyncLock
        End Function
    End Class
End Namespace

修改位于第 28、41、69 行。 对于类型为 [UPDATE, DELETE] 的 SQL 操作,若发生类型为 [IBatisNet.Common.Exceptions.ConcurrentException] 的异常,则返回 0 作为结果,以此表明未修改或删除任何行。 完成上述操作后,将重新生成该项目的 DLL,将其放置在 [test1] 文件夹中,并重新运行 NUnit 测试:

Image

这次没问题了。今后我们将使用这个 DLL。

2.5.6.9. 将新层 [dao] 集成到应用程序 [webarticles] 中

2.5.6.9.1. 数据源 ODBC

此处我们测试第2.3.3.1节中研究的数据源ODBC。该数据源通过SqlMap进行调用。

我们遵循第2.3.4节中的步骤。对文件夹[runtime]的内容进行以下修改:

  • 在文件夹 [bin] 中,将旧图层 DLL旧层 [dao] 中的 DLL 被新层 [dao] 中的 DLL 所替换,该新层由类 [ArticlesDaoSqlMap] 实现。 我们在此添加了 Firebird 所需的 DLL 以及 SqlMap:

Image

  • 在 [runtime] 中,放置 SqlMap 和 [providers.config, sqlmap.config, properties.xml, articles.xml] 的配置文件:

Image

  • 在 [runtime] 中,将 [web.config] 的配置文件替换为一个考虑了新实现类的文件:
<?xml version="1.0" encoding="iso-8859-1" ?>
<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 type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
            <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoSqlMap, webarticles-dao"/>
            <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
                <constructor-arg index="0">
                    <ref object="articlesDao" />
                </constructor-arg>
            </object>
        </objects>
    </spring>
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx" />
        <add key="urlInfos" value="vues/infos.aspx" />
        <add key="urlErreurs" value="vues/erreurs.aspx" />
        <add key="urlListe" value="vues/liste.aspx" />
        <add key="urlPanier" value="vues/panier.aspx" />
        <add key="urlPanierVide" value="vues/paniervide.aspx" />
    </appSettings>
</configuration>

注释

  • 第 14 行将新类 [ArticlesDaoSqlMap] 的一个实例与单例 [articlesDao] 关联起来。这是唯一的修改。

现在可以进行测试了。我们将 Web 服务器 [Cassini] 配置为与之前的测试相同。我们使用以下值初始化文章表:

Image

使用浏览器访问 URL [http://localhost/webarticles/main.aspx]:

Image

现在检查 [ARTICLES] 表的内容:

Image

商品 [couteau] 和 [cuiller] 已成功采购,其库存已按采购数量相应减少。商品 [fourchette] 因需求数量超过库存数量而未能采购。 建议读者进行进一步测试。

2.5.6.9.2. 数据源 MSDE

在此我们测试第2.4.3.1节中讨论的数据源MSDE。该数据源通过SqlMap进行调用。我们采用与之前相同的操作步骤,对文件[runtime]的内容进行如下修改:

  • [bin]文件夹的内容保持不变
  • 在 [runtime] 中,SqlMap 和 [providers.config, properties.xml] 的配置文件发生变更。[sqlmap.config, articles.xml] 的配置文件保持不变。
  • 文件 [providers.config] 配置了一个新的 <provider>:
<?xml version="1.0" encoding="utf-8" ?> 

<providers>
    <clear/>
    <provider
        name="sqlServer1.1"
        assemblyName="System.Data, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        connectionClass="System.Data.SqlClient.SqlConnection"
        commandClass="System.Data.SqlClient.SqlCommand"
        parameterClass="System.Data.SqlClient.SqlParameter"
        parameterDbTypeClass="System.Data.SqlDbType"
        parameterDbTypeProperty="SqlDbType"
        dataAdapterClass="System.Data.SqlClient.SqlDataAdapter"
        commandBuilderClass="System.Data.SqlClient.SqlCommandBuilder"    
        usePositionalParameters = "false"    
        useParameterPrefixInSql = "true"
        useParameterPrefixInParameter = "true"                
        parameterPrefix="@"
    />        
</providers>

该 <provider> 使用 .NET 类来访问 SQL Server 数据源。它作为标准组件集成在随 SqlMap 一起分发的 [providers.config] 模板文件中。

  • 文件 [properties.xml] 定义了源 MSDE 的 <provider> 以及其连接字符串:
<?xml version="1.0" encoding="utf-8" ?> 
<settings>
    <add key="provider" value="sqlServer1.1" />
    <add 
        key="connectionString" 
        value="Data Source=portable1_tahe\msde140405;Initial Catalog=dbarticles;UID=admarticles;PASSWORD=mdparticles;"/>
</settings>
  • 在 [runtime] 中,配置文件 [web.config] 保持不变。

我们已准备好进行测试。Web 服务器 [Cassini] 保持其常规配置。我们将源 MSDE 的文章表初始化为 [EMS MS SQL Manager]:

Image

通过浏览器请求 URL [http://localhost/webarticles/main.aspx]:

Image

现在我们来检查表 [ARTICLES] 与 [EMS MS SQL Manager] 的内容:

Image

物料 [ballon foot] 和 [raquette tennis] 已采购,其库存已按采购数量减少。物料 [rollers] 无法采购,因为需求数量超过了库存数量。 建议读者进行进一步测试。

2.5.6.9.3. 数据源 OleDb

在此我们测试第2.4.5.1节中介绍的数据源ACCESS。该数据源通过SqlMap进行调用。我们采用与之前相同的操作步骤,对文件[runtime]的内容进行以下修改:

  • [bin]文件夹的内容保持不变
  • 在 [runtime] 中,SqlMap 和 [providers.config, properties.xml] 的配置文件发生了变化。[sqlmap.config, articles.xml] 的配置文件保持不变。
  • 文件 [providers.config] 配置了一个新的 <provider>:
<?xml version="1.0" encoding="utf-8" ?> 

<providers>
    <clear/>
    <provider 
        name="OleDb1.1" 
        enabled="true" 
        assemblyName="System.Data, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
        connectionClass="System.Data.OleDb.OleDbConnection" 
        commandClass="System.Data.OleDb.OleDbCommand" 
        parameterClass="System.Data.OleDb.OleDbParameter" 
        parameterDbTypeClass="System.Data.OleDb.OleDbType" 
        parameterDbTypeProperty="OleDbType" 
        dataAdapterClass="System.Data.OleDb.OleDbDataAdapter" 
        commandBuilderClass="System.Data.OleDb.OleDbCommandBuilder" 
        usePositionalParameters = "true"
        useParameterPrefixInSql = "false"
        useParameterPrefixInParameter = "false"
        parameterPrefix = ""
    />
</providers>

该 <provider> 使用数据源访问类 NET。它作为标准组件集成在随 SqlMap 一起分发的模板文件 [providers.config] 中。

  • 文件 [properties.xml] 定义了数据源 OleDb 的 <provider> 以及该数据源的连接字符串:
<?xml version="1.0" encoding="utf-8" ?> 
<settings>
    <add key="provider" value="OleDb1.1" />
    <add 
        key="connectionString" 
        value="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;"/>
</settings>
  • 在 [runtime] 中,配置文件 [web.config] 保持不变。

我们已准备好进行测试。Web 服务器 [Cassini] 保持其常规配置。我们按以下方式初始化源 ACCESS 的文章表:

Image

使用浏览器访问 URL 和 [http://localhost/webarticles/main.aspx]:

Image

现在,我们使用以下命令查看表 [ARTICLES] 的内容:

Image

商品 [pantalon] 和 [jupe] 已成功采购,其库存已按采购数量相应减少。商品 [manteau] 因需求数量超过库存数量而未能采购。 建议读者进行进一步测试。

2.5.7. 结论

至此,这篇长篇教程文章告一段落。我们做了什么?

  • 我们通过四种不同方式实现了三层Web应用程序中[dao]层:
    1. 使用访问类 .NET 访问源代码 ODBC
    2. 使用访问类 .NET 访问源代码 SQL Server
    3. 使用访问类 .NET 访问源文件 OleDb
    4. 使用第三方访问类访问 Firebird 数据库
  • 每次,我们都将新的 [dao] 层集成到三层应用程序 [webarticles] 中,而无需重新编译 [web, domain, dao] 或 [web, domain] 中的任何层
  • 最后,我们推出了工具 [SqlMap],它使我们能够创建一个 [dao] 层,该层能够以对代码透明的方式适配不同的数据源。因此,借助这个新层,我们得以依次使用之前 1 到 3 号实现中的数据源。 这一过程是通过配置文件以透明的方式实现的。
  • 我们展示了Spring和SqlMap工具为三层Web应用程序带来的高度灵活性。