Skip to content

2. Część 2

2.1. Introduction

Zaczniemy od przypomnienia tego, co zostało omówione w części 1, a w szczególności od zastosowanej architektury trójwarstwowej [web, domain, dao]. W proponowanym rozwiązaniu warstwa [dao] pełniła rolę warstwy testowej: źródło danych zostało zaimplementowane przez obiekt [ArrayList]. W niniejszym artykule skupimy się na warstwie [dao], przedstawiając różne możliwe sposoby jej implementacji w sytuacji, gdy dane znajdują się w obiekcie SGBD.

Wykorzystane narzędzia:

  • SGBD Firebird – patrz załącznik, punkt 3.5.
  • SGBD MSDE (Microsoft Data Engine) – patrz załącznik, punkt 3.12.
  • IBExpert, wersja osobista do graficznego zarządzania SGBD Firebird – patrz załącznik, punkt 3.6.
  • EMS MS SQL Manager do graficznego zarządzania SGBD MSDE – patrz załącznik, punkt 3.14.
  • Ibatis SqlMap dla warstwy dostępu do danych w SGBD – patrz punkt 2.5.6.2.

W skali początkujący–średniozaawansowany–zaawansowany niniejszy dokument plasuje się w części [intermédiaire-avancé]. Jego zrozumienie wymaga spełnienia różnych warunków wstępnych. Niektóre z nich można poznać w dokumentach, które sam napisałem. W takim przypadku je cytuję. Oczywiste jest, że jest to jedynie sugestia i że czytelnik może korzystać ze swoich ulubionych dokumentów.

  • język VB.net: [Introduction au langage VB.NET par l'exemple ]
  • programowanie stron internetowych w języku VB.net: [Développement WEB avec ASP.NET 1.1 ]
  • korzystanie z komponentu IoC w Spring: [Spring IoC pour .NET ]
  • dokumentacja Ibatis SqlMap: [http://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • dokumentacja Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf]
  • dokumentacja Spring.net: [http://www.springframework.net/documentation.html]

2.2. Aplikacja webarticles – Podsumowanie

Poniżej przedstawiamy elementy uproszczonej aplikacji internetowej do handlu elektronicznego omówionej w części 1. Aplikacja ta umożliwia użytkownikom internetowym:

  • przeglądanie listy artykułów pochodzących z bazy danych
  • dodawanie niektórych z nich do koszyka
  • zatwierdzenie koszyka. Jedynym skutkiem tego zatwierdzenia jest aktualizacja stanów magazynowych zakupionych artykułów w bazie danych.

2.2.1. Widoki aplikacji

Różne widoki prezentowane użytkownikowi są następujące:

  • widok [ERREURS], który sygnalizuje wszelkie błędy aplikacji

Image

2.2.2. Ogólna architektura aplikacji

Aplikacja stworzona w części 1 ma architekturę trójwarstwową:

  • trzy warstwy zostały uniezależnione dzięki zastosowaniu interfejsów
  • integrację poszczególnych warstw zrealizowano za pomocą biblioteki Spring
  • każda warstwa posiada oddzielną przestrzeń nazw: web (warstwa UI), domain (warstwa biznesowa) oraz dao (warstwa dostępu do danych).

Aplikacja jest zgodna z architekturą MVC (Model – Widok – Kontroler). Jeśli powrócimy do powyższego schematu warstwowego, architektura MVC wpisuje się w niego w następujący sposób:

Przetwarzanie żądania klienta przebiega zgodnie z następującymi etapami:

  1. klient wysyła żądanie do kontrolera. Kontrolerem jest tutaj strona .aspx, której przypisano szczególną rolę. Przechodzą przez nią wszystkie żądania klientów. Stanowi ona bramę wejściową do aplikacji. Jest to litera C w skrócie MVC.
  2. kontroler przetwarza to żądanie. W tym celu może potrzebować pomocy warstwy biznesowej, zwanej modelem M w strukturze MVC.
  3. Kontroler otrzymuje odpowiedź od warstwy biznesowej. Żądanie klienta zostało przetworzone. Może to skutkować kilkoma możliwymi odpowiedziami. Klasycznym przykładem jest
    • strona błędów, jeśli żądania nie udało się poprawnie przetworzyć
    • w przeciwnym razie strona potwierdzenia
  4. kontroler wybiera odpowiedź (= widok), którą ma wysłać do klienta. Najczęściej jest to strona zawierająca elementy dynamiczne. Kontroler dostarcza je do widoku.
  5. Widok jest wysyłany do klienta. Jest to „V” w skrócie MVC.

2.2.3. Szablon

Model M w MVC składa się tutaj z następujących elementów:

  1. klasy biznesowe
  2. klasy dostępu do danych
  3. baza danych

2.2.3.1. Baza danych

Baza danych zawiera tylko jedną tabelę o nazwie ARTICLES, wygenerowaną za pomocą następujących poleceń 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
);
/* ograniczenia */
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);
/* klucz główny */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
klucz podstawowy jednoznacznie identyfikujący artykuł
nom
nazwa artykułu
prix
jego cena
stockactuel
aktualny stan magazynowy
stockminimum
poziom zapasów, poniżej którego należy złożyć zamówienie na uzupełnienie zapasów

2.2.3.2. Przestrzenie nazw modelu

Szablon M jest dostarczany w postaci dwóch przestrzeni nazw:

  • istia.st.articles.dao: zawiera klasy dostępu do danych warstwy [dao]
  • istia.st.articles.domain: zawiera klasy biznesowe warstwy [domain]

Każda z tych przestrzeni nazw znajduje się w osobnym pliku „assembly”:

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]: interfejs dostępu do warstwy [dao] Jest to jedyny interfejs widoczny dla warstwy [domain]. Nie widzi ona żadnego innego.
- [Article]: klasa definiująca artykuł
- [ArticlesDaoArrayList]: klasa implementująca interfejs [IArticlesDao] wraz z klasą [ArrayList]
warstwa dostępu do danych – znajduje się w całości w warstwie [dao] trójwarstwowej architektury aplikacji internetowej
webarticles-domain
- [IArticlesDomain]: interfejs dostępu do warstwy [domain]. Jest to jedyny interfejs widoczny dla warstwy internetowej. Nie widzi ona żadnego innego.
- [AchatsArticles]: klasa implementująca [IArticlesDomain]
- [Achat]: klasa reprezentująca zakup klienta
- [Panier]: klasa reprezentująca zbiór zakupów klienta
reprezentuje model zakupów internetowych – znajduje się w całości w warstwie [domain] trójwarstwowej architektury aplikacji internetowej

2.2.4. Wdrożenie i testy aplikacji [webarticles]

2.2.4.1. Wdrożenie

Wdrażamy aplikację opracowaną w części 1 tego artykułu w folderze o nazwie [runtime]:

 

Uwagi:

Folder [runtime] zawiera trzy pliki i dwa podfoldery:

  • kontrolery [global.asax] i [main.aspx]
  • plik konfiguracyjny [web.config]
  • folder [bin], który zawiera:
    • pliki DLL z trzema warstwami: [webarticles-dao.dll], [webarticles-domain.dll], [webarticles-web.dll]
    • pliki niezbędne dla Springa: [Spring-Core.*], [log4net.dll]
  • folder [vues] zawierający kod odpowiedzialny za wyświetlanie poszczególnych widoków.
  • Obecność plików kodu .vb jest zbędna, ponieważ ich skompilowana wersja znajduje się w plikach DLL.

2.2.4.2. Testy

Konfigurujemy serwer WWW [Cassini] w następujący sposób:

Image

z:

Ścieżka fizyczna: D:\data\serge\travail\2004-2005\aspnet\webarticles-010405\runtime\

Ścieżka wirtualna: /webarticles

W przeglądarce wywołujemy adres URL [http://localhost/webarticles/main.aspx]

Image

Przypomnijmy, że warstwa [dao] jest zaimplementowana przez klasę, która przechowuje artykuły w obiekcie [ArrayList]. Klasa ta tworzy początkową listę czterech artykułów. Korzystając z powyższego widoku, używamy linków z menu do wykonywania operacji. Oto kilka z nich. Lewa kolumna przedstawia zapytanie klienta, a prawa kolumna – udzieloną mu odpowiedź.

2.2.5. Nowa wersja warstwy [dao]

W naszej pierwszej implementacji warstwy [dao] interfejs dostępu do danych [IArticlesDao] został zaimplementowany za pomocą klasy przechowującej artykuły w obiekcie [ArrayList]. Dzięki temu nie musieliśmy zagłębiać się w szczegóły tej warstwy i mogliśmy pokazać, że liczy się wyłącznie jej interfejs, a nie implementacja. W ten sposób udało nam się stworzyć działającą aplikację internetową. Składa się ona z trzech warstw: [web], [domain] oraz [dao]. W niniejszym artykule zaproponujemy różne implementacje warstwy [dao]. Każda z nich będzie mogła zastąpić obecną warstwę [dao] bez konieczności wprowadzania jakichkolwiek zmian w warstwach [domain] i [web]. Ta elastyczność wynika z faktu, że:

  • warstwa [domain] nie odnosi się do konkretnej klasy, lecz do interfejsu [IArticlesDao]
  • dzięki Springowi udało nam się ukryć przed warstwą [domain] nazwę klasy implementującej interfejs [IArticlesDao].

2.2.5.1. Elementy warstwy [dao]

Przypomnijmy niektóre elementy warstwy [dao], które zostaną zachowane w nowych implementacjach:

  • - [IArticlesDao]: interfejs dostępu do warstwy [dao]
  • - [Article]: klasa definiująca artykuł

2.2.5.2. Klasa [Article]

Klasa definiująca artykuł ma następujący wygląd:

Imports System

Namespace istia.st.articles.dao

    Public Class Article

         ' pola prywatne
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

         ' identyfikator artykułu
        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

         ' nazwa artykułu
        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

         ' cena artykułu
        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

         ' aktualny stan magazynowy artykułu
        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

         ' minimalny stan magazynowy artykułu
        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

         ' domyślny producent
        Public Sub New()
        End Sub

         ' producent z atrybutami
        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

         ' metoda identyfikacji artykułu
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

Ta klasa oferuje:

  1. konstruktor umożliwiający ustawienie 5 informacji dotyczących artykułu: [id, nom, prix, stockactuel, stockminimum]
  2. publiczne właściwości umożliwiające odczyt i zapis tych 5 informacji.
  3. weryfikację danych wprowadzonych do artykułu. W przypadku błędnych danych generowany jest wyjątek.
  4. metodę toString, która pozwala uzyskać wartość artykułu w postaci ciągu znaków. Jest to często przydatne podczas debugowania aplikacji.

2.2.5.3. Interfejs [IArticlesDao]

Interfejs [IArticlesDao] jest zdefiniowany w następujący sposób:

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
         ' lista wszystkich artykułów
        Function getAllArticles() As IList
         ' dodaje artykuł
        Function ajouteArticle(ByVal unArticle As Article) As Integer
         ' usuwa artykuł
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
         ' modyfikuje artykuł
        Function modifieArticle(ByVal unArticle As Article) As Integer
         ' wyszukuje artykuł
        Function getArticleById(ByVal idArticle As Integer) As Article
         ' usuwa wszystkie artykuły
        Sub clearAllArticles()
         ' zmienia stan magazynowy artykułu
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

Rola poszczególnych metod interfejsu jest następująca:

getAllArticles
zwraca wszystkie artykuły z źródła danych
clearAllArticles
opróżnia źródło danych
getArticleById
zwraca obiekt [Article] zidentyfikowany na podstawie jego numeru
ajouteArticle
umożliwia dodanie pozycji do źródła danych
modifieArticle
umożliwia zmianę pozycji w źródle danych
supprimerArticle
umożliwia usunięcie pozycji ze źródła danych
changerStockArticle
umożliwia zmianę stanu magazynowego pozycji w źródle danych

Interfejs udostępnia programom klienckim pewną liczbę metod zdefiniowanych wyłącznie poprzez ich sygnatury. Nie zajmuje się tym, w jaki sposób metody te zostaną faktycznie zaimplementowane. Zapewnia to elastyczność aplikacji. Program kliencki wywołuje interfejs, a nie konkretną implementację tego interfejsu.

Wybór konkretnej implementacji odbywa się za pomocą pliku konfiguracyjnego Spring.

2.3. Klasa implementacyjna [ArticlesDaoPlainODBC]

Proponujemy nową implementację warstwy [dao], która zakłada, że dane znajdują się w źródle ODBC. Wiadomo, że w systemie Windows niemal wszystkie dostępne na rynku urządzenia SGBD posiadają sterownik ODBC. Zaletą tego rozwiązania jest możliwość zmiany urządzenia SGBD w sposób przezroczysty dla aplikacji. Wadą jest to, że sterownik ODBC, wykorzystujący jedynie cechy wspólne dla wszystkich urządzeń SGBD, jest zazwyczaj mniej wydajny niż sterownik napisany specjalnie w celu wykorzystania pełnego potencjału konkretnego urządzenia SGBD. W punkcie 3.7 można zapoznać się z przykładem tworzenia kodu źródłowego dla modelu ODBC.

2.3.1. Kod

2.3.1.1. Szkielet

Klasa [ArticlesDaoPlainODBC] implementuje interfejs [IArticlesDao] w następujący sposób:

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

Namespace istia.st.articles.dao

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

         ' pola prywatne
        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

         ' producent
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
             ' DSN: nazwa źródła ODBC
             ' uid: identyfikator użytkownika
             ' hasło: jego hasło
...
        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
             ' wykonanie zapytania SELECT 
....
        End Function

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

End Namespace

Komentarze:

  • w wierszu 3 importowana jest przestrzeń nazw zawierająca klasy .NET zapewniające dostęp do źródeł ODBC
  • wiersz 11 – zapisuje połączenie ze źródłem ODBC
  • wiersz 12 – zapisuje nazwę źródła danych DSN
  • wiersze 13–19 – zmienne prywatne typu [OdbcCommand] definiujące zapytania SQL wykorzystywane przez różne metody klasy
  • wiersze 22–27 – konstruktor. Otrzymuje on elementy, które pozwalają mu utworzyć obiekt [OdbcConnection], który połączy kod ze źródłem danych ODBC
  • wiersze 29–31 – metoda dodawania artykułu
  • wiersze 33–35 – metoda zmiany stanu magazynowego artykułu
  • wiersze 37–39 – metoda usuwająca wszystkie artykuły ze źródła danych ODBC
  • wiersze 41–43 – metoda pobierająca listę wszystkich artykułów ze źródła ODBC
  • wiersze 45–47 – metoda umożliwiająca pobranie konkretnego artykułu
  • wiersze 49–51 – metoda umożliwiająca modyfikację niektórych pól pozycji, której numer jest znany
  • wiersze 53–55 – metoda umożliwiająca usunięcie pozycji, której numer jest znany
  • wiersze 57–60 – metoda pomocnicza umożliwiająca wykonanie [SELECT] na źródle danych i zwrócenie wyniku
  • wiersze 62–64 – metoda pomocnicza umożliwiająca wykonanie operacji [INSERT, UPDATE, DELETE] na źródle danych i zwrócenie jej wyniku

2.3.1.2. Konstruktor

         ' konstruktor
        Public Sub New(ByVal DSN As String, ByVal uid As String, ByVal password As String)
             ' DSN: nazwa źródła ODBC
             ' uid: identyfikator użytkownika
             ' password: hasło użytkownika

             'pobieramy nazwę bazy danych przekazaną jako argument
            Me.DSN = DSN
            Dim connectString As String = String.Format("DSN={0};UID={1};PASSWORD={2}", DSN, uid, password)
             ' tworzy się instancję połączenia
            connexion = New OdbcConnection(connectString)
             ' przygotowuje się zapytania 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

Uwagi:

  • wiersz 2 – konstruktor otrzymuje trzy informacje potrzebne do połączenia się ze źródłem ODBC: nazwę źródła DSN, identyfikator, za pomocą którego należy się zalogować, oraz powiązane hasło.
  • wiersz 8 – zapisujemy nazwę źródła DSN, aby móc ją podać w komunikatach o błędach.
  • wiersz 9 – tworzona jest instancja obiektu [OdbcConnection]. Utworzona instancja połączenia nie oznacza jeszcze otwartego połączenia. Otwarcie połączenia odbywa się za pomocą metody [open].
  • wiersze 12–19 – przygotowuje się zapytania SQL w obiektach [OdbcCommand]. Dzięki temu nie trzeba będzie ich tworzyć od nowa za każdym razem, gdy będą potrzebne. Parametry formalne ? zapytań zostaną zastąpione rzeczywistymi wartościami w momencie wykonania zapytania.

2.3.1.3. Metoda executeQuery

        Private Function executeQuery(ByVal query As OdbcCommand) As IList
             ' wykonanie zapytania SELECT 
             ' deklaracja obiektu umożliwiającego dostęp do wszystkich wierszy tabeli wynikowej
            Dim myReader As OdbcDataReader = Nothing
            Try
                 'tworzy się połączenie z BDD
                connexion.Open()
                 '– wykonanie zapytania
                myReader = query.ExecuteReader()
                 'deklaruje się listę pozycji w celu jej późniejszego zwrócenia
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'przygotowuje się pozycję z wartościami z czytnika
                    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)
                     'dodaje się pozycję do listy
                    articles.Add(unArticle)
                End While
                 'zwracamy wynik
                Return articles
            Finally
                 ' zwolnienie zasobów
                If Not myReader Is Nothing And Not myReader.IsClosed Then myReader.Close()
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function

Uwagi:

  • metoda [executeQuery] jest metodą pomocniczą, która:
    • wykonuje zapytanie [SELECT id, nom, prix, stockactuel, stockminimum from ARTICLES ...] na źródle danych
    • zwraca wynik w postaci listy obiektów [Article]
  • wiersz 1 – jedynym parametrem tej metody jest obiekt [OdbcCommand] zawierający zapytanie [Select], które ma zostać wykonane.
  • wiersz 7 – połączenie zostało otwarte. Zostanie ono zamknięte w wierszu 29, niezależnie od tego, czy wystąpił błąd, czy nie.
  • wiersz 9 – tworzona jest instancja obiektu [OdbcDataReader], niezbędnego do przetworzenia wyniku [Select]
  • wiersze 13–23 – każdy wiersz wynikowy z [Select] jest umieszczany w obiekcie [Article], który dołącza do pozostałych pozycji w obiekcie [ArrayList]
  • lista artykułów jest zwracana w wierszu 25
  • nie obsługuje się żadnych wyjątków. Muszą one zostać obsłużone przez kod wywołujący tę metodę.

2.3.1.4. Metoda executeUpdate

        Private Function executeUpdate(ByVal sqlCommand As OdbcCommand) As Integer
             ' wykonanie zapytania o aktualizację
            Try
                 '– nawiązywanie połączenia z BDD
                connexion.Open()
                 '– wykonywanie zapytania
                Return sqlCommand.ExecuteNonQuery()
            Finally
                 ' zwolnienie zasobów
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

Uwagi:

  • metoda otrzymuje obiekt [OdbcCommand], który zawiera żądanie SQL typu [Insert, Update, Delete].
  • Połączenie jest otwierane w wierszu 5. Zostanie zamknięte w wierszu 10, niezależnie od tego, czy wystąpił wyjątek, czy nie.
  • Zapytanie aktualizujące jest wykonywane w wierszu 7. Wynik, czyli liczba wierszy w tabeli ARTICLES zmodyfikowanych przez zapytanie, jest zwracany natychmiast.

2.3.1.5. Metoda ajouteArticle

        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie o wstawienie
                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
                     '– wykonywanie zapytania
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'błąd zapytania
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function

Komentarze:

  • wiersz 1 – metoda otrzymuje pozycję, która ma zostać dodana do źródła danych ODBC. Zwraca liczbę wierszy, na które ma wpływ ta operacja, c.a.d. 1 lub 0
  • wiersze 3 i 20 – metoda jest zsynchronizowana. Dotyczy to wszystkich metod dostępu do danych. Oznacza to, że w danym momencie tylko jeden wątek może pracować nad źródłem danych. Jest to prawdopodobnie zbyt konserwatywne podejście. Istnieją lepsze alternatywy, w szczególności włączenie tych operacji do transakcji. W tym przypadku to obiekt SGBD zarządza dostępem współbieżnym. Nie chcieliśmy na razie wprowadzać pojęcia transakcji. Spring oferuje nam możliwość wprowadzenia ich w warstwie [domain]. Być może będziemy mieli okazję powrócić do tego tematu w innym artykule.
  • W wierszach 5–12 przypisujemy wartości do parametrów formalnych zapytania obiektu [insertCommand] zainicjowanego przez konstruktor. Przypomnijmy to zapytanie:
insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)

Pięć wartości niezbędnych do wykonania zapytania podano w wierszach 7–11.

  • W wierszach 13–19 żądanie jest wykonywane. Jeśli przebiegnie pomyślnie, zwracany jest wynik. W przeciwnym razie generowany jest wyjątek generyczny z wyraźnym komunikatem o błędzie

2.3.1.6. Metoda modifieArticle

        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie o aktualizację
                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
                 ' wykonywanie zapytania
                Try
                     '– wykonywanie zapytania o wstawienie
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'błąd zapytania
                    Throw New Exception("Erreur lors de la modification de l'article [" + unArticle.ToString + "]", ex)
                End Try
            End SyncLock
        End Function

Komentarze:

  • wiersz 1 – metoda otrzymuje artykuł do modyfikacji ze źródła danych ODBC. Zwraca liczbę wierszy, których dotyczy ta operacja, c.a.d. 1 lub 0
  • komentarze do metody [ajouteArticle] można umieścić tutaj

2.3.1.7. Metoda supprimeArticle

        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowujemy zapytanie typu DELETE
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                 '– wykonywany
                Try
                     'wykonywane jest zapytanie o usunięcie
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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

Komentarze:

  • wiersz 1 – metoda otrzymuje numer artykułu do usunięcia z źródła danych ODBC. Zwraca liczbę wierszy, których dotyczy ta operacja, c.a.d. 1 lub 0
  • komentarze do metody [ajouteArticle] można tutaj powtórzyć

2.3.1.8. Metoda getAllArticles

            Public Function getAllArticles() As System.Collections.IList Implements IArticlesDao.getAllArticles
             ' sekcja wyłączna
            SyncLock Me
                Try
                     '– wykonywanie zapytania SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'zwracana jest lista
                    Return articles
                Catch ex As Exception
                     'błąd zapytania
                    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

Komentarze:

  • wiersz 1 – metoda nie przyjmuje żadnych parametrów. Zwraca listę wszystkich artykułów ze źródła danych ODBC
  • zapytanie [Select], pobierające wszystkie artykuły, jest przekazywane do metody [executeQuery] – wiersz 6
  • uzyskana lista jest zwracana w wierszu 8
  • w wierszach 9–12 obsługiwany jest ewentualny wyjątek

2.3.1.9. Metoda getArticleById

        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("id", idArticle))
                End With
                 '– wykonuje się ją
                Try
                     '– wykonywanie zapytania
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'sprawdzanie, czy znaleziono artykuł
                    If articles.Count = 0 Then Return Nothing
                     'zwracamy artykuł
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'błąd zapytania
                    Throw New Exception(String.Format("Erreur lors de la recherche de l'article [{0} : {1}", idArticle, ex.Message))
                End Try
            End SyncLock
        End Function

Uwagi:

  • wiersz 1 – metoda otrzymuje jako parametr numer poszukiwanego artykułu. Zwraca ten numer, jeśli zostanie on znaleziony w źródle ODBC, w przeciwnym razie zwraca numer referencyjny [nothing].
  • Zapytanie [Select] dotyczące artykułu jest inicjowane w wierszach 5–8
  • jest ona wykonywana w wierszu 12 – otrzymujemy listę artykułów
  • jeśli lista ta jest pusta, zwracany jest numer referencyjny [nothing] w wierszu 14
  • w przeciwnym razie zwracany jest jedyny artykuł z listy w wierszu 16
  • w wierszach 17–20 obsługiwany jest ewentualny wyjątek

2.3.1.10. Metoda clearAllArticles

        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' sekcja wyłączna
            SyncLock Me
                Try
                     '– wykonywane jest zapytanie o usunięcie
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'błąd zapytania
                    Throw New Exception(String.Format("Erreur lors de la suppression des articles : {0}", ex.Message))
                End Try
            End SyncLock
        End Sub

Komentarze:

  • wiersz 1 – metoda nie przyjmuje żadnych parametrów i nie zwraca żadnej wartości
  • wiersz 6 – wykonywane jest zapytanie o usunięcie wszystkich artykułów
  • wiersze 7–10: obsługa ewentualnego wyjątku

2.3.1.11. Metoda changerStockArticle

        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowywanie zapytania o aktualizację stanu magazynowego
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OdbcParameter("mvt1", mouvement))
                    .Add(New OdbcParameter("id", idArticle))
                    .Add(New OdbcParameter("mvt2", mouvement))
                End With
                 '– żądanie jest wykonywane
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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

Komentarze:

  • wiersz 1 – metoda otrzymuje jako parametry numer artykułu, którego stan magazynowy ma zostać zmieniony, oraz wartość przyrostu (dodatnią lub ujemną). Zwraca liczbę wierszy zmodyfikowanych przez operację c.a.d. 0 lub 1.
  • wiersze 5–10: inicjowane jest zapytanie [updateStockCommand]. Przypomnijmy treść zapytania SQL:
            updateStockCommand = New OdbcCommand("update ARTICLES set stockactuel=stockactuel+? where id=? and (stockactuel+?)>=0", connexion)

należy zauważyć, że stan magazynowy jest modyfikowany tylko wtedy, gdy po zmianie pozostaje >=0.

  • Zapytanie dotyczące aktualizacji stanu magazynowego artykułu jest wykonywane w wierszu 13, a wynik jest zwracany
  • w wierszach 14–18, gdzie obsługiwany jest ewentualny wyjątek

2.3.2. Generowanie zestawu plików warstwy [dao]

Projekt Visual Studio tej nowej wersji warstwy [dao] ma następującą strukturę:

Image

Projekt jest skonfigurowany do generowania pliku DLL o nazwie [webarticles-dao.dll]:

2.3.3. Testy NUnit dla warstwy [dao]

2.3.3.1. Tworzenie źródła danych ODBC-Firebird

Aby przetestować naszą nową warstwę [dao], potrzebujemy źródła danych ODBC, a zatem bazy danych. Korzystamy z Firebirda SGBD (punkt 3.5). Za pomocą IBExpert (punkt 3.6) tworzymy następującą bazę artykułów:

Administratorem tej bazy będzie użytkownik [SYSDBA] z hasłem [masterkey]. Tworzymy kilka artykułów:

Image

Teraz tworzymy następujące źródło ODBC w Firebirdzie (patrz punkt 3.7):

Utworzony plik źródłowy ODBC ma następujące cechy:

  • nazwa DSN: odbc-firebird-articles
  • identyfikator logowania: SYSDBA
  • powiązane hasło: masterkey

2.3.3.2. Klasa testowa NUnit

Napisaliśmy już klasę testową dla pierwotnie utworzonej warstwy [dao]. Jak czytelnik zapewne pamięta, klasa ta nie testowała konkretnej klasy, lecz interfejs [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
         ' obiekt do testowania
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' pobieramy instancję generatora obiektów Spring
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
             ' żądamy instancjonowania obiektu articlesdao
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

....

Widać, że w metodzie atrybutu <Setup()> zwracamy się do Springa o odwołanie do singletonu o nazwie [articlesdao] typu [IArticlesDao], a więc typu interfejsu. Singleton [articlesdao] został zdefiniowany w następującym pliku konfiguracyjnym [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>

Pokażmy, że początkowa klasa testowa pozwala nam przetestować naszą nową warstwę [dao] bez wprowadzania zmian ani ponownej kompilacji.

  • Utwórzmy w folderze Visual Studio naszej nowej warstwy [dao] folder [tests] (po prawej poniżej), kopiując folder [bin] z projektu testowego pierwotnej warstwy [dao] (po lewej poniżej). W razie potrzeby zachęcamy czytelnika do zapoznania się z projektem testowym pierwszej wersji warstwy [dao] w pierwszej części artykułu.
  • w folderze [tests] zastępujemy plik DLL oraz [webarticles-dao.dll] pochodzące zstarej warstwie [dao] na DLL i [webarticles-dao.dll] pochodzące z nowej warstwy [dao]
  • zmodyfikujmy plik konfiguracyjny [spring-config.xml] w celu instancjonowania nowej klasy [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>

Uwagi:

  • w wierszu 6 obiekt [articlesdao] jest teraz powiązany z instancją klasy [ ArticlesDaoPlainODBC]
  • klasa ta posiada konstruktor z trzema argumentami:
  • nazwa źródła DSN – wiersz 8
  • identyfikator, za pomocą którego będzie realizowany dostęp do bazy danych – wiersz 11
  • hasło powiązane z tym identyfikatorem – wiersz 14

Przenosimy tutaj informacje ze źródła ODBC-Firebird, które utworzyliśmy wcześniej.

2.3.3.3. Testy

Jesteśmy teraz gotowi do przeprowadzenia testów. Za pomocą aplikacji [Nunit-Gui] ładujemy plik DLL z folderu [test-webarticles-dao.dll], o którym mowa powyżej, i uruchamiamy test [testGetAllArticles]:

Image

Patrząc na powyższy zrzut ekranu, można ubolewać nad nazwą [NUnitTestArticlesDaoArrayList] nadaną początkowo klasie testowej. Jest to mylące. To właśnie klasa [ArticlesDaoPlainODBC] jest tutaj testowana. Zrzut ekranu pokazuje, że poprawnie pobraliśmy artykuły, które umieściliśmy w tabeli [ARTICLES]. Teraz przeprowadźmy wszystkie testy:

Image

W lewym oknie widoczna jest lista testowanych metod. Kolor kropki przed nazwą każdej metody wskazuje, czy metoda zakończyła się powodzeniem (zielony) czy niepowodzeniem (czerwony). Czytelnik przeglądający ten dokument na ekranie może zauważyć, że wszystkie testy zakończyły się powodzeniem.

2.3.3.4. Wniosek

Właśnie wykazaliśmy, że:

  • ponieważ klasa testowa NUnit odwoływała się nie do klasy, ale do interfejsu;
  • ponieważ dokładna nazwa klasy instancjonującej interfejs została podana w pliku konfiguracyjnym, a nie w kodzie;
  • ponieważ Spring zajmował się instancjonowaniem klasy i przekazywaniem jej odwołania do kodu testowego;

W związku z tym kod testowy napisany dla początkowej warstwy [dao] pozostał aktualny dla nowej implementacji tej samej warstwy. Nie musieliśmy mieć dostępu do kodu klasy testowej. Korzystaliśmy wyłącznie z jej skompilowanej wersji, wygenerowanej podczas testowania początkowej warstwy [dao]. Podobne wnioski wyciągniemy, gdy zajdzie potrzeba zintegrowania nowej warstwy [dao] z aplikacją [webarticles].

2.3.4. Integracja nowej warstwy [dao] z aplikacją [webarticles]

2.3.4.1. Testy integracyjne

Przypomnijmy, że początkowa wersja aplikacji [webarticles] została wdrożona w następującym folderze [runtime]:

 

Zaleca się zapoznanie się z punktem 2.2.4, który szczegółowo opisuje procedurę wdrożenia aplikacji [webarticles]. Wprowadzamy następujące zmiany w zawartości folderu [runtime]:

  • w folderze [bin] plik DLL z poprzedniej warstwy [dao] zostaje zastąpiony plikiem DLL z nowej warstwy [dao]
  • w [runtime] plik konfiguracyjny [web.config] zostaje zastąpiony plikiem uwzględniającym nową klasę implementacji warstwy [dao]:

Nowy plik konfiguracyjny [web.config] ma następującą treść:

<?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>

Uwagi:

  • wiersze 14–24 przypisują do singletonu [articlesDao] instancję nowej klasy [ArticlesDaoPlainODBC]. Jest to jedyna zmiana. Spotkaliśmy się z nią już podczas testów nowej warstwy [dao].

Jesteśmy gotowi do testów. Konfigurujemy serwer WWW [Cassini] w taki sam sposób, jak w punkcie 2.2.4. Inicjujemy tabelę artykułów [Firebird] następującymi wartościami:

Image

Upewnij się, że serwer WWW Cassini oraz serwery SGBD i [Firebird] są uruchomione. W przeglądarce wpisujemy adres URL [http://localhost/webarticles/main.aspx]:

Image

Teraz sprawdźmy zawartość tabeli [ARTICLES] w bazie danych [Firebird]:

Image

Artykuły [parapluie] i [bottes] zostały zakupione, a ich stany magazynowe zmniejszono o zakupioną ilość. Artykułu [chapeau] nie udało się zakupić, ponieważ zamówiona ilość przekraczała stan magazynowy. Zachęcamy czytelnika do przeprowadzenia dodatkowych testów.

2.3.4.2. Wnioski

Co zrobiliśmy?

  • przywróciliśmy wersję wdrożeniową poprzedniej wersji;
  • zastąpiliśmy pozycję DLL z warstwy [dao] nową wersją. Pliki DLL warstw [web] i [domain] pozostały bez zmian;
  • zmodyfikowaliśmy plik konfiguracyjny [web.config], aby uwzględniał nową klasę implementacyjną warstwy [dao]

Całość jest przejrzysta i zapewnia dużą elastyczność rozbudowy aplikacji internetowej. Te istotne cechy wynikają z dwóch wyborów architektonicznych:

  • dostęp do warstw poprzez interfejsy
  • integracja i konfiguracja warstw za pomocą Springa.

Obecnie proponujemy nową implementację warstwy [dao].

2.4. Klasa implementacyjna [ArticlesDaoSqlServer]

Druga implementacja warstwy [dao] zakłada, że dane znajdują się w bazie SQL Server. Firma Microsoft udostępnia serwer o nazwie MSDE, który jest ograniczoną wersją serwera SQL. Informacje na temat jego pobrania i instalacji znajdują się w załączniku, w punkcie 3.12.

2.4.1. Kod

Klasa [ArticlesDaoSqlServer] jest bardzo zbliżona do omówionej wcześniej klasy [ArticlesDaoPlainODBC]. W związku z tym wskażemy jedynie zmiany wprowadzone w stosunku do poprzedniej wersji:

  • niezbędne klasy znajdują się w przestrzeni nazw [System.Data.SqlClient] zamiast w przestrzeni nazw [System.Data.Odbc]
  • połączenie typu [OdbcConnection] ma teraz typ [SqlConnection]
  • obiekty typu [OdbcCommand] mają teraz typ [SqlCommand]
  • zmienia się składnia zapytań SQL z parametrami. Zapytanie o wstawienie przyjmuje zatem następującą postać:
insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)

podczas gdy wcześniej brzmiała:

insertCommand = New OdbcCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (?,?,?,?,?)", connexion)
  • metoda [ajouteArticle] przyjmuje zatem następującą postać:
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowujemy zapytanie o wstawienie
                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
                     '– wykonuje się ją
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'błąd zapytania
                    Throw New Exception(String.Format("Erreur à l'ajout de l'article [{0}] : {1}", unArticle.ToString, ex.Message))
                End Try
            End SyncLock
        End Function
  • zmieniono również nazwę producenta:
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' serwer: nazwa instancji SQL, z którą należy się połączyć
             ' databaseName: nazwa bazy danych, z którą należy się połączyć
             ' uid: identyfikator użytkownika
             ' password: hasło użytkownika

             'pobieramy nazwę bazy danych przekazaną jako argument
            Me.databaseName = databaseName
             ': nawiązywanie połączenia
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
             ' przygotowuje się zapytania SQL
            insertCommand = New SqlCommand("insert into ARTICLES(id, nom, prix, stockactuel, stockminimum) values (@id,@nom,@prix,@sa,@sm)", connexion)
...
        End Sub

Konstruktor przyjmuje teraz cztery parametry:

             ' serwer: nazwa instancji SQL, z którą należy nawiązać połączenie
             ' databaseName: nazwa bazy danych, z którą należy się połączyć
             ' uid: identyfikator użytkownika
             ' hasło: hasło użytkownika

Pełny kod klasy [ArticlesDaoSqlServer] wygląda następująco:

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

Namespace istia.st.articles.dao

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

         ' pola prywatne
        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

         ' twórca
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' serwer: nazwa instancji serwera SQL, z którą należy się połączyć
             ' databaseName: nazwa bazy danych docelowej
             ' uid: identyfikator użytkownika
             ' password: hasło użytkownika

             'pobieramy nazwę bazy danych przekazaną jako argument
            Me.databaseName = databaseName
             ': tworzy się instancję połączenia
            Dim connectString As String = String.Format("Data Source={0};Initial Catalog={1};UID={2};PASSWORD={3}", serveur, databaseName, uid, password)
            connexion = New SqlConnection(connectString)
             ' przygotowuje się zapytania 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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowujemy zapytanie o wstawienie
                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
                     ' wykonuje się 
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie o aktualizację stanu magazynowego
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@mvt", mouvement))
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 ' wykonywanie zapytania
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                Try
                     '– wykonywanie zapytania o wstawienie
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                Try
                     'wykonywane jest zapytanie SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'zwracana jest lista
                    Return articles
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 '– wykonuje się ją
                Try
                     ' wykonywanie zapytania
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'sprawdzanie, czy znaleziono artykuł
                    If articles.Count = 0 Then Return Nothing
                     'zwracamy artykuł
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie aktualizacyjne
                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
                 ' wykonuje się ją
                Try
                     'wykonywanie zapytania o wstawienie
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowujemy zapytanie typu DELETE
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New SqlParameter("@id", idArticle))
                End With
                 ' wykonywanie
                Try
                     'wykonywane jest zapytanie o usunięcie
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' wykonanie zapytania SELECT 
             ' deklaracja obiektu umożliwiającego dostęp do wszystkich wierszy tabeli wynikowej
            Dim myReader As SqlDataReader = Nothing
            Try
                 'tworzone jest połączenie z BDD
                connexion.Open()
                 ' wykonywanie zapytania
                myReader = query.ExecuteReader()
                 'deklaruje się listę pozycji w celu jej późniejszego zwrócenia
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'przygotowuje się pozycję z wartościami z czytnika
                    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)
                     'dodaje się pozycję do listy
                    articles.Add(unArticle)
                End While
                 'zwracamy wynik
                Return articles
            Finally
                 ' zwolnienie zasobów
                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
             ' wykonanie zapytania o aktualizację
            Try
                 '– nawiązywanie połączenia z BDD
                connexion.Open()
                 ' wykonywanie zapytania
                Return updateCommand.ExecuteNonQuery()
            Finally
                 ' zwolnienie zasobów
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Zachęcamy czytelnika do zapoznania się z tym kodem w kontekście wcześniejszych uwag dotyczących klasy [ArticlesDaoPlainODBC].

2.4.2. Generowanie zestawu warstwy [dao]

Nowy projekt w Visual Studio ma następującą strukturę:

Image

Projekt jest skonfigurowany do generowania pliku DLL o nazwie [webarticles-dao.dll]:

2.4.3. Testy NUnit warstwy [dao]

2.4.3.1. Tworzenie serwera danych źródłowych o nazwie SQL

Aby przetestować naszą nową warstwę [dao], potrzebujemy źródła danych SQL Server, a tym samym SGBD SQL Server. W rzeczywistości wykorzystamy silnik danych SGBD MSDE (MicroSoft Data Engine) (punkt 3.12), który jest wersją serwera SQL, ograniczoną jedynie liczbą obsługiwanych jednoczesnych użytkowników. Za pomocą [EMS MS SQL Manager] (punkt 3.14) tworzymy następującą bazę artykułów w instancji MSDE o nazwie [portable1_tahe\msde140405]:

Image

Baza należy do użytkownika [mdparticles] o haśle [admarticles]. Polecenie Transact-SQL służące do utworzenia tabeli [ARTICLES] jest następujące:

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

Tworzymy kilka artykułów:

Image

2.4.3.2. Klasa testowa NUnit

Klasa testowa NUnit dla klasy implementacyjnej [ArticlesDaoSqlServer] jest taka sama jak dla klasy [ArticlesDaoPlainODBC] (patrz punkt 2.3.3.2). Postępujemy analogicznie, aby przygotować test NUnit dla tej klasy:

  • w folderze Visual Studio projektu [dao-sqlserver] tworzymy folder [tests] (po prawej), kopiując folder [tests] z projektu [dao-odbc] (po lewej):
  • w folderze [tests] projektu [dao-sqlserver], zastępujemy pliki DLL i [webarticles-dao.dll] plikami DLL i [webarticles-dao.dll] wygenerowanymi w ramach projektu [dao-sqlserver]
  • modyfikujemy plik konfiguracyjny [spring-config.xml] w celu utworzenia instancji nowej klasy [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>

Uwagi:

  • w wierszu 7 obiekt [articlesdao] jest teraz powiązany z instancją klasy [ ArticlesDaoSqlServeur]
  • klasa ta posiada konstruktor z czterema argumentami:
  • nazwa używanej instancji MSDE – wiersz 9
  • nazwa bazy danych – wiersz 12
  • identyfikator, za pomocą którego zostanie nawiązany dostęp do bazy danych – wiersz 15
  • hasło powiązane z tym identyfikatorem – wiersz 18

Pobieramy tutaj informacje ze źródła MSDE, które utworzyliśmy wcześniej.

2.4.3.3. Testy

Jesteśmy gotowi do przeprowadzenia testów. Za pomocą aplikacji [Nunit-Gui] ładujemy plik DLL [test-webarticles-dao.dll] z powyższego folderu [tests] i uruchamiamy test [testGetAllArticles]:

Image

Pomimo nazwy [NUnitTestArticlesDaoArrayList] nadanej początkowo klasie testowej, która została zachowana, ponieważ korzystamy z DLL i [tests-webarticles-dao.dll] pochodzących z tej klasy, to właśnie klasa [ArticlesDaoSqlserver] jest tutaj testowana. Zrzut ekranu pokazuje, że poprawnie odzyskaliśmy artykuły, które umieściliśmy w tabeli [ARTICLES]. Teraz przeprowadźmy wszystkie testy:

Image

W lewym oknie widoczna jest lista testowanych metod. Kolor kropki przed nazwą każdej metody wskazuje, czy metoda zakończyła się powodzeniem (zielony) czy niepowodzeniem (czerwony). Czytelnik przeglądający ten dokument na ekranie może zauważyć, że wszystkie testy zakończyły się powodzeniem.

2.4.4. Włączenie nowej warstwy [dao] do aplikacji [webarticles]

Postępujemy zgodnie z procedurą opisaną w punkcie 2.3.4. Wprowadzamy następujące zmiany w zawartości folderu [runtime]:

  • w folderze [bin] plik DLL zstarej warstwy [dao] zostaje zastąpiony plikiem DLL z nowej warstwy [dao] zaimplementowanej przez klasę [ArticlesDaoSqlServer]
  • w pliku [runtime] plik konfiguracyjny [web.config] zostaje zastąpiony plikiem uwzględniającym nową klasę implementacyjną:
<?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>

Uwagi:

  • w wierszach 15–33 do singletonu [articlesDao] przypisano instancję nowej klasy [ArticlesDaoSqlServer]. Jest to jedyna zmiana. Spotkaliśmy się z nią już podczas testów nowej warstwy [dao]

Jesteśmy gotowi do testów. Zachowujemy tę samą konfigurację serwera WWW [Cassini], co poprzednio. Inicjujemy tabelę artykułów [MSDE] następującymi wartościami:

Image

Upewnij się, że serwer WWW Cassini oraz SGBD MSDE (w tym przypadku instancja portable1_tahe\msde140405) są uruchomione. W przeglądarce wywołujemy serwer URL [http://localhost/webarticles/main.aspx]:

Image

Teraz sprawdźmy zawartość tabeli [ARTICLES] w bazie danych [MSDE]:

Image

Artykuły [ballon foot] i [raquette tennis] zostały zakupione, a ich stany magazynowe zmniejszono o zakupioną ilość. Artykułu [rollers] nie udało się zakupić, ponieważ zamówiona ilość przekraczała stan magazynowy. Zachęcamy czytelnika do przeprowadzenia dodatkowych testów.

2.4.5. Klasa implementacyjna [ArticlesDaoOleDb]

2.4.5.1. y źródłowe danych OleDb

Trzecia implementacja warstwy [dao] zakłada, że dane znajdują się w bazie dostępnej za pośrednictwem sterownika OleDb. Zasada działania źródeł OleDb jest analogiczna do zasady działania źródeł ODBC. Program korzystający ze źródła OleDb robi to za pośrednictwem standardowego interfejsu wspólnego dla wszystkich źródeł OleDb. Zmiana źródła OleDb sprowadza się do zmiany sterownika OleDb. Sam kod nie ulega zmianie.

Za pomocą programu Visual Studio można sprawdzić, jakie sterowniki OleDb są dostępne na danym komputerze:

  • wyświetlić eksplorator serwerów za pomocą [Affichage/Explorateur de serveurs]:

Image

  • aby dodać nowe połączenie, kliknij prawym przyciskiem myszy na [Connexion de données] i wybierz opcję [Ajouter une connexion]. Pojawi się wówczas kreator, za pomocą którego można zdefiniować parametry połączenia:

Image

  • Panel [Fournisseur] zawiera listę dostępnych sterowników OLEDB. Do nowej warstwy [dao] wykorzystamy sterownik [Microsoft Jet 4.0 OLE DB Provider], który zapewnia dostęp do baz danych ACCESS.
  • Wyjdźmy na chwilę z programu Visual Studio, aby utworzyć bazę danych o nazwie ACCESS [articles.mdb], zawierającą następującą pojedynczą tabelę:

Image

  • Struktura tabeli jest następująca:
id
liczbowy – całkowity – klucz główny
nom
tekst – 20 znaków –
prix
liczbowy – liczba zmiennoprzecinkowa typu double
stockactuel
liczbowa – liczba całkowita
stockminimum
liczbowy – całkowity
  • Wróćmy do programu Visual Studio i utwórzmy nowe połączenie zgodnie z wcześniejszymi instrukcjami:

Image

  • Wybieramy sterownik [Microsoft Jet 4.0] i przechodzimy do panelu [Connexion]:

Image

  • za pomocą przycisku [1] wybieramy właśnie utworzoną bazę ACCESS, a następnie kończymy definiowanie połączenia, klikając przycisk [Terminer]. Utworzone połączenie pojawia się teraz na liście dostępnych połączeń:

Image

  • podwójne kliknięcie na tabelę [ARTICLES] umożliwia dostęp do jej zawartości:

Image

  • można wówczas dodawać, modyfikować i usuwać wiersze w tabeli.
  • wybierz w przeglądarce serwerów nowe połączenie, aby uzyskać dostęp do jego okna właściwości:

Image

  • Warto zapamiętać ciąg połączenia. Będzie on nam potrzebny do połączenia się z bazą danych:
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
  • Z tego ciągu zachowamy tylko następujące elementy:
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\data\serge\databases\access\articles\articles.mdb;

2.4.5.2. Kod klasy [ArticlesDaoOleDb]

Klasa [ArticlesDaoOleDb] jest bardzo zbliżona do omówionej wcześniej klasy [ArticlesDaoPlainODBC]. W związku z tym wskażemy jedynie zmiany wprowadzone w stosunku do poprzedniej wersji:

  • niezbędne klasy znajdują się w przestrzeni nazw [System.Data.OleDb] zamiast w przestrzeni nazw [System.Data.Odbc]
  • połączenie typu [OdbcConnection] ma teraz typ [OleDbConnection]
  • obiekty typu [OdbcCommand] mają teraz typ [OleDbCommand]

Konstruktor tej klasy przyjmuje jako jedyny parametr ciąg znaków określający połączenie z bazą danych:

         ' konstruktor
        Public Sub New(ByVal connectString As String)
             ' connectString: ciąg połączenia ze źródłem OleDb
             'tworzy się instancję połączenia
            connexion = New OleDbConnection(connectString)
             ' przygotowuje się zapytania SQL
...
        End Sub

Pełny kod klasy [ArticlesDaoOleDb] jest następujący:

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

Namespace istia.st.articles.dao

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

         ' pola prywatne
        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

         ' konstruktor
        Public Sub New(ByVal connectString As String)
             ' connectString: ciąg połączenia ze źródłem OleDb
             ' instancja połączenia
            connexion = New OleDbConnection(connectString)
             ' przygotowuje się zapytania 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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie o wstawienie
                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
                     ' żądanie jest wykonywane
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie o aktualizację stanu magazynowego
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("mvt1", mouvement))
                    .Add(New OleDbParameter("id", idArticle))
                    .Add(New OleDbParameter("mvt2", mouvement))
                End With
                 ' wykonywanie zapytania
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                Try
                     '– wykonywanie zapytania o wstawienie
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                Try
                     'wykonywane jest zapytanie SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'zwracana jest lista
                    Return articles
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                 '– wykonuje się ją
                Try
                     ' wykonywanie zapytania
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'sprawdzanie, czy znaleziono artykuł
                    If articles.Count = 0 Then Return Nothing
                     'zwracamy artykuł
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie aktualizacyjne
                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
                 ' wykonuje się ją
                Try
                     'wykonywanie zapytania o wstawienie
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowujemy zapytanie typu DELETE
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New OleDbParameter("id", idArticle))
                End With
                 ' wykonywany
                Try
                     'wykonywane jest zapytanie o usunięcie
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' wykonanie zapytania SELECT 
             ' zadeklarowanie obiektu umożliwiającego dostęp do wszystkich wierszy tabeli wynikowej
            Dim myReader As OleDbDataReader = Nothing
            Try
                 '– nawiązywanie połączenia z BDD
                connexion.Open()
                 ' wykonanie zapytania
                myReader = query.ExecuteReader()
                 'deklaruje się listę pozycji w celu jej późniejszego zwrócenia
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'przygotowuje się artykuł z wartościami z czytnika
                    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)
                     'dodaje się pozycję do listy
                    articles.Add(unArticle)
                End While
                 'zwracamy wynik
                Return articles
            Finally
                 ' zwolnienie zasobów
                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
             ' wykonanie zapytania o aktualizację
            Try
                 '– nawiązywanie połączenia z BDD
                connexion.Open()
                 ' wykonywanie zapytania
                Return sqlCommand.ExecuteNonQuery()
            Finally
                 ' zwolnienie zasobów
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Zachęcamy czytelnika do zapoznania się z tym kodem w kontekście wcześniejszych uwag dotyczących klasy [ArticlesDaoPlainODBC].

2.4.5.3. Generowanie zestawu warstwy [dao]

Nowy projekt w Visual Studio ma następującą strukturę:

Image

Projekt jest skonfigurowany do generowania pliku DLL o nazwie [webarticles-dao.dll]:

2.4.5.4. Testy NUnit dla warstwy [dao]

2.4.5.4.1. Klasa testowa NUnit

Klasa testowa NUnit dla klasy implementacyjnej [ArticlesDaoOleDb] jest taka sama jak dla klasy [ArticlesDaoPlainODBC] (patrz punkt 2.3.3.2). Postępujemy analogicznie, aby przygotować test NUnit dla tej klasy:

  • w folderze Visual Studio projektu [dao-oledb] tworzymy folder [tests] (po prawej), kopiując folder [tests] z projektu [dao-odbc] (po lewej):
  • w folderze [tests] projektu [dao-oledb] zastępujemy pliki DLL i [webarticles-dao.dll] plikami DLL i [webarticles-dao.dll], wygenerowaną w ramach projektu [dao-oledb]
  • modyfikujemy plik konfiguracyjny [spring-config.xml] w celu utworzenia instancji nowej klasy [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>

Uwagi:

  • w wierszu 7 obiekt [articlesdao] jest teraz powiązany z instancją klasy [ ArticlesDaoOleDb]
  • klasa ta posiada konstruktor z jednym argumentem: ciągiem znaków połączenia z bazą danych OleDb ACCESS – wiersz 9
2.4.5.4.2. Tests

Jesteśmy gotowi do testów. Za pomocą aplikacji [Nunit-Gui] ładujemy plik DLL [test-webarticles-dao.dll] z powyższego folderu [tests] i uruchamiamy test [testGetAllArticles]:

Image

Pomimo nazwy [NUnitTestArticlesDaoArrayList] nadanej początkowo klasie testowej, to właśnie klasa [ArticlesDaoOleDb] jest tutaj testowana. Zrzut ekranu pokazuje, że poprawnie pobraliśmy pozycje, które umieściliśmy w tabeli [ARTICLES]. Teraz przeprowadźmy wszystkie testy:

Image

Czytelnik przeglądający ten dokument na ekranie będzie mógł zobaczyć, że wszystkie testy zakończyły się powodzeniem (kolor zielony).

2.4.5.5. Włączenie nowej warstwy [dao] do aplikacji [webarticles]

Postępujemy zgodnie z procedurą opisaną w punkcie 2.3.4. Wprowadzamy następujące zmiany w zawartości folderu [runtime]:

  • w folderze [bin] plik DLL zstarej warstwy [dao] zostaje zastąpiony plikiem DLL z nowej warstwy [dao] zaimplementowanej przez klasę [ArticlesDaoOleDb]
  • w pliku [runtime] plik konfiguracyjny [web.config] zostaje zastąpiony plikiem uwzględniającym nową klasę implementacyjną:
<?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>

Uwagi:

  • w wierszach 14–18 do singletonu [articlesDao] przypisano instancję nowej klasy [ArticlesDaoOleDb]. Jest to jedyna zmiana.

Zachowujemy tę samą konfigurację serwera WWW [Cassini], co poprzednio. Inicjujemy tabelę artykułów następującymi wartościami:

Image

Upewnij się, że baza artykułów nie jest używana przez program taki jak Visual Studio lub ACCESS. W przeglądarce należy wywołać URL [http://localhost/webarticles/main.aspx]:

Image

Teraz sprawdźmy zawartość tabeli [ARTICLES] wraz z ACCESS:

Image

Artykuły [pantalon] i [jupe] zostały zakupione, a ich stany magazynowe zmniejszono o zakupioną ilość. Artykułu [manteau] nie udało się zakupić, ponieważ zamówiona ilość przekraczała stan magazynowy. Zachęcamy czytelnika do przeprowadzenia dodatkowych testów.

2.5. Klasa implementacyjna [ArticlesDaoFirebirdProvider]

2.5.1. Dostawca dostępu Firebird-net-provider

Wcześniej korzystaliśmy już ze źródła danych [Firebird], z którego korzystaliśmy za pośrednictwem sterownika ODBC. Chociaż sterowniki ODBC zapewniają dużą możliwość ponownego wykorzystania kodu, który z nich korzysta, są one jednak mniej wydajne niż sterowniki napisane specjalnie dla docelowego SGBD. SGBD [Firebird] można wykorzystać za pomocą biblioteki specjalnych klas, którą można pobrać ze strony Firebird [http://firebird.sourceforge.net/]. Strona pobierania zawiera następujące linki (kwiecień 2005 r.):

Image

Link [firebird-net-provider] służy do pobrania klas .NET umożliwiających dostęp do Firebirda SGBD. Po zainstalowaniu pakietu powstaje folder o następującej strukturze:

Image

Nasze zainteresowanie budzą dwa elementy:

  • [FirebirdSql.Data.Firebird.dll]: zestaw zawierający klasy .NET zapewniające dostęp do SGBD Firebird
  • [FirebirdNETProviderSDK.chm]: dokumentacja dotycząca tych klas

Następnie, aby projekt Visual Studio mógł korzystać z tych klas, wykonamy dwie czynności:

  • umieścić assembly [FirebirdSql.Data.Firebird.dll] w folderze [bin] projektu
  • dodamy ten sam assembly do odwołań projektu

2.5.2. Kod klasy [ArticlesDaoFirebirdProvider]

Klasa [ArticlesDaoFirebirdProvider] jest bardzo podobna do omówionej wcześniej klasy [ArticlesDaoSqlServer]. W związku z tym wskażemy jedynie zmiany wprowadzone w stosunku do tej wersji:

  • niezbędne klasy znajdują się w przestrzeni nazw [FirebirdSql.Data.Firebird] zamiast w przestrzeni nazw [System.Data.SqlClient]
  • połączenie typu [SqlConnection] ma teraz typ [FbConnection]
  • obiekty typu [SqlCommand] mają teraz typ [FbCommand]
  • obiekty typu [SqlParameter] mają teraz typ [FbParameter]

Konstruktor tej klasy przyjmuje cztery parametry, na podstawie których tworzy ciąg połączenia z bazą danych:

         ' konstruktor
        Public Sub New(ByVal serveur As String, ByVal databaseName As String, ByVal uid As String, ByVal password As String)
             ' serwer: nazwa komputera hosta SGBD
             ' databaseName: ścieżka dostępu do bazy danych
             ' uid: identyfikator logujący się użytkownika
             ' password: jego hasło
...
        End Sub

Pełny kod klasy [ArticlesDaoFirebirdProvider] brzmi następująco:

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

Namespace istia.st.articles.dao

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

         ' pola prywatne
        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

         ' producent
        Public Sub New(ByVal serveur As String, ByVal databasePath As String, ByVal uid As String, ByVal password As String)
             ' serwer: nazwa hosta serwera SGBD Firebird
             ' databaseName: ścieżka dostępu do bazy danych, z której ma być pobierane dane
             ' uid: identyfikator użytkownika logującego się do bazy
             ' password: hasło użytkownika

             'pobieramy nazwę bazy danych przekazaną jako argument
            Me.databasePath = databasePath
             ' nawiązywanie połączenia
            Dim connectString As String = String.Format("DataSource={0};Database={1};User={2};Password={3}", serveur, databasePath, uid, password)
            connexion = New FbConnection(connectString)
             ' przygotowuje się zapytania 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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie o wstawienie
                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
                     ' żądanie jest wykonywane
                    Return executeUpdate(insertCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie o aktualizację stanu magazynowego
                With updateStockCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@mvt", mouvement))
                    .Add(New FbParameter("@id", idArticle))
                End With
                 ' wykonywanie zapytania
                Try
                    Return executeUpdate(updateStockCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                Try
                     '– wykonywanie zapytania o wstawienie
                    executeUpdate(deleteAllCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                Try
                     'wykonywane jest zapytanie SELECT
                    Dim articles As IList = executeQuery(selectAllCommand)
                     'zwracana jest lista
                    Return articles
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie SELECT
                With selectSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                 '– zapytanie jest wykonywane
                Try
                     ' wykonywanie zapytania
                    Dim articles As IList = executeQuery(selectSomeCommand)
                     'sprawdzanie, czy znaleziono artykuł
                    If articles.Count = 0 Then Return Nothing
                     'zwracamy artykuł
                    Return CType(articles.Item(0), Article)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowuje się zapytanie aktualizacyjne
                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
                 ' wykonuje się ją
                Try
                     '– wykonywanie zapytania o wstawienie
                    Return executeUpdate(updatecommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' sekcja wyłączna
            SyncLock Me
                 ' przygotowujemy zapytanie typu DELETE
                With deleteSomeCommand.Parameters
                    .Clear()
                    .Add(New FbParameter("@id", idArticle))
                End With
                 ' wykonywano
                Try
                     'wykonywane jest zapytanie o usunięcie
                    Return executeUpdate(deleteSomeCommand)
                Catch ex As Exception
                     'błąd zapytania
                    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
             ' wykonanie zapytania SELECT 
             ' deklaracja obiektu umożliwiającego dostęp do wszystkich wierszy tabeli wynikowej
            Dim myReader As FbDataReader = Nothing
            Try
                 'tworzone jest połączenie z BDD
                connexion.Open()
                 ' wykonanie zapytania
                myReader = query.ExecuteReader()
                 'deklaruje się listę pozycji w celu jej późniejszego zwrócenia
                Dim articles As IList = New ArrayList
                Dim unArticle As Article
                While myReader.Read()
                     'przygotowuje się pozycję z wartościami z czytnika
                    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)
                     'dodaje się pozycję do listy
                    articles.Add(unArticle)
                End While
                 'zwracamy wynik
                Return articles
            Finally
                 ' zwolnienie zasobów
                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
             ' wykonanie zapytania o aktualizację
            Try
                 '– nawiązywanie połączenia z BDD
                connexion.Open()
                 ' wykonywanie zapytania
                Return updateCommand.ExecuteNonQuery()
            Finally
                 ' zwolnienie zasobów
                If Not connexion Is Nothing Then connexion.Close()
            End Try
        End Function
    End Class

End Namespace

Zachęcamy czytelnika do zapoznania się z tym kodem w kontekście wcześniejszych uwag dotyczących klasy [ArticlesDaoSqlServer].

2.5.3. Generowanie zestawu warstwy [dao]

Nowy projekt Visual Studio ma następującą strukturę:

Image

Warto zwrócić uwagę na obecność zestawu [FirebirdSql.Data.Firebird.dll] w odwołaniach projektu. Ten zestaw DLL został umieszczony w folderze [bin] projektu. Projekt jest skonfigurowany tak, aby wygenerować plik DLL o nazwie [webarticles-dao.dll]:

2.5.4. Testy Nunit warstwy [dao]

2.5.4.1. Klasa testowa NUnit

Klasa testowa Nunit dla klasy implementacyjnej [ArticlesDaoFirebirdProvider] jest taka sama jak dla klasy [ArticlesDaoPlainODBC] (patrz punkt 2.3.3.2). Podobną procedurę stosujemy przy przygotowywaniu testu Nunit dla klasy [ArticlesDaoFirebirdProvider]:

  • w folderze Visual Studio projektu [dao-firebird-provider] tworzymy folder [tests] (po prawej), kopiując folder [bin] z projektu testowego warstwy [dao-odbc] (po lewej):
  • w folderze [tests] zastępujemy pliki DLL i [webarticles-dao.dll] plikami DLL i [webarticles-dao.dll] pochodzącymi z generacji projektu [dao-firebird-provider]
  • modyfikujemy plik konfiguracyjny [spring-config.xml] w celu instancjonowania nowej klasy [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>

Uwagi:

  • w wierszu 7 obiekt [articlesdao] jest teraz powiązany z instancją klasy [ArticlesDaoFirebirdProvider]
  • klasa ta posiada konstruktor z czterema argumentami
  • komputer hosta obiektu SGBD – wiersz 9
  • ścieżka dostępu do bazy danych Firebird – wiersz 12
  • nazwa użytkownika logującego się – wiersz 15
  • jego hasło – wiersz 18

2.5.4.2. Testy

Tabela [ARTICLES] w źródle danych jest wypełniona następującymi wpisami (należy użyć tabeli IBExpert):

Image

Jesteśmy gotowi do przeprowadzenia testów. Za pomocą aplikacji [Nunit-Gui] ładujemy pliki DLL i [test-webarticles-dao.dll] z powyższego folderu [tests] i uruchamiamy test [testGetAllArticles]:

Image

Pomimo nazwy [NUnitTestArticlesDaoArrayList] nadanej początkowo klasie testowej, to właśnie klasa [ArticlesDaoFirebirdProvider] jest tutaj testowana. Zrzut ekranu pokazuje, że poprawnie pobraliśmy pozycje, które umieściliśmy w tabeli [ARTICLES]. Teraz przeprowadźmy wszystkie testy:

Image

Czytelnik przeglądający ten dokument na ekranie zobaczy, że wszystkie testy zakończyły się powodzeniem (kolor zielony). Nie widać jednak, że testy przebiegły znacznie szybciej niż w przypadku bazy artykułów, do której dostęp uzyskiwano za pośrednictwem sterownika ODBC z naszej pierwszej implementacji.

2.5.5. Włączenie nowej warstwy [dao] do aplikacji [webarticles]

Postępujemy zgodnie z procedurą wyjaśnioną już dwukrotnie, w szczególności w paragrafie 2.3.4. Wprowadzamy następujące zmiany w zawartości folderu [runtime]:

  • w folderze [bin] plik DLL zstarej warstwy [dao] zostaje zastąpiony przez plik DLL z nowej warstwy [dao] zaimplementowanej przez klasę [ArticlesDaoFirebirdProvider]. Umieszczamy tam również warstwę DLL, niezbędną dla Firebird [FirebirdSql.Data.Firebird.dll]:

Image

  • w pliku [runtime] plik konfiguracyjny [web.config] zostaje zastąpiony plikiem uwzględniającym nową klasę implementacyjną:
<?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>

Uwagi:

  • w wierszach 14–27 do singletonu [articlesDao] przypisano instancję nowej klasy [ArticlesDaoFirebirdProvider]. To jedyna zmiana.

Jesteśmy gotowi do przeprowadzenia testów. Konfigurujemy serwer WWW [Cassini] tak samo jak w poprzednich testach. Inicjujemy tabelę artykułów następującymi wartościami:

Image

Za pomocą przeglądarki wywołujemy stronę URL [http://localhost/webarticles/main.aspx]:

Image

Teraz sprawdźmy zawartość tabeli [ARTICLES]:

Image

Artykuły [crayon bille] i [ramette 50 feuilles] zostały zakupione, a ich stany magazynowe zmniejszono o zakupioną ilość. Artykułu [stylo plume] nie udało się zakupić, ponieważ zamówiona ilość przekraczała stan magazynowy. Zachęcamy czytelnika do przeprowadzenia dodatkowych testów.

2.5.6. Klasa implementacyjna [ArticlesDaoSqlMap]

2.5.6.1. Produkt Ibatis SqlMap

Napisaliśmy cztery różne implementacje warstwy [dao] naszej aplikacji [webarticles]. Za każdym razem udało nam się zintegrować nową warstwę [dao] z aplikacją [webarticles] bez konieczności rekompilacji dwóch pozostałych warstw: [web] i [domain]. Należy przypomnieć, że osiągnięto to dzięki dwóm rozwiązaniom architektonicznym:

  • dostęp do warstw za pośrednictwem interfejsów
  • integracja warstw za pomocą Spring

Chcielibyśmy pójść nieco dalej. Chociaż nasze cztery implementacje warstwy [dao] są różne, wykazują uderzające podobieństwa. Po napisaniu pierwszej implementacji pozostałe trzy uzyskano praktycznie poprzez skopiowanie i wklejenie kodu oraz zastąpienie niektórych słów kluczowych innymi. Logika natomiast nie uległa zmianie. Można się zastanawiać, czy nie byłoby możliwe stworzenie implementacji, która uwolniłaby nas od różnych sposobów dostępu do danych. Wykorzystaliśmy cztery z nich:

  • dostęp za pośrednictwem sterownika ODBC do źródła danych ODBC
  • bezpośredni dostęp do bazy danych SQL Server
  • dostęp za pośrednictwem sterownika Ole Db do źródła danych Ole Db
  • bezpośredni dostęp do bazy danych Firebird

Narzędzie Ibatis SqlMap [[http://www.ibatis.com/] umożliwia tworzenie warstw dostępu do danych, które są niezależne od rzeczywistego rodzaju źródła danych. Dostęp do danych jest zapewniony za pomocą:

  • plików konfiguracyjnych, w których umieszczone są informacje definiujące źródło danych oraz operacje, które mają być na nim wykonywane
  • biblioteki klas, które wykorzystują te informacje w celu uzyskania dostępu do danych

Narzędzie Ibatis SqlMap zostało pierwotnie opracowane dla platformy Java. Jego przeniesienie na platformę .NET jest niedawne i wydaje się, że zawiera pewne błędy (osobista opinia, która wymagałaby dokładnej weryfikacji). Niemniej jednak, ponieważ narzędzie to sprawdziło się na platformie Java, warto zaprezentować jego wersję .NET.

2.5.6.2. Gdzie można znaleźć IBATIS SqlMap?

Główna strona Firebirda to [http://www.ibatis.com/]. Strona z plikami do pobrania zawiera następujące linki:

Image

Wybierz link [Stable Binaries], który przekierowuje nas do strony [SourceForge.net]. Należy przeprowadzić proces pobierania do końca. Otrzymujemy plik ZIP zawierający następujące pliki:

Image

W projekcie Visual Studio korzystającym z Ibatis SqlMap należy wykonać dwie czynności:

  • umieścić powyższe pliki w folderze [bin] projektu
  • dodać do projektu odwołanie do każdego z tych plików

2.5.6.3. Pliki konfiguracyjne Ibatis SqlMap

Źródło danych [SqlMap] zostanie zdefiniowane za pomocą następujących plików konfiguracyjnych:

  1. providers.config: definiuje biblioteki klas, które mają być używane do uzyskania dostępu do danych
  2. sqlmap.config: definiuje parametry połączenia, które ma zostać nawiązane
  3. pliki mapowania: definiują operacje, które mają być wykonywane na danych

Logika działania tych plików jest następująca:

  • Aby uzyskać dostęp do danych, będziemy potrzebować połączenia. Aby je przedstawić, zetknęliśmy się już z kilkoma klasami: OdbcConnection, SqlConnection, OleDbConnection, FbConnection. Będziemy również potrzebować obiektu [Command] do wysyłania zapytań SQL: OdbcCommand, SqlCommand, OleDbCommand, FbCommand. Itd. W pliku [providers.config] definiujemy wszystkie potrzebne nam klasy.
  • Plik [sqlmap.config] zasadniczo definiuje ciąg połączenia z bazą danych zawierającą dane. Połączenie z bazą danych zostanie otwarte poprzez instancjonowanie klasy [Connection] zdefiniowanej w pliku [providers.config], do której konstruktora zostanie przekazany ciąg połączenia zdefiniowany w pliku [sqlmap.config].
  • Pliki mapowania definiują:
    • powiązania między wierszami tabel danych a klasą .NET, której instancje będą zawierały te wiersze
    • operacje SQL, które mają zostać wykonane. Są one identyfikowane za pomocą nazwy. Kod .NET wykonuje te operacje na podstawie ich nazw, co powoduje wyeliminowanie całego kodu SQL z kodu .NET.

2.5.6.4. Pliki konfiguracyjne projektu [dao-sqlmap]

Przyjrzyjmy się na przykładzie dokładnej naturze plików konfiguracyjnych projektu SqlMap. Rozważmy przypadek, w którym źródłem danych jest źródło Firebird o nazwie ODBC z paragrafu 2.3.3.1.

2.5.6.4.1. providers.config

Plik [providers.config] dla źródła ODBC wygląda następująco:

<?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>

Uwagi:

  • plik [providers.config] jest dostarczany wraz z pakietem [SqlMap]. Zawiera on kilka standardowych dostawców usług internetowych (providerów). Powyższy kod pochodzi bezpośrednio z tego pliku.
  • element <provider> ma nazwę – wiersz 6 – która może być dowolna
  • element <provider> może być włączony ([enabled=true]) lub wyłączony ([enabled=false]). Jeśli jest włączony, element DLL, do którego odwołuje się wiersz 8, musi być dostępny. Plik [providers.config] może zawierać wiele tagów <provider>.
  • wiersz 8 – nazwa zestawu zawierającego zdefiniowane klasy wiersze 9–15
  • wiersz 9 – klasa, której należy użyć do utworzenia połączenia
  • wiersz 10 – klasa, której należy użyć do utworzenia obiektu [Command] służącego do wysyłania poleceń SQL
  • wiersz 11 – klasa służąca do zarządzania parametrami skonfigurowanego polecenia SQL
  • wiersz 12 – klasa wyliczeniowa możliwych typów danych dla pól tabeli
  • wiersz 13 – nazwa właściwości obiektu [Parameter], która zawiera typ wartości tego parametru
  • wiersz 14 – nazwa klasy [Adapter] umożliwiającej tworzenie obiektów [DataSet] na podstawie źródła danych
  • wiersz 15 – nazwa klasy [CommandBuilder], która w połączeniu z obiektem [Adapter] umożliwia automatyczne generowanie właściwości [InsertCommand, DeleteCommand, UpdateCommand] tego obiektu na podstawie jego właściwości [SelectCommand]
  • wiersze 16–19 – definiuje się sposób obsługi skonfigurowanych poleceń SQL. W zależności od sytuacji należy wpisać na przykład:
insert into ARTICLES(id,nom,prix,stockactuel,stockminimum) values (?,?,?,?,?)

lub

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

W pierwszym przypadku mowa jest o formalnych parametrach pozycyjnych. Ich rzeczywiste wartości należy podać w kolejności zgodnej z kolejnością parametrów formalnych. W drugim przypadku mamy do czynienia z parametrami nazwanymi. Wartość takiego parametru podaje się, określając jego nazwę. Kolejność nie ma już znaczenia.

  • wiersz 16 – wskazano, że źródła ODBC wykorzystują parametry pozycyjne
  • wiersze 17–19 – dotyczą parametrów nazwanych. Tutaj nie ma żadnych takich parametrów.

Informacje te pozwalają programowi SqlMap ustalić na przykład, jaką klasę należy instancjonować w celu utworzenia połączenia. W tym przypadku będzie to klasa [OdbcConnection] (wiersz 9).

2.5.6.4.2. sqlmap.config

Plik [providers.config] definiuje klasy, których należy użyć w celu uzyskania dostępu do źródła ODBC. Nie wskazuje on żadnego źródła ODBC. Robi to plik [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>
     <!-- ==== źródło danych ========= --    >
    <database>
        <provider name="${provider}"/>
        <dataSource name="sqlmaparticles"  connectionString="${connectionString}"/>
        <transactionManager type="ADO/SWC" />
    </database>
    <sqlMaps>
        <sqlMap resource="articles.xml" />
    </sqlMaps>
</sqlMapConfig>

Uwagi:

  • wiersz 3 – definiuje się plik właściwości [properties.xml]. Plik ten definiuje pary (klucz, wartość). Klucze mogą być dowolne. Wartość powiązana z kluczem C jest uzyskiwana za pomocą notacji ${C} w pliku [sqlmap.config]. Oto plik [properties.xml], który zostanie powiązany z poprzednim plikiem [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>

wiersz 3 – zdefiniowano klucz [provider]. Jego wartością jest nazwa tagu <provider>, który ma być użyty w pliku [providers.config]

wiersz 4 – zdefiniowano klucz [connectionString]. Jego wartością jest ciąg połączenia, który należy wykorzystać do nawiązania połączenia ze źródłem danych ODBC Firebird.

  • wiersze 4–7 – parametry konfiguracyjne:
    • wiersz 5 – zapytania SQL będą identyfikowane za pomocą nazwy, która sama w sobie może należeć do przestrzeni nazw. [useStatementNamespaces="false"] oznacza, że nie będą używane przestrzenie nazw.
    • wiersz 6 – SqlMap posiada różne strategie buforowania w celu zminimalizowania dostępu do źródła danych. [cacheModelsEnabled="false"] oznacza, że nie będzie stosowana żadna z nich.
  • wiersze 9–13 – definiuje się cechy źródła danych:
    • wiersz 10 – nazwa dostawcy <provider> [providers.config], który ma być używany
    • wiersz 11 – ciąg połączenia z źródłem danych
    • wiersz 12 – menedżer transakcji. W tym przypadku nie został on użyty, jednak wiersz ten pozostawiono, ponieważ znajdował się w standardowym pliku dystrybucyjnym.
  • wiersze 14–16 – lista plików definiujących operacje SQL, które mają zostać wykonane na źródle danych.
  • wiersz 15 – definiuje plik mapowania [articles.xml]
2.5.6.4.3. articles.xml

Plik ten pełni dwie funkcje:

  • definiuje mapowanie obiektowe tabel źródła danych. W najprostszych przypadkach sprowadza się to do przypisania klasy do wiersza w tabeli.
  • zdefiniowanie operacji SQL wraz z ich parametrami oraz nadanie im nazw.

Wykorzystamy następujący plik [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>
     <!-- zapytań SQL -->
    <statements>
         <!-- pobieranie wszystkich artykułów -->
        <select id="getAllArticles" resultMap="article">
                select ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM FROM ARTICLES
            </select> 
         <!-- usunięcie wszystkich artykułów-->
        <delete id="clearAllArticles">
                delete from ARTICLES
            </delete> 
         <!-- dodanie pozycji -->
        <insert id="insertArticle" parameterClass="istia.st.articles.dao.Article">
                insert into ARTICLES (id, nom, prix,stockactuel, stockminimum) values
                ( #id#, #nazwa#, #cena#, #aktualny stan magazynowy#, #minimalny stan magazynowy#)
            </insert>
         <!-- usunięcie jednego produktu -->
        <delete id="deleteArticle" parameterClass="int">
                delete FROM ARTICLES where ID= #wartość#
            </delete>
         <!-- zmiana produktu -->
        <update id="modifyArticle" parameterClass="istia.st.articles.dao.Article">
                update ARTICLES set NOM= #nazwa# ,PRIX= #cena# ,STOCKACTUEL= #aktualny stan magazynowy# ,STOCKMINIMUM= #minimalny stan magazynowy#, gdzie ID= #id#
            </update>
         <!-- wyszukiwanie konkretnego produktu -->
        <select id="getArticleById" resultMap="article" parameterClass="int">
                select ID, NOM, PRIX,    STOCKACTUEL, STOCKMINIMUM FROM ARTICLES where ID= #wartość#
            </select>
         <!-- zmiana stanu magazynowego artykułu -->
        <update id="changerStockArticle" parameterClass="Hashtable">
                update ARTICLES set STOCKACTUEL=(STOCKACTUEL + #ruch#), gdzie ID=#id# oraz ((STOCKACTUEL + #ruch#) >=0)
      </update>
    </statements>
</sqlMap>

Komentarze:

  • wiersze 4–11 – definiujemy mapowanie między wierszem tabeli [ARTICLES] ze źródła danych a klasą [istia.st.articles.dao.Article]. Każdej kolumnie (column) tabeli przypisana jest właściwość (property) klasy [Article]. To mapowanie pozwala klasie [SqlMap] na utworzenie wyniku operacji SQL SELECT. Każdy wiersz wynikowy operacji SELECT zostanie umieszczony w obiekcie [Article] zgodnie z regułami mapowania.
  • wiersz 5 – mapowanie jest otoczone tagiem <resultMap> i nazwane za pomocą atrybutu [id="article"]. Powiązana klasa jest określona przez atrybut [class="istia.st.articles.dao.Article"].
  • wiersze 14–44 – definiuje się potrzebne operacje SQL
  • wiersze 16–18 – definiuje się operację SELECT o nazwie [getAllArticles]
    • wiersz 16 – operacja SELECT nosi nazwę [name= "getAllArticles "], a mapowanie, które ma zostać użyte, jest określone przez atrybut [resultMap="article"]. Odwołujemy się więc tutaj do mapowania z wierszy 5–11
    • wiersz 17 – tekst polecenia SQL, które ma zostać wykonane
  • wiersze 20–22 – definiuje się polecenie SQL-Delete o nazwie [clearAllArticles], służące do wyczyszczenia tabeli artykułów.
  • wiersze 24–27 – definiuje się polecenie SQL-Insert [insertArticle] służące do dodania nowego artykułu do tabeli artykułów. Jest to zapytanie parametryzowane przez elementy (#id#, #nazwa#, #cena#, #aktualnystan#, #minimalnystan#). Wartości tych pięciu elementów będą pochodzić z obiektu [Article] przekazanego jako parametr: [parameterClass="istia.st.articles.dao.Article"]. Obiekt parametryczny musi posiadać właściwości (id, nazwa, cena, aktualny stan magazynowy, minimalny stan magazynowy), do których odwołuje się skonfigurowane polecenie SQL.
  • wiersze 29–31 – definiujemy polecenie SQL Delete [deleteArticle], służące do usunięcia artykułu o znanym numerze #value#. Numer ten zostanie przekazany jako parametr: [parameterClass="int"]. Jest to ogólna zasada. Gdy parametr jest unikalny, odwołuje się do niego za pomocą słowa kluczowego #value# w treści polecenia SQL.
  • wiersze 33–35 – definiuje się polecenie SQL-Update [modifyArticle] służące do modyfikacji artykułu, którego numer jest znany. Podobnie jak w przypadku polecenia [insertArticle], pięć niezbędnych informacji pochodzi z właściwości obiektu [istia.st.articles.dao.Article].
  • wiersze 37–39 – definiuje się polecenie SQL-Select [getArticleById], które pozwala uzyskać wiersz artykułu, którego numer jest znany.
  • wiersze 41–43 – definiuje się polecenie SQL-Update [changerStockArticle], które modyfikuje pole [stockactuel] dla artykułu, którego numer jest znany. Dwie niezbędne informacje, nr #id# artykułu oraz przyrost #mouvement# stanu magazynowego, znajdują się w słowniku: [parameterClass="Hashtable"]. Słownik ten musi posiadać dwa klucze: id i mouvement. To właśnie wartości powiązane z tymi dwoma kluczami zostaną wykorzystane w poleceniu SQL.
2.5.6.4.4. Lokalizacja plików konfiguracyjnych

Rozważymy dwie różne sytuacje:

  • w przypadku testu Nunit pliki konfiguracyjne [SqlMap] zostaną umieszczone w tym samym folderze co testowane pliki binarne.
  • w przypadku aplikacji internetowej zostaną one umieszczone w katalogu głównym aplikacji.

2.5.6.5. API z SqlMap

Klasy z pliku SqlMap są zawarte w pliku DLL, który zazwyczaj umieszcza się w folderze [bin] aplikacji:

Image

Aplikacje korzystające z klas z pliku SqlMap muszą zaimportować przestrzeń nazw [IBatisNet.DataMapper]:

Imports IBatisNet.DataMapper

Wszystkie operacje klasy SQL są wykonywane za pośrednictwem singletonu typu [Mapper], klasy należącej do przestrzeni nazw [IBatisNet.DataMapper ]. Singleton uzyskuje się w następujący sposób:

        Dim mappeur As SqlMapper = Mapper.Instance

Aby wykonać polecenie SqlMap [getAllArticles], należy wpisać:

                    dim articles as IList=mappeur.QueryForList("getAllArticles", Nothing)
  • metoda [QueryForList] pozwala uzyskać wynik polecenia SELECT w postaci listy
  • pierwszy parametr to nazwa polecenia SQL, które ma zostać wykonane (patrz articles.xml)
  • drugim parametrem jest parametr, który ma zostać przekazany do żądania SQL. Musi on odpowiadać atrybutowi [parameterClass] polecenia SqlMap. W [articles.xml] mamy [parameterClass=Nothing]. Dlatego przekazujemy tutaj wskaźnik zerowy.
  • Wynik ma typ IList. Obiekty z tej listy są wskazane przez atrybut [resultMap] polecenia SQL-select: [resultMap="article"]. „article” to nazwa mapowania:
<resultMap id="article" class="istia.st.articles.dao.Article">

Klasą powiązaną z tym mapowaniem jest [istia.st.articles.dao.Article]. Ostatecznie zmienna [articles] zdefiniowana powyżej stanowi listę obiektów [ istia.st.articles.dao.Article]. W ten sposób uzyskaliśmy całą tabelę [ARTICLES] za pomocą jednej instrukcji. Jeśli tabela [ARTICLES] jest pusta, otrzymujemy obiekt [IList] zawierający 0 elementów.

Aby wykonać polecenie SqlMap [getArticleById], należy wpisać:

dim unArticle as Article=CType(mappeur.QueryForObject("getArticleById", idArticle), Article)
  • metoda [QueryForObject] pozwala uzyskać wynik polecenia SELECT, które zwraca tylko jedną linię
  • pierwszym parametrem jest nazwa polecenia SqlMap, które ma zostać wykonane
  • drugi parametr to parametr, który ma zostać przekazany do zapytania SQL. Musi on odpowiadać atrybutowi [parameterClass] polecenia SqlMap. W [articles.xml] mamy [parameterClass="int"]. Dlatego przekazujemy tutaj liczbę całkowitą reprezentującą numer poszukiwanego artykułu.
  • Wynik jest typu Object. Jeśli polecenie SELECT nie zwróciło żadnego wiersza, jako wynik otrzymujemy wskaźnik zerowy (nothing).

Aby wykonać polecenie SqlMap [insertArticle], należy wpisać:

                    mappeur.Insert("insertArticle", unArticle)
  • metoda [Insert] umożliwia wykonanie poleceń SQL i INSERT
  • pierwszym parametrem jest nazwa polecenia SqlMap, które ma zostać wykonane
  • drugim parametrem jest parametr, który ma zostać przekazany do tego polecenia. Musi on odpowiadać atrybutowi [parameterClass] polecenia SqlMap. W [articles.xml] występuje [parameterClass="istia.st.articles.dao.Article"]. Dlatego przekazujemy tutaj obiekt typu [istia.st.articles.dao.Article].

Aby wykonać polecenie SqlMap [deleteArticle], należy wpisać:

                    dim nbArticles as Integer=mappeur.Delete("deleteArticle", idArticle)
  • metoda [Delete] umożliwia wykonanie poleceń SQL i DELETE
  • pierwszym parametrem jest nazwa polecenia SQL, które ma zostać wykonane
  • drugim parametrem jest parametr, który ma zostać przekazany do tego polecenia. Musi on odpowiadać atrybutowi [parameterClass] polecenia SqlMap. W poleceniu [articles.xml] występuje [parameterClass="int"]. Dlatego w tym miejscu podaje się numer artykułu, który ma zostać usunięty.
  • Wynikiem metody [Delete] jest liczba usuniętych wierszy

Analogicznie, aby wykonać polecenie SqlMap [clearAllArticles], należy wpisać:

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

Aby wykonać polecenie SqlMap [modifyArticle], należy wpisać:

                    dim nbArticles as Integer=mappeur.Update("modifyArticle", unArticle)
  • metoda [Update] umożliwia wykonanie poleceń SQL oraz UPDATE
  • pierwszy parametr to nazwa polecenia SqlMap, które ma zostać wykonane
  • drugim parametrem jest parametr, który ma zostać przekazany do tego polecenia. Musi on odpowiadać atrybutowi [parameterClass] polecenia SqlMap. W [articles.xml] występuje [parameterClass="istia.st.articles.dao.Article"]. Dlatego przekazujemy tutaj obiekt typu [istia.st.articles.dao.Article].
  • Wynikiem działania metody [Update] jest liczba zmodyfikowanych wierszy.

Analogicznie, aby wykonać polecenie SqlMap [changerStockArticle], należy wpisać:

                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' aktualizacja
                    dim nbLignes as Integer= mappeur.Update("changerStockArticle", paramètres)
  • drugi parametr odpowiada atrybutowi [parameterClass] polecenia SqlMap. W [articles.xml] występuje [parameterClass="Hashtable"]. Polecenie SQL, skonfigurowane jako [changerStockArticle], wykorzystuje parametry [id, mouvement]. Dlatego przekazujemy tutaj słownik zawierający te dwa klucze.

2.5.6.6. Kod klasy [ArticlesDaoSqlMap]

Po powyższych wyjaśnieniach jesteśmy teraz w stanie napisać następującą nową klasę implementacyjną [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

         ' pola prywatne
        Dim mappeur As SqlMapper = Mapper.Instance

         ' lista wszystkich artykułów
        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

         ' dodanie artykułu
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            SyncLock Me
                Try
                     ' unArticle: artykuł do dodania
                     ' wstawienie
                    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

         ' usuwa artykuł
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                     ' id: identyfikator pozycji do usunięcia
                     ' usunięcie
                    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

         ' zmiana artykułu
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                     ' aktualizacja
                    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

         ' wyszukiwanie artykułu
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            SyncLock Me
                Try
                     ' id: identyfikator szukanego artykułu
                    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

         ' usunięcie wszystkich artykułów
        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

         ' zmiana stanu magazynowego artykułu
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                     ' id: identyfikator artykułu, którego stan magazynowy jest zmieniany
                     ' ruch: ruch magazynowy
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' aktualizacja
                    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

Zachęcamy czytelnika do zapoznania się z tym kodem w świetle wyjaśnień dotyczących kodu API z SqlMap. Warto zauważyć, że zastosowanie kodu [SqlMap] znacznie zmniejszyło ilość kodu do napisania.

2.5.6.7. Generowanie pliku assembly warstwy [dao]

Nowy projekt w Visual Studio ma następującą strukturę:

Image

Warto zwrócić uwagę na obecność „assembly” niezbędnych dla SqlMap w odwołaniach projektu. Te DLL zostały umieszczone w folderze [bin] projektu. Projekt jest skonfigurowany tak, aby generować plik o nazwie DLL, zwany [webarticles-dao.dll]:

2.5.6.8. Testy Nunit warstwy [dao]

2.5.6.8.1. Klasa testowa NUnit

Klasa testowa Nunit dla klasy implementacyjnej [ArticlesDaoSqlMap] jest taka sama jak dla klasy [ArticlesDaoPlainODBC] (patrz punkt 2.3.3.2). Podobną procedurę stosujemy przy przygotowywaniu testu Nunit dla klasy [ArticlesDaoSqlMap]:

  • w folderze Visual Studio projektu [dao-sqlmap] tworzymy folder [test1] (po prawej), kopiując folder [tests] z projektu [dao-odbc] (po lewej):
  • w folderze [tests] zastępujemy pliki DLL i [webarticles-dao.dll] plikami DLL i [webarticles-dao.dll] pochodzącymi z generacji projektu [dao-sqlmap].
  • Dodajemy pliki DLL niezbędne dla SqlMap, a także omówione pliki konfiguracyjne [providers.config, sqlmap.config, properties.xml, articles.xml].
  • Modyfikujemy plik konfiguracyjny [spring-config.xml] w celu utworzenia instancji nowej klasy [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>

Uwagi:

  • w wierszu 7 obiekt [articlesdao] jest teraz powiązany z instancją klasy [ArticlesDaoSqlMap]
  • klasa ta nie posiada konstruktora. Zostanie użyty konstruktor domyślny.
2.5.6.8.2. Tests

Tabela [ARTICLES] w źródle danych Firebird została wypełniona następującymi pozycjami:

Image

Jesteśmy gotowi do testów. Za pomocą aplikacji [Nunit-Gui] ładujemy pliki DLL i [test-webarticles-dao.dll] z powyższego folderu [test1] i uruchamiamy test [testGetAllArticles]:

Image

Pomimo nazwy [NUnitTestArticlesDaoArrayList] nadanej początkowo klasie testowej, to właśnie klasa [ArticlesDaoSqlMap] jest tutaj testowana. Zrzut ekranu pokazuje, że poprawnie pobraliśmy pozycje, które umieściliśmy w tabeli [ARTICLES]. Teraz przeprowadźmy wszystkie testy:

Image

Czytelnik przeglądający ten dokument na ekranie zauważy, że niektóre testy zakończyły się powodzeniem (kolor zielony), a inne zakończyły się niepowodzeniem (kolor czerwony). Testy, które zakończyły się niepowodzeniem, to testy [testArticleAbsent] i [testChangerStockArticle]. Po długich poszukiwaniach wydaje się, że przyczyny tych niepowodzeń są następujące:

  • w teście [testArticleAbsent] wymagana jest modyfikacja artykułu, który nie istnieje. W tym celu używana jest metoda [modifieArticle], która zwraca liczbę zmodyfikowanych wierszy, czyli 0 lub 1. W tym przypadku powinna to być wartość 0. Zamiast tego pojawia się wyjątek typu [IBatisNet.Common.Exceptions.ConcurrentException].
  • W [changerStockArticle] ponownie występuje operacja typu [update]. Chodzi o zmniejszenie stanu magazynowego o ilość większą niż sam stan magazynowy. W tym celu stosuje się metodę [changerStockArticle], która zwraca liczbę zmodyfikowanych wierszy, czyli 0 lub 1. Polecenie SQL zostało napisane w celu uniknięcia aktualizacji (por. polecenie SQL „changerStockArticle” w articles.xml), która spowodowałaby, że stan magazynowy stałby się ujemny. Oczekuje się tutaj, że wynikiem metody [changerStockArticle] będzie 0. Ponownie pojawia się wyjątek typu [IBatisNet.Common.Exceptions.ConcurrentException].

Możliwych źródeł błędów jest wiele:

  1. kod klasy [ArticlesDaoSqlMap] jest błędny. Jest to możliwe. Pochodzi on jednak z portu klasy Java, która działała poprawnie z wersją Java klasy SqlMap.
  2. wersja .NET klasy SqlMap zawiera błąd
  3. sterownik ODBC dla Firebirda zawiera błąd
  4. ...

Wobec braku pewności obejdziemy tę przeszkodę, przechwytując wspomniany wyjątek [IBatisNet.Common.Exceptions.ConcurrentException]. Nowy kod klasy [ArticlesDaoSqlMap] wygląda następująco:

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

    Public Class ArticlesDaoSqlMap
        Implements IArticlesDao

         ' pola prywatne
        Dim mappeur As SqlMapper = Mapper.Instance

         ' lista wszystkich artykułów
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
...
        End Function

         ' dodanie artykułu
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
...
        End Function

         ' usunięcie artykułu
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            SyncLock Me
                Try
                     ' id: identyfikator artykułu do usunięcia
                     ' usunięcie
                    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

         ' zmiana artykułu
        Public Function modifieArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.modifieArticle
            SyncLock Me
                Try
                     ' aktualizacja
                    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

         ' wyszukiwanie artykułu
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
...
        End Function

         ' usunięcie wszystkich artykułów
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
....
        End Sub

         ' zmiana stanu magazynowego artykułu
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            SyncLock Me
                Try
                     ' id: identyfikator artykułu, którego stan magazynowy jest zmieniany
                     ' ruch: ruch magazynowy
                    Dim paramètres As New Hashtable(2)
                    paramètres("id") = idArticle
                    paramètres("mouvement") = mouvement
                     ' aktualizacja
                    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

Zmiany dotyczą wierszy: 28, 41, 69. W przypadku operacji SQL typu [UPDATE, DELETE], jeśli wystąpi wyjątek typu [IBatisNet.Common.Exceptions.ConcurrentException], jako wynik zwracamy 0, wskazując tym samym, że żaden wiersz nie został zmodyfikowany ani usunięty. Następnie operacja DLL projektu jest ponownie generowana, umieszczana w folderze [test1], a testy NUnit są uruchamiane ponownie:

Image

Tym razem wszystko działa poprawnie. Od tej pory będziemy pracować z tym plikiem DLL.

2.5.6.9. Włączenie nowej warstwy [dao] do aplikacji [webarticles]

2.5.6.9.1. źródło danych ODBC

Testujemy tutaj źródło danych ODBC omówione w paragrafie 2.3.3.1. Jest ono tutaj wykorzystywane za pośrednictwem SqlMap.

Postępujemy zgodnie z procedurą opisaną w punkcie 2.3.4. Wprowadzamy następujące zmiany w zawartości folderu [runtime]:

  • w folderze [bin] plik DLL zstarej warstwy [dao] zostaje zastąpiony przez DLL z nowej warstwy [dao] zaimplementowanej przez klasę [ArticlesDaoSqlMap]. Dodajemy do niej warstwy DLL niezbędne dla Firebirda oraz SqlMap:

Image

  • w [runtime] umieszcza się pliki konfiguracyjne z SqlMap i [providers.config, sqlmap.config, properties.xml, articles.xml]:

Image

  • w [runtime] plik konfiguracyjny [web.config] zostaje zastąpiony plikiem uwzględniającym nową klasę implementacji:
<?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>

Uwagi:

  • w wierszu 14 do singletonu [articlesDao] przypisano instancję nowej klasy [ArticlesDaoSqlMap]. To jedyna zmiana.

Jesteśmy gotowi do przeprowadzenia testów. Konfigurujemy serwer WWW [Cassini] tak samo jak w poprzednich testach. Inicjujemy tabelę artykułów następującymi wartościami:

Image

Za pomocą przeglądarki wywołujemy stronę URL [http://localhost/webarticles/main.aspx]:

Image

Teraz sprawdźmy zawartość tabeli [ARTICLES]:

Image

Artykuły [couteau] i [cuiller] zostały zakupione, a ich stany magazynowe zmniejszono o zakupioną ilość. Artykułu [fourchette] nie udało się zakupić, ponieważ zamówiona ilość przekraczała stan magazynowy. Zachęcamy czytelnika do przeprowadzenia dodatkowych testów.

2.5.6.9.2. źródło danych MSDE

W tym miejscu testujemy źródło danych MSDE omówione w paragrafie 2.4.3.1. Jest ono tutaj wykorzystywane za pośrednictwem SqlMap. Postępujemy tak samo jak poprzednio. Wprowadzamy następujące zmiany w zawartości folderu [runtime]:

  • zawartość folderu [bin] pozostaje bez zmian
  • w pliku [runtime] zmieniają się pliki konfiguracyjne z plików SqlMap i [providers.config, properties.xml]. Pliki konfiguracyjne z pliku [sqlmap.config, articles.xml] pozostają bez zmian.
  • Plik [providers.config] konfiguruje nowy element <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>

Ten element <provider> wykorzystuje klasy .NET zapewniające dostęp do źródeł danych serwera SQL. Jest on domyślnie zintegrowany z plikiem szablonu [providers.config] dystrybuowanym wraz z plikiem SqlMap.

  • Plik [properties.xml] definiuje element <provider> źródła MSDE oraz jego ciąg połączenia:
<?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>
  • w pliku [runtime] plik konfiguracyjny [web.config] pozostaje bez zmian.

Jesteśmy gotowi do testów. Serwer WWW [Cassini] zachowuje swoją standardową konfigurację. Inicjujemy tabelę artykułów źródła MSDE przy użyciu pliku [EMS MS SQL Manager]:

Image

Za pomocą przeglądarki wysyłamy żądanie do serwera URL i [http://localhost/webarticles/main.aspx]:

Image

Teraz sprawdźmy zawartość tabeli [ARTICLES] wraz z [EMS MS SQL Manager]:

Image

Artykuły [ballon foot] i [raquette tennis] zostały zakupione, a ich stany magazynowe zmniejszono o zakupioną ilość. Artykułu [rollers] nie udało się zakupić, ponieważ zamówiona ilość przekraczała stan magazynowy. Zachęcamy czytelnika do przeprowadzenia dodatkowych testów.

2.5.6.9.3. źródło danych OleDb

W tym miejscu testujemy źródło danych ACCESS przedstawione w paragrafie 2.4.5.1. Jest ono tutaj wykorzystywane za pośrednictwem pliku SqlMap. Postępujemy tak samo jak poprzednio. Wprowadzamy następujące zmiany w zawartości pliku [runtime]:

  • zawartość folderu [bin] pozostaje bez zmian
  • w pliku [runtime] zmieniają się pliki konfiguracyjne z plików SqlMap i [providers.config, properties.xml]. Pliki konfiguracyjne z pliku [sqlmap.config, articles.xml] pozostają bez zmian.
  • Plik [providers.config] konfiguruje nowy element <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>

Ten <provider> wykorzystuje klasy .NET do dostępu do źródeł danych OleDb. Jest on domyślnie zintegrowany z plikiem szablonu [providers.config] dystrybuowanym wraz z SqlMap.

  • Plik [properties.xml] definiuje element <provider> źródła OleDb oraz jego ciąg połączenia:
<?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>
  • w pliku [runtime] plik konfiguracyjny [web.config] pozostaje bez zmian.

Jesteśmy gotowi do testów. Serwer WWW [Cassini] zachowuje swoją standardową konfigurację. Inicjujemy tabelę artykułów ze źródła ACCESS w następujący sposób:

Image

Za pomocą przeglądarki wysyłamy żądanie do serwera URL i [http://localhost/webarticles/main.aspx]:

Image

Teraz sprawdźmy zawartość tabeli [ARTICLES] za pomocą:

Image

Artykuły [pantalon] i [jupe] zostały zakupione, a ich stany magazynowe zmniejszono o zakupioną ilość. Artykułu [manteau] nie udało się zakupić, ponieważ zamówiona ilość przekraczała stan magazynowy. Zachęcamy czytelnika do przeprowadzenia dodatkowych testów.

2.5.7. Wnioski

Na tym kończymy ten obszerny artykuł-poradnik. Co zrobiliśmy?

  • Zaimplementowaliśmy warstwę [dao] w trójwarstwowej aplikacji internetowej na cztery różne sposoby:
    1. wykorzystując klasy dostępu .NET do źródeł ODBC
    2. wykorzystując klasy dostępu .NET do zasobów serwera SQL
    3. korzystając z klas dostępu .NET do źródeł OleDb
    4. przy użyciu klas dostępu innych producentów w celu uzyskania dostępu do bazy danych Firebird
  • za każdym razem wbudowaliśmy nową warstwę [dao] do aplikacji [webarticles] o trzech warstwach [web, domain, dao] bez konieczności rekompilacji żadnej z warstw [web, domain]
  • w końcu wprowadziliśmy narzędzie [SqlMap], które pozwoliło nam stworzyć warstwę [dao] zdolną do dostosowywania się do różnych źródeł danych w sposób przezroczysty dla kodu. Dzięki tej nowej warstwie mogliśmy kolejno korzystać ze źródeł danych z poprzednich implementacji od 1 do 3. Odbyło się to w sposób przejrzysty przy pomocy plików konfiguracyjnych.
  • Wykazaliśmy dużą elastyczność, jaką narzędzia Spring i SqlMap zapewniają trójwarstwowym aplikacjom internetowym.