Skip to content

2. Частина 2

2.1. Introduction

Спочатку нагадаємо, що було зроблено в частині 1, зокрема, про використану трирівневу архітектуру [web, domain, dao]. У запропонованому рішенні рівень [dao] був тестовим: джерело даних реалізовувалося об’єктом [ArrayList]. У цій статті ми зупинимося на шарі [dao], представивши різні можливі варіанти його реалізації, коли дані містяться в SGBD.

Використані інструменти:

  • SGBD Firebird — див. додаток, параграф 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: [Développement WEB avec ASP.NET 1.1 ]
  • використання компонента IoC у Spring: [Spring IoC pour .NET ]
  • документація 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. Веб-додаток «articles» — нагадування

Тут ми представляємо елементи спрощеного веб-додатку для електронної комерції, розглянутого в частині 1. Цей додаток дозволяє веб-користувачам:

  • переглядати перелік товарів із бази даних
  • додавати деякі з них до електронного кошика
  • підтвердити його. Єдиним наслідком такого підтвердження є оновлення в базі даних запасів придбаних товарів.

2.2.1. Види додатка

Користувачеві доступні такі екрани:

  • екран [ERREURS], який повідомляє про будь-які помилки в роботі програми

Image

2.2.2. Загальна архітектура додатка

Додаток, створений у частині 1, має трирівневу архітектуру:

  • три рівні стали незалежними завдяки використанню інтерфейсів
  • інтеграція різних рівнів була здійснена за допомогою Spring
  • кожен рівень має окремий простір імен: web (рівень UI), domain (бізнес-рівень) та dao (рівень доступу до даних).

Додаток відповідає архітектурі MVC (Модель — Представлення — Контролер). Якщо повернутися до наведеної вище схеми шарів, архітектура MVC вписується в неї наступним чином:

Обробка запиту клієнта відбувається за такими етапами:

  1. клієнт надсилає запит до контролера. Цей контролер у даному випадку — це сторінка .aspx, якій відведено особливу роль. Вона обробляє всі запити клієнтів. Це вхідні ворота додатка. Це літера «C» у MVC.
  2. контролер обробляє цей запит. Для цього йому може знадобитися допомога бізнес-шару, який у структурі MVC називається моделлю M.
  3. Контролер отримує відповідь від бізнес-шару. Запит клієнта було оброблено. Це може призвести до кількох можливих відповідей. Класичним прикладом є
    • сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
    • сторінка підтвердження в іншому випадку
  4. контролер обирає відповідь (= представлення), яку потрібно надіслати клієнту. Найчастіше це сторінка, що містить динамічні елементи. Контролер передає їх до представлення.
  5. Вигляд надсилається клієнту. Це «V» у MVC.

2.2.3. Шаблон

Модель M у MVC складається з таких елементів:

  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»:

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]: інтерфейс доступу до шару [dao]. Це єдиний інтерфейс, який бачить шар [domain]. Він не бачить жодного іншого.
- [Article]: клас, що визначає статтю
- [ArticlesDaoArrayList]: клас реалізації інтерфейсу [IArticlesDao] із класом [ArrayList]
шар доступу до даних — повністю знаходиться в шарі [dao] трирівневої архітектури веб-додатку
webarticles-domain
- [IArticlesDomain]: інтерфейс доступу до шару [domain]. Це єдиний інтерфейс, який бачить веб-шар. Інших він не бачить.
- [AchatsArticles]: клас, що реалізує [IArticlesDomain]
- [Achat]: клас, що представляє покупку клієнта
- [Panier]: клас, що представляє сукупність покупок клієнта
представляє модель покупок у мережі — повністю розміщується в шарі [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. Тестування

Ми налаштовуємо веб-сервер [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], [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, не впливаючи на роботу програми. Недоліком є те, що драйвер ODBC, який використовує лише спільні для всіх SGBD функції, зазвичай менш ефективний, ніж драйвер, спеціально написаний для використання всього потенціалу конкретного SGBD. Приклад створення коду для ODBC можна знайти в розділі 3.7.

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: ідентифікатор користувача
             ' password: його пароль
...
        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 імпортується простір імен, що містить класи .NET для доступу до джерел ODBC
  • у рядку 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 — готуємо запити SQL в об’єктах [OdbcCommand]. Це дозволить нам уникнути їхнього повторного формування щоразу, коли вони знадобляться. Формальні параметри ? запитів будуть замінені на фактичні значення під час виконання запиту.

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 — створюється екземпляр об’єкта [OdbcDataReader], необхідного для обробки результату [Select]
  • рядки 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], який містить запит SQL типу [Insert, Update, Delete].
  • З'єднання відкривається у рядку 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)

5 значень, необхідних для запиту, надаються у рядках 7–11.

  • У рядках 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
  • запит [Select], що вимагає всіх статей, передається методу [executeQuery] — рядок 6
  • отриманий список повертається у рядку 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]

Проєкт Visual Studio цієї нової версії шару [dao] має таку структуру:

Image

Проєкт налаштовано на генерацію DLL під назвою [webarticles-dao.dll]:

2.3.3. Тести Nunit для шару [dao]

2.3.3.1. Створення джерела даних ODBC-Firebird

Для тестування нашого нового шару [dao] нам потрібне джерело даних ODBC, а отже, і база даних. Ми використовуємо Firebird SGBD (розділ 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] без змін та перекомпіляції.

  • Створимо у папці Visual Studio нашого нового шару [dao] папку [tests] (праворуч нижче), скопіювавши папку [bin] з тестового проєкту початкового шару [dao] (ліворуч нижче). За потреби читачеві пропонується ознайомитися з тестовим проектом першої версії шару [dao] у першій частині статті.
  • у папці [tests] замінюємо DLL та [webarticles-dao.dll], отримані зстарого шару [dao] на DLL та [webarticles-dao.dll], що походять з нового шару [dao]
  • змінимо файл конфігурації [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] із папки [tests], зазначеної вище, і виконуємо тест [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] файл DLL зі старого шару [dao] замінено на файл DLL з нового шару [dao]
  • у [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 до синглтона [articlesDao] прив’язується екземпляр нового класу [ArticlesDaoPlainODBC]. Це єдина зміна. Ми вже стикалися з нею під час тестування нового шару [dao].

Ми готові до тестування. Налаштовуємо веб-сервер [Cassini] так само, як описано в розділі 2.2.4. Ініціалізуємо таблицю статей [Firebird] такими значеннями:

Image

Переконайтеся, що веб-сервер Cassini, а також SGBD і [Firebird] запущені. У браузері вводимо URL-адресу [http://localhost/webarticles/main.aspx]:

Image

Тепер перевіримо вміст таблиці [ARTICLES] у базі даних [Firebird]:

Image

Товари [parapluie] та [bottes] було придбано, а їхні запаси зменшено на кількість, що була придбана. Товар [chapeau] не вдалося придбати, оскільки запитувана кількість перевищувала кількість на складі. Пропонуємо читачеві провести додаткові тести.

2.3.4.2. Висновок

Що ми зробили?

  • ми відновили робочу версію попередньої версії;
  • ми замінили DLL з шару [dao] на нову версію. DLL шарів [web] та [domain] залишилися без змін;
  • ми змінили файл конфігурації [web.config], щоб він враховував новий клас реалізації шару [dao]

Все це забезпечує чіткість коду та значну гнучкість у розвитку веб-додатку. Ці важливі характеристики забезпечуються двома архітектурними рішеннями:

  • доступ до шарів через інтерфейси
  • інтеграція та конфігурація шарів за допомогою Spring.

Зараз ми пропонуємо нову реалізацію шару [dao].

2.4. Клас реалізації [ArticlesDaoSqlServer]

Друга реалізація шару [dao] передбачає, що дані знаходяться в базі даних SQL Server. Microsoft надає SGBD під назвою MSDE, який є обмеженою версією сервера SQL. Інформацію про те, як його отримати та встановити, наведено в додатку, параграф 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: ідентифікатор користувача
             ' password: його пароль

Повний код класу [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
                     'виконується запит на вибірку
                    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

Проєкт налаштовано на генерацію DLL під назвою [webarticles-dao.dll]:

2.4.3. Тести Nunit для шару [dao]

2.4.3.1. Створення сервера даних SQL

Щоб протестувати наш новий шар [dao], нам потрібне джерело даних SQL Server, а отже, і SGBD SQL Server. Фактично ми будемо використовувати SGBD MSDE (MicroSoft Data Engine) (параграф 3.12), який є версією сервера SQL, обмеженою лише кількістю одночасних користувачів. За допомогою [EMS MS SQL Manager] (параграф 3.14) ми створюємо таку базу статей в екземплярі MSDE під назвою [portable1_tahe\msde140405]:

Image

База належить користувачеві [mdparticles] з паролем [admarticles]. Команда Transact-SQL для створення таблиці [ARTICLES] виглядає так:

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

Клас тесту Nunit для класу реалізації [ArticlesDaoSqlServer] є таким самим, як і для класу [ArticlesDaoPlainODBC] (див. розділ 2.3.3.2). Ми дотримуємося аналогічного підходу для підготовки тесту Nunit для цього класу:

  • у папці Visual Studio проекту [dao-sqlserver] створюємо папку [tests] (праворуч), скопіювавши папку [tests] з проекту [dao-odbc] (ліворуч):
  • у папці [tests] проекту [dao-sqlserver], замінюємо DLL [webarticles-dao.dll] на DLL [webarticles-dao.dll], отримані в результаті генерації проекту [dao-sqlserver]
  • ми змінюємо файл конфігурації [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] завантажуємо DLL [test-webarticles-dao.dll] із папки [tests], зазначеної вище, і виконуємо тест [testGetAllArticles]:

Image

Незважаючи на те, що тестовому класу спочатку було надано ім’я [NUnitTestArticlesDaoArrayList], яке було збережено, оскільки ми використовуємо DLL та [tests-webarticles-dao.dll], отримані з цього класу, саме клас [ArticlesDaoSqlserver] тут і тестується. Знімок екрана показує, що ми правильно витягли статті, які розмістили в таблиці [ARTICLES]. Тепер проведемо повний набір тестів:

Image

У лівому вікні ми бачимо список протестованих методів. Колір крапки перед назвою кожного методу вказує на успішне виконання (зелений) або невдачу (червоний) методу. Читач, який переглядає цей документ на екрані, зможе побачити, що всі тести пройшли успішно.

2.4.4. Інтеграція нового шару [dao] у додаток [webarticles]

Ми дотримуємося підходу, описаного в розділі 2.3.4. Вносимо такі зміни до вмісту папки [runtime]:

  • у папці [bin] файл DLL ізстарого шару [dao] замінюється на DLL з нового шару [dao], реалізованого класом [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 до синглтона [articlesDao] прив’язується екземпляр нового класу [ArticlesDaoSqlServer]. Це єдина зміна. Ми вже стикалися з нею під час тестування нового шару [dao]

Ми готові до тестування. Ми зберігаємо ту саму конфігурацію веб-сервера [Cassini], що й раніше. Ми ініціалізуємо таблицю статей [MSDE] із такими значеннями:

Image

Переконайтеся, що веб-сервер Cassini, а також SGBD та MSDE (у даному випадку екземпляр portable1_tahe\msde140405) запущені. За допомогою браузера зверніться до URL та [http://localhost/webarticles/main.aspx]:

Image

Тепер перевіримо вміст таблиці [ARTICLES] у базі даних [MSDE]:

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 [articles.mdb], яка містить лише одну таблицю:

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
                     'виконується запит на вибірку
                    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

Проєкт налаштовано на генерацію DLL під назвою [webarticles-dao.dll]:

2.4.5.4. Тести Nunit для шару [dao]

2.4.5.4.1. Тестовий клас NUnit

Клас тесту Nunit для класу реалізації [ArticlesDaoOleDb] є таким самим, як і для класу [ArticlesDaoPlainODBC] (див. розділ 2.3.3.2). Ми дотримуємося аналогічного підходу для підготовки тесту Nunit для цього класу:

  • у папці Visual Studio проекту [dao-oledb] створюємо папку [tests] (праворуч), скопіювавши папку [tests] з проекту [dao-odbc] (ліворуч):
  • у папці [tests] проекту [dao-oledb] замінюємо 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] файл DLL ізстарого шару [dao] замінюється на DLL з нового шару [dao], реалізованого класом [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 до синглтона [articlesDao] прив’язується екземпляр нового класу [ArticlesDaoOleDb]. Це єдина зміна.

Ми зберігаємо ту саму конфігурацію веб-сервера [Cassini], що й раніше. Ми ініціалізуємо таблицю статей із такими значеннями:

Image

Переконайтеся, що база статей не використовується програмою, такою як Visual Studio або ACCESS. За допомогою браузера ми запитуємо URL [http://localhost/webarticles/main.aspx]:

Image

Тепер перевіримо вміст таблиці [ARTICLES] та ACCESS:

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 року):

Image

Посилання [firebird-net-provider] слід використовувати для завантаження класів .NET, що забезпечують доступ до SGBD Firebird. В результаті встановлення пакета створюється папка, схожа на таку:

Image

Нас цікавлять два елементи:

  • [FirebirdSql.Data.Firebird.dll]: збірка, що містить класи .NET для доступу до SGBD Firebird
  • [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
                     'виконується запит на вибірку
                    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] проекту. Проєкт налаштовано на генерацію збірки DLL під назвою [webarticles-dao.dll]:

2.5.4. Тести Nunit для шару [dao]

2.5.4.1. Клас тесту NUnit

Тестовий клас Nunit для класу реалізації [ArticlesDaoFirebirdProvider] є таким самим, як і для класу [ArticlesDaoPlainODBC] (див. розділ 2.3.3.2). Ми дотримуємося аналогічного підходу для підготовки тесту Nunit для класу [ArticlesDaoFirebirdProvider]:

  • у папці Visual Studio проекту [dao-firebird-provider] створюємо папку [tests] (праворуч), скопіювавши папку [bin] з тестового проекту шару [dao-odbc] (ліворуч):
  • у папці [tests] замінюємо DLL та [webarticles-dao.dll] на DLL та [webarticles-dao.dll], отримані в результаті генерації проекту [dao-firebird-provider]
  • ми змінюємо файл конфігурації [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] завантажуємо DLL та [test-webarticles-dao.dll] із вищезазначеної папки [tests] і виконуємо тест [testGetAllArticles]:

Image

Незважаючи на те, що спочатку класу тесту було присвоєно ім’я [NUnitTestArticlesDaoArrayList], насправді тут тестується саме клас [ArticlesDaoFirebirdProvider]. Знімок екрана показує, що ми правильно отримали статті, які ми розмістили в таблиці [ARTICLES]. Тепер виконаємо всі тести:

Image

Читач, який переглядає цей документ на екрані, побачить, що всі тести пройшли успішно (зелений колір). Що він не може побачити, так це те, що тести пройшли значно швидше, ніж із базою статей, до якої здійснювався доступ через драйвер ODBC з нашої першої реалізації.

2.5.5. Інтеграція нового шару [dao] у додаток [webarticles]

Ми дотримуємося підходу, який вже двічі пояснювали, зокрема в розділі 2.3.4. Вносимо такі зміни до вмісту папки [runtime]:

  • у папці [bin] файл DLL ізстарого шару [dao] замінюється на DLL з нового шару [dao], реалізованого класом [ArticlesDaoFirebirdProvider]. Ми також розміщуємо там DLL, необхідний для Firebird [FirebirdSql.Data.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 до синглтона [articlesDao] прив’язується екземпляр нового класу [ArticlesDaoFirebirdProvider]. Це єдина зміна.

Ми готові до тестування. Налаштовуємо веб-сервер [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

Ми написали чотири різні реалізації шару [dao] для нашого додатка [webarticles]. У кожному випадку нам вдалося інтегрувати новий рівень [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]. Пройдіть процес завантаження до кінця. У результаті отримаємо архів у форматі zip, що містить такі файли:

Image

У проєкті Visual Studio, що використовує Ibatis SqlMap, потрібно виконати дві дії:

  • помістити вищезазначені файли в папку [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], по суті, визначає рядок підключення до бази даних, яка містить дані. З'єднання з базою даних буде відкрито шляхом інстанціювання класу [Connection], визначеного у файлі [providers.config], у конструктор якого буде передано рядок з'єднання, визначений у файлі [sqlmap.config].
  • Файли маппінгу визначають:
    • відповідності між рядками таблиць даних та класом .NET, екземпляри якого міститимуть ці рядки
    • операції SQL, які потрібно виконати. Вони ідентифікуються за іменем. Код .NET виконує ці операції за їхніми іменами, що призводить до видалення всього коду SQL з коду .NET.

2.5.6.4. Конфігураційні файли проєкту [dao-sqlmap]

Розглянемо на прикладі точну природу файлів конфігурації SqlMap. Розглянемо випадок, коли джерелом даних є джерело ODBC Firebird з розділу 2.3.3.1.

2.5.6.4.1. providers.config

Файл [providers.config] для джерела ODBC виглядає так:

<?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]). Якщо він активований, то DLL, на який є посилання у рядку 8, має бути доступним. Файл [providers.config] може містити кілька тегів <provider>.
  • рядок 8 — ім’я збірки, що містить класи, визначені в рядках 9–15
  • рядок 9 — клас, який використовується для створення з’єднання
  • рядок 10 — клас, який слід використовувати для створення об’єкта [Command] для відправлення команд SQL
  • рядок 11 — клас, який використовується для управління параметрами налаштованої команди SQL
  • рядок 12 — клас переліку можливих типів даних для полів таблиці
  • рядок 13 — ім’я властивості об’єкта [Parameter], що містить тип значення цього параметра
  • рядок 14 — ім’я класу [Adapter], що дозволяє створювати об’єкти [DataSet] на основі джерела даних
  • рядок 15 — ім'я класу [CommandBuilder], який у поєднанні з об'єктом [Adapter] дозволяє автоматично генерувати його властивості [InsertCommand, DeleteCommand, UpdateCommand] на основі його властивості [SelectCommand]
  • рядки 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, отримується за допомогою нотації ${C} у файлі [sqlmap.config]. Ось файл [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]. Його значенням є ім’я тегу <provider>, яке слід використовувати у файлі [providers.config]

рядок 4 — визначено ключ [connectionString]. Його значенням є рядок підключення, який слід використовувати для встановлення з’єднання з джерелом даних ODBC Firebird.

  • рядки 4–7 — параметри конфігурації:
    • рядок 5 — запити SQL ідентифікуватимуться за іменем, яке саме по собі може бути частиною простору імен. [useStatementNamespaces="false"] вказує, що простори імен не використовуватимуться.
    • рядок 6 — SqlMap має різні стратегії кешування для мінімізації звернень до джерела даних. [cacheModelsEnabled="false"] вказує, що жодна з них не використовуватиметься.
  • рядки 9–13 — визначаються характеристики джерела даних:
    • рядок 10 — ім’я <provider> з [providers.config], яке слід використовувати
    • рядок 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) таблиці відповідає властивість (property) класу [Article]. Це зіставлення дозволяє [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"]. Об’єкт-параметр повинен мати властивості (id, nom, prix, stockactuel, stockminimum), на які посилається налаштована команда SQL.
  • рядки 29–31 — визначається команда SQL Delete [deleteArticle], призначена для видалення товару, номер якого відомий як #value#. Цей номер буде передано як параметр: [parameterClass="int"]. Це загальне правило. Коли параметр є єдиним, на нього посилаються за допомогою ключового слова #value# у тексті команди SQL.
  • рядки 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] будуть розміщені в тій самій папці, що й тестовані бінарні файли.
  • у випадку веб-додатку вони будуть розміщені в кореневій папці додатка.

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. Він повинен відповідати атрибуту [parameterClass] команди SqlMap. У [articles.xml] міститься [parameterClass=Nothing]. Тому тут передається нульовий покажчик.
  • результат має тип IList. Об’єкти цього списку вказані атрибутом [resultMap] команди SQL-select: [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] порожня, ми отримуємо об’єкт [IList] із 0 елементами.

Щоб виконати команду SqlMap [getArticleById], потрібно написати:

dim unArticle as Article=CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
  • метод [QueryForObject] дозволяє отримати результат виконання команди SELECT, яка повертає лише один рядок
  • перший параметр — це назва команди SqlMap, яку потрібно виконати
  • другий параметр — це параметр, який потрібно передати у запиті SQL. Він повинен відповідати атрибуту [parameterClass] команди SqlMap. У [articles.xml] міститься [parameterClass="int"]. Тому тут передається ціле число, що позначає номер шуканого товару.
  • Результат має тип Object. Якщо SELECT не повернув жодного рядка, то в якості результату отримуємо нульовий покажчик (nothing).

Щоб виконати команду SqlMap [insertArticle], потрібно написати:

                    mappeur.Insert("insertArticle", unArticle)
  • метод [Insert] дозволяє виконувати команди SQL INSERT
  • перший параметр — це назва команди SqlMap, яку потрібно виконати
  • другий параметр — це параметр, який потрібно передати цій команді. Він повинен відповідати атрибуту [parameterClass] команди SqlMap. У [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, яку потрібно виконати
  • другий параметр — це параметр, який потрібно передати цій команді. Він повинен відповідати атрибуту [parameterClass] команди SqlMap. У [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, яку потрібно виконати
  • другий параметр — це параметр, який потрібно передати цій команді. Він повинен відповідати атрибуту [parameterClass] команди SqlMap. У [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)
  • другий параметр відповідає атрибуту [parameterClass] команди SqlMap. У [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: ідентифікатор статті, яку потрібно видалити
                     ' видалення
                    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: ідентифікатор статті, яку шукають
                    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: ідентифікатор товару, запаси якого змінюються
                     ' рух: рух запасів
                    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] проекту. Проєкт налаштовано на генерацію DLL під назвою [webarticles-dao.dll]:

2.5.6.8. Тести Nunit для шару [dao]

2.5.6.8.1. Тестовий клас NUnit

Тестовий клас Nunit для класу реалізації [ArticlesDaoSqlMap] є таким самим, як і для класу [ArticlesDaoPlainODBC] (див. розділ 2.3.3.2). Ми дотримуємося аналогічного підходу для підготовки тесту Nunit для класу [ArticlesDaoSqlMap]:

  • у папці Visual Studio проекту [dao-sqlmap] створюємо папку [test1] (праворуч), скопіювавши папку [tests] з проекту [dao-odbc] (ліворуч):
  • у папці [tests] замінюємо DLL та [webarticles-dao.dll] на DLL та [webarticles-dao.dll], отримані в результаті генерації проекту [dao-sqlmap].
  • ми додаємо необхідні DLL до SqlMap, а також розглянуті конфігураційні файли [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

Таблиця [ARTICLES] у джерелі даних Firebird заповнена такими елементами:

Image

Ми готові до тестування. За допомогою програми [Nunit-Gui] завантажуємо DLL та [test-webarticles-dao.dll] із вищезгаданої папки [test1] і виконуємо тест [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, який працював коректно з версією SqlMap для Java.
  2. версія .NET класу SqlMap містить помилку
  3. драйвер ODBC для Firebird містить помилки
  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: ідентифікатор статті, яку потрібно видалити
                     ' видалення
                    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: ідентифікатор товару, запаси якого змінюються
                     ' рух: рух запасів
                    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. Для операцій SQL типу [UPDATE, DELETE], якщо виникає виняток типу [IBatisNet.Common.Exceptions.ConcurrentException], як результат повертається 0, що вказує на те, що жоден рядок не було змінено чи видалено. Після цього DLL проекту генерується заново, розміщується в папці [test1], а тести NUnit запускаються знову:

Image

Цього разу все гаразд. Відтепер ми будемо працювати з цим DLL.

2.5.6.9. Інтеграція нового шару [dao] у додаток [webarticles]

2.5.6.9.1. джерело даних ODBC

Тут ми тестуємо джерело даних ODBC, розглянуте в розділі 2.3.3.1. Воно використовується тут через SqlMap.

Ми дотримуємося підходу, описаного в розділі 2.3.4. Вносимо такі зміни до вмісту папки [runtime]:

  • у папці [bin] файл DLL ізстарого шару [dao] замінюється на DLL з нового шару [dao], реалізованого класом [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 до синглтона [articlesDao] прив’язується екземпляр нового класу [ArticlesDaoSqlMap]. Це єдина зміна.

Ми готові до тестування. Налаштовуємо веб-сервер [Cassini] так само, як і в попередніх тестах. Ініціалізуємо таблицю статей із такими значеннями:

Image

За допомогою браузера ми звертаємося до URL [http://localhost/webarticles/main.aspx]:

Image

Тепер перевіримо вміст таблиці [ARTICLES]:

Image

Артикули [couteau] та [cuiller] було придбано, а їхні запаси зменшено на кількість, що була придбана. Артикул [fourchette] не вдалося придбати, оскільки запитувана кількість перевищувала кількість на складі. Пропонуємо читачеві провести додаткові тести.

2.5.6.9.2. джерело даних MSDE

Тут ми тестуємо джерело даних MSDE, розглянуте в розділі 2.4.3.1. Воно використовується тут через 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. Він стандартно інтегрований у файл-шаблон [providers.config], що поширюється разом із SqlMap.

  • Файл [properties.xml] визначає <provider> джерела MSDE, а також ланцюжок підключення до нього:
<?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] не змінюється.

Ми готові до тестування. Веб-сервер [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

Тут ми тестуємо джерело даних ACCESS, представлене в розділі 2.4.5.1. Воно використовується тут через 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 для доступу до джерел даних OleDb. Він стандартно інтегрований у файл-шаблон [providers.config], що поширюється разом із SqlMap.

  • Файл [properties.xml] визначає <provider> джерела OleDb, а також ланцюжок підключення до нього:
<?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] не змінюється.

Ми готові до тестування. Веб-сервер [Cassini] зберігає свою звичну конфігурацію. Ми ініціалізуємо таблицю статей джерела ACCESS наступним чином:

Image

За допомогою браузера ми звертаємося до URL [http://localhost/webarticles/main.aspx]:

Image

Тепер перевіримо вміст таблиці [ARTICLES] за допомогою:

Image

Артикули [pantalon] та [jupe] було придбано, а їхні запаси зменшено на кількість, що була придбана. Артикул [manteau] не вдалося придбати, оскільки запитувана кількість перевищувала кількість на складі. Пропонуємо читачеві провести додаткові тести.

2.5.7. Висновок

На цьому ми завершуємо цю довгу статтю-посібник. Що ми зробили?

  • Ми реалізували рівень [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 надають тришаровим веб-додаткам.