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

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:
- 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.
- kontroler przetwarza to żądanie. W tym celu może potrzebować pomocy warstwy biznesowej, zwanej modelem M w strukturze MVC.
- 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
- 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.
- 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:
- klasy biznesowe
- klasy dostępu do danych
- 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);
klucz podstawowy jednoznacznie identyfikujący artykuł | |
nazwa artykułu | |
jego cena | |
aktualny stan magazynowy | |
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:

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]

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:
- konstruktor umożliwiający ustawienie 5 informacji dotyczących artykułu: [id, nom, prix, stockactuel, stockminimum]
- publiczne właściwości umożliwiające odczyt i zapis tych 5 informacji.
- weryfikację danych wprowadzonych do artykułu. W przypadku błędnych danych generowany jest wyjątek.
- 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:
zwraca wszystkie artykuły z źródła danych | |
opróżnia źródło danych | |
zwraca obiekt [Article] zidentyfikowany na podstawie jego numeru | |
umożliwia dodanie pozycji do źródła danych | |
umożliwia zmianę pozycji w źródle danych | |
umożliwia usunięcie pozycji ze źródła danych | |
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:
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
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
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
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
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
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
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
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
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
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
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ę:

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:

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

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:

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ść:
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:

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

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

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

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]:
![]() | ![]() |

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:

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

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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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:

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

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

![]() |
Teraz sprawdźmy zawartość tabeli [ARTICLES] wraz z ACCESS:

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.):
![]()
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:

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

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]:
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):

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

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:

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

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

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

![]() |
Teraz sprawdźmy zawartość tabeli [ARTICLES]:

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:

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:

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:
- providers.config: definiuje biblioteki klas, które mają być używane do uzyskania dostępu do danych
- sqlmap.config: definiuje parametry połączenia, które ma zostać nawiązane
- 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:
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:
lub
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]:
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]:
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]:
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:

Aplikacje korzystające z klas z pliku SqlMap muszą zaimportować przestrzeń nazw [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:
Aby wykonać polecenie SqlMap [getAllArticles], należy wpisać:
- 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:
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ć:
- 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ć:
- 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ć:
- 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ć:
Aby wykonać polecenie SqlMap [modifyArticle], należy wpisać:
- 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ę:

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

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

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:

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:
- 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.
- wersja .NET klasy SqlMap zawiera błąd
- sterownik ODBC dla Firebirda zawiera błąd
- ...
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:
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:

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:

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

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

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

![]() |
Teraz sprawdźmy zawartość tabeli [ARTICLES]:

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

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

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

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:

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

![]() |
Teraz sprawdźmy zawartość tabeli [ARTICLES] za pomocą:

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:
- wykorzystując klasy dostępu .NET do źródeł ODBC
- wykorzystując klasy dostępu .NET do zasobów serwera SQL
- korzystając z klas dostępu .NET do źródeł OleDb
- 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.































































