Trochę teorii
Dla tych, którzy nie spotkali się jeszcze z pojęciem O/RM
krótkie wyjaśnienie. Skrót oznacza Object-relational mapping. Narzędzia tej
klasy służą do odwzorowania relacyjnej bazy danych, na bliższe każdemu
programiście obiekty. Mapowanie odbywa się najczęsciej jako odworowanie definicji
klas na strukturę tabel, a każdy rekord takich tabel reprezentowany jest przez
konkretny obiekt. Jest kilka powodów, dla których warto zastanowić się nad
takim rozwiązaniem. W rozwiązaniach nie korzystających z O/RM, musimy używać
dwóch zupełnie różnych języków programowania, a kod, gdzie język obiektowy
przeplata się z SQL’em, jest ciężki do utrzymania i rozwijania, nie wspominając o debuggowaniu. Narzędzia klasy O/RM pozwalają uprościć i
zautomatyzować proces rozwijania i utrzymywania aplikacji bazodanych. Jednym z takich rozwiązań jest Sooda – Simple
Object Oriented Data Access, autorstwa Jarka Kowalskiego.
Sooda oparta jest na mechanizmie generacji kodu (o tym trochę
więcej za chwilę). W naturalny sposób odwzorowuje obiekty na tabele, wspiera
również kolekcje mapując je na relacje 1-N i M-N, dodatkowo umożliwiając
dziedziczenie i polimorfizm. Pozwala także osiągnąć wysoką wydajność, m.in poprzez
zaawansowane cache’owanie obiektów, "leniwe
ładowanie" (ang. lazy loading), itp. Cała konfiguracja natomiast przechowywana
jest w jednym miejscu, co jest dużą zaletą przy np. zapoznawaniu się z
aplikacją opartą na Soodzie.
Do tej pory największą bolączką Soody, był brak kompletnej
dokumentacji. Od paru dni na stronie domowej Soody, jest kompletna i bardzo
szczegółowa dokumentacja – link możecie znaleźć na końcu artykułu. Mam nadzieję, że
artykuł ten szczegółowo wyjaśni jak skonfigurować aplikację, żeby zaczęła
korzystać z Soody i krok po kroku wprowadzi Was w świat O/RM. Ponieważ
najlepiej uczyć się na przykładzie, stworzymy prostą ‘Wypożyczalnię Video’.
Osoby obecne na pierwszym spotkaniu Warszawskiej Grupie .NET z pewnością pamiętają ten przykład. Zacznijmy
więc od początku.
Konfiguracja aplikacji
Na początek przyda nam się plik instalacyjny Soody, lub też
źródła (odpowiednie adresy na końcu). Mając przykładową bazę
danych:

chcielibyśmy otrzymać strukturę klas odpowiadającą naszej
bazie danych taką, jak przedstawia nam diagram klas:

Podczas
konfiguracji projektu będziemy korzystać z aplikacji SoodaStubGen, dostępnej w
pakiecie instalacyjnym Soody. Ten mały program ma za zadanie z dwóch plików
konfiguracyjnych *.soodaproject i schematu bazy danych w pliku XML, wygenerować
klasy odpowiadające naszej bazie. Użyteczną funkcją tego programu, jest to, że
potrafi zaktualizować projekt Visual Studio, dodając do odpowiedniego pliku
*.csproj wygenerowane pliki. Kod wygenerowany przez Soodę (może to być C#,
VB.NET, Boo i teoretycznie każdy język wspierający generację kodu ze
struktur CodeDom) znajduje się głównie w tzw. Stubs’ach – zachęcam do
zerknięcia, co w tym pliku się znajduje. Dodatkowo SoodaStubGen generuje puste
klasy, w których będziemy umieszczać naszą logikę biznesową. Powyżej opisany
schemat tworzenia kodu wygląda mniej więcej tak:

Najwięcej pracy będziemy mieli przy tworzeniu pliku
SoodaSchema.xml, odpowiedzialnego za odwzorowanie klas na tabele w bazie danych. Pierwszą rzeczą jaką musimy umieścić w tym
pliku, jest definicja źródła danych – w tej chwili jedynym wspieranym źródłem
jest relacyjna baza danych. Aby zdefiniować źródło danych należy na początku
pliku SoodaSchema.xml dodać element:
<datasource name="default" type="Sooda.Sql.SqlDataSource" />
Podstawową tabelą, którą będziemy używać w naszej
wypożyczalni jest tabela Video. Zmapujmy więc ją na interesującą nas
klasę tak jak poniżej (pełne
mapowanie na końcu artykułu):
1 <class name="Video">
2 <table name="Video">
3 <field name="Id" type="Integer" primaryKey="true" nullable="false" />
4 <field name="Title" type="String" nullable="false" />
5 <field name="Category" type="Integer" references="VideoCategory" prefetch="1" />
6 <field name="Status" type="Integer" references="VideoStatus" prefetch="1" />
7 <field name="RentedOutTo" dbcolumn="rented_out_to" type="Integer" references="Customer" />
8 <field name="RentedDate" dbcolumn="rented_date" type="DateTime" nullable="true" />
9 <field name="ReturnedDate" dbcolumn="returned_date" type="DateTime" nullable="true" />
10 <field name="DirectedBy" dbcolumn="director" type="Integer" references="Artist" prefetch="1" />
11 <field name="YearOfProduction" dbcolumn="year_of_production" type="Integer" />
12 </table>
13 </class>
Podstawowym elementem jest class oparty na jednej lub
wielu tabelach (możliwe jest również oparcie wielu klas na jednej tabeli) w
bazie danych o nazwie „Video” (<table name="Video">).
Każdej kolumnie w tabeli odpowiada jedna właściwość:
<field name="RentedDate" dbcolumn="rented_date" type="DateTime" nullable="true" />
Każda taka właściwość
ma oznaczony typ, zazwyczaj taki jak typ w bazie danych, nazwę –
niekoniecznie taką samą i atrybut mówiący czy kolumna może być pusta. Poniżej wymieniono
najczęściej spotykane typy pól (są też inne np. typ Image – zachęcam do lektury
dokumentacji)
· String
- dodatkowo należy wyspecyfikować parametr size dla właściwości
· Integer
· DateTime
· BooleanAsInteger
– bardzo ciekawy typ, w bazie danych reprezentowany jako int (o wartości 1 lub
0), natomiast właściwość będzie przedstawiona jako typ boolean.
Przynajmniej jedna z właściwości musi być kluczem głównym (primaryKey="true"). Sooda
wspiera również złożone klucze główne.
Jak łatwo na pełnym mapowaniu część właściwości ma zdefiniowany
parametr references. Użycie
go, spowoduje wygenerowanie właściwości
będącej referencją do innej klasy – czyli tak naprawdę klucza obcego np.
w klasie Video, właściwość Status jest
tak naprawdę referencją do klasy VideoStatus.
Sooda wspiera również kolekcje jeden do wielu i wiele do wielu.
Te pierwsze definiuje się poprzez element:
·
<collectionOneToMany> - reprezentowana jako referencja i kolekcja (np.
dla klasy Video zdefiniowana jest kolekcja przetrzymująca hisorię wypożyczeń
(VideoHistory)
·
<collectionManyToMany> - reprezentowana
jako para kolekcji - dla Video jest to kolekcja aktorów, przypisanych do filmu
(Actors2Video). W odróżeniu od klas, relacja jest przedstawiona w pliku
mapującym, jako <relation>, gdzie klucze główne są referencjami
Przypuśćmy, że chcielibyśmy, aby w naszej aplikacji oprócz danych klientów,
przetrzymywać również dane pracowników, którzy mogliby wypożyczać filmy po
specjalnych cenach. Aby nie dublować funkcjonalności w dwóch różnych obiektach
(klient i pracownik). Najrozsądniejszym rozwiązaniem jest dziedziczenie klasy
pracownik z klasy klient, lub stworzenie klasy abstrakcyjnej, z której
dziedziczyłyby te dwie. Sooda wspomaga nas przy takich zadaniach, umożliwiając
zdefinowanie pola (selektora), które będzie używane do odróżniania konkretnych
podklas danej klasy. Najlepiej to zrozumieć na przykładzie naszej aplikacji.
Klasa Person ma zdefniowany selektor: subclassSelectorField="Type". Na podstawie wartości pola Type,
rekord z bazy danych będzie zwrócony jako obiekt tej, albo innej klasy. Klasy Customer
i Employee dziedziczą z klasy Person (inheritFrom="Person") na
podstawie różnych wartości pola Type (wartość zdefiniowana przez atrybut subclassSelectorValue klasy).
Klasa , która nie ma zdefinowanego subclassSelectorValue, będzie klasą
abstrakcyjną.
Ostatnią rzeczą, o której chciałbym wspomnieć , są obiekty
wyróźnione. Prawdziwą udręką podczas rozwijania aplikacji jest moment, gdy
podczas przeglądania zapytania do bazy danych, nie pamiętamy co oznacza wartość
1 czy 2 w polu Status – trzeba przegrzebywać się przez ustalenia, dokumentację,
czasem przeglądać zawartość bazy danych. Obiekty wyróżnione mają nam w tym
pomóc. Wystarczy zdefiniować dla klasy odpowiednie elementy <const> mówiące, że rekord z tabeli, np. VideoStatus o kluczu głównym 1 oznacza film wypożyczony
(RentedOut). Od tej pory będzie można
poprzez proste wywołanie w kodzie, wyciągnąć ten rekord, w celu użycia go w
zapytaniu, porównaniu, itp – ale o tym za chwilę.
Generowanie kodu
Mamy już więc plik
mapujący. W tym momencie będziemy potrzebować SoodaStubGen.exe. Zadaniem tego
programu jest wygenerowanie na podstawie przygotowanego przez nas pliku,
odpowiednich klas, z właściwościami, kolekcjami, obiektami wyróżnionymi itp.
oraz wygenerowanie pustych klas, w których umieszczać będziemy całą funkcjonalność.
SoodaStubGen ma wiele parametrów wywołania. Zamiast podawania długiej komendy
łatwiej będzie przygotować plik
*.soodaproject – mały pliku xml, w którym zostaną umieszczone te parametry. Dzięki
niemu będzie nam łatwiej przekonfigurowywać aplikację. Zawartość przykładowego
pliku wygląda np tak:
1 <?xml version="1.0" encoding="utf-8"?>
2 <sooda-project xmlns="http://www.sooda.org/schemas/SoodaProject.xsd">
3 <schema-file>SoodaSchema.xml</schema-file>
4 <language>c#</language>
5 <output-namespace>VideoRental</output-namespace>
6 <output-path>.</output-path>
7 <nullable-representation>SqlType</nullable-representation>
8 <not-null-representation>Raw</not-null-representation>
9 <with-indexers>false</with-indexers>
10 <with-typed-queries>true</with-typed-queries>
11 <embedded-schema-type>Binary</embedded-schema-type>
12 <external-projects>
13 <project type="vs2005" file="VideoRental.csproj"/>
14 </external-projects>
15 </sooda-project>
Najważniejsze z parametrów:
-
schema-file – nazwa pliku mapującego;
-
language – język w którym kod zostanie
wygenerowany (C# jest moim zdaniem najlepszym rozwiązaniem, gdyż tylko ten
język pozwala na zastosowanie zapytań typowanych, o których za chwilkę opowiem);
-
output-namespace – przestrzeń nazw dla
wygenerowanych klas;
-
output-path – miejsce, w którym pliki zostaną
zapisane;
-
nullable-representation – reprezentacja
właściwości mogących przyjmować wartość NULL. Możliwe jest użycie:
o
Boxed – w tym przypadku tracimy informacje o
typie, a właściwości będą zwracane jako typ object,
o
SqlType – użycie specjalnych typów z
przestrzeni nazw System.Data.SqlTypes,
o
Raw – dane są przetrzymywane jako
‘standardowe’ typy,
o
Nullable – użyte będą typy Nullable z .NET 2.0,
o
RawWithIsNull – rozszerzenie opcji Raw. Dla
każdej właściwości zostaną wygenerowane metody IsNull zwracające wartość prawda
/ fałsz.
- not-null-representation – podobnie jak wyżej,
tym razem dla właściwości nienullowalnych;
- with-indexers – włącza lub wyłącza, indeksery
dla wygenerowanych list;
-
with-typed-queries – włącza lub wyłącza
używanie zapytań typowanych;
-
external-projects – pozwala zaktualizować
pliki projektów VS2005 i VS2003 o wygenerowane pliki
Teraz musimy uruchomić SoodaStubGen podając jako parametr
nazwę naszego pliku *.soodaproject. Zalecane jest ustawienie w projekcie Visual
Studio w pre-build-event command line
wywołania SoodaStubGen np. w ten sposób:
$(SolutionDir)\Sooda\SoodaStubGen.exe
$(ProjectDir)VideoRental.soodaproject
Spowodouje to wywołanie SoodaStubGen z podktalogu Sooda, a
jako parametr należy podajć plik VideoRental.soodaproject z katalogu projektu Wygenerowanie
w ten sposób kodu, spowoduje wyświetlenie ostrzeżenia w Visual Studio, że plik
projektu został zmieniony i należy go ponownie załadować. Dwie rzeczy, na które
warto zwrócić uwagę:
1. SoodaStubGen
rozpoznaje, czy plik mapujący się zmienił – wówczas generuje jeszcze raz plik /
pliki szkieletowe. Nie rozpoznaje jednak czy zmienił się plik *.soodaproj. W
przypadku, gdy zmienimy coś w tym pliku, najlepiej jest zapisać plik
mapującytak, aby zmieniła się data ostatniej modyfikacji pliku.
2. SoodaStubGen
potrafi dodawać nowo wygenerowane pliki do pliku projektu Visual Studio –
jednak, co jest oczywist