1. Wprowadzenie do ORM NHibernate
Plik PDF z tego dokumentu jest dostępny pod nazwą |TUTAJ|.
Przykłady zawarte w niniejszym dokumencie są dostępne w |TUTAJ|.
Niniejszy dokument stanowi zwięzłe wprowadzenie do NHibernate, odpowiednika .NET w frameworku Java Hibernate. Pełne wprowadzenie można znaleźć w:
Tytuł: „NHibernate in Action”, autor: Pierre-Henri Kuaté, wydawnictwo: Manning, ISBN-13: 978-1932394924
ORM (Object Relational Mapper) to zbiór bibliotek umożliwiających programowi korzystającemu z bazy danych korzystanie z niej bez wysyłania jawnych poleceń SQL i bez znajomości specyfiki używanej bazy danych SGBD.
Wymagania wstępne
W strukturze [débutant-intermédiaire-avancé] niniejszy dokument znajduje się w sekcji [intermédiaire]. Jego zrozumienie wymaga spełnienia różnych warunków wstępnych, które można znaleźć w niektórych dokumentach mojego autorstwa:
- [Spring IoC], dostępny pod adresem [Spring IoC dla .NET (2005)]. Przedstawia podstawy odwrócenia kontroli (Inversion of Control) lub wstrzykiwania zależności (Dependency Injection) w frameworku Spring.Net [Spring.NET | Homepage ].
Na początku niektórych akapitów niniejszego dokumentu zamieszczono czasami wskazówki dotyczące lektury. Odnoszą się one do poprzednich dokumentów.
Narzędzia
Narzędzia wykorzystane w niniejszym studium przypadku są ogólnodostępne w Internecie. Są to (grudzień 2011 r.):
- Nhibernate 3.2 dostępny pod adresem URL [http://nhforge.org/Default.aspx]
- Spring.net 1.3.2 dostępny pod adresem URL [http://www.springframework.net]. Framework Spring.net oferuje bardzo bogaty zestaw funkcji. W niniejszym opracowaniu wykorzystamy jedynie bibliotekę, którą udostępnia on w celu ułatwienia korzystania z frameworka Nhibernate.
- Log4net 1.2.10 dostępny pod adresem URL [http://logging.apache.org/log4net]. Ten framework do logowania jest wykorzystywany przez Nhibernate.
- Nunit 2.5 dostępny pod adresem URL [http://www.nunit.org/]. Ten framework do testów jednostkowych jest odpowiednikiem dla .NET frameworka JUnit dla platformy Java.
- Sterownik ADO.NET 6.4.4 dla SGBD MySQL 5 dostępny pod adresem URL [http://dev.mysql.com/downloads/connector/net]
Wszystkie pliki DLL niezbędne do realizacji projektów w Visual Studio 2010 zostały zebrane w folderze [libnet4]:
![]() |
1.1. Miejsce pliku NHIBERNATE w warstwowej architekturze .NET
Aplikacja .NET korzystająca z bazy danych może być zorganizowana warstwowo w następujący sposób:
![]() |
Warstwa [dao] komunikuje się z warstwą SGBD za pośrednictwem warstw API i ADO.NET. Przypomnijmy główne metody tej warstwy API.
W trybie połączonym aplikacja:
- nawiązuje połączenie ze źródłem danych
- współpracuje ze źródłem danych w trybie odczytu/zapisu
- zamyka połączenie
Operacje te dotyczą głównie trzech interfejsów ADO.NET:
- IDbConnection, który zawiera właściwości i metody połączenia.
- IDbCommand, który zawiera właściwości i metody wykonanego polecenia SQL.
- IDataReader, który zawiera właściwości i metody wyniku polecenia SQL Select.
Interfejs IDbConnection
Służy do zarządzania połączeniem z bazą danych. Wśród metod M i właściwości P tego interfejsu znajdują się następujące:
Nazwa | Typ | Rola |
P | ciąg znaków służący do połączenia z bazą danych. Określa on wszystkie parametry niezbędne do nawiązania połączenia z konkretną bazą danych. | |
M | otwiera połączenie z bazą zdefiniowaną przez ConnectionString | |
M | zamyka połączenie | |
M | rozpoczyna transakcję. | |
P | stan połączenia: ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken |
Jeśli Connection jest klasą implementującą interfejs IDbConnection, nawiązanie połączenia może przebiegać w następujący sposób:
Interfejs IDbCommand
Służy do wykonania polecenia SQL lub procedury przechowywanej. Wśród metod M i właściwości P tego interfejsu znajdują się następujące:
Nazwa | Typ | Rola |
P | wskazuje, co należy wykonać – przyjmuje wartości z listy wyliczeniowej: - CommandType.Text: wykonuje polecenie SQL zdefiniowane we właściwości CommandText. Jest to wartość domyślna. - CommandType.StoredProcedure: wykonuje procedurę przechowywaną w bazie | |
P | - tekst polecenia SQL, które ma zostać wykonane, jeśli CommandType = CommandType.Text - nazwa procedury przechowywanej, która ma zostać wykonana, jeśli CommandType = CommandType.StoredProcedure | |
P | połączenie IDbConnection, które ma zostać użyte do wykonania zlecenia SQL | |
P | transakcja IDbTransaction, w ramach której ma zostać wykonane zlecenie SQL | |
P | lista parametrów zdefiniowanego polecenia SQL. Polecenie update articles set price=price*1.1 where id=@id posiada parametr @id. | |
M | w celu wykonania polecenia SQL Select. Otrzymujemy obiekt IDataReader reprezentujący wynik polecenia Select. | |
M | w celu wykonania polecenia SQL Update, Insert, Delete. Uzyskuje się liczbę wierszy, których dotyczy operacja (zaktualizowanych, wstawionych, usuniętych). | |
M | w celu wykonania polecenia SQL. Polecenie Select zwraca tylko jeden wynik, tak jak w przypadku: select count(*) from articles. | |
M | w celu utworzenia parametrów IDbParameter dla zdefiniowanego polecenia SQL. | |
M | pozwala zoptymalizować wykonanie zapytania z parametrami, gdy jest ono wykonywane wielokrotnie z różnymi parametrami. |
Jeśli Command jest klasą implementującą interfejs IDbCommand, wykonanie polecenia SQL bez transakcji będzie miało następującą postać:
Interfejs IDataReader
Służy do enkapsulacji wyników polecenia SQL Select. Obiekt IDataReader reprezentuje tabelę z wierszami i kolumnami, z której korzysta się sekwencyjnie: najpierw pierwszy wiersz, potem drugi itd. Wśród metod M i właściwości P tego interfejsu znajdują się następujące:
Nazwa | Typ | Rola |
P | liczba kolumn w tabeli IDataReader | |
M | GetName(i) zwraca nazwę kolumny nr i z tabeli IDataReader. | |
P | Element [i] reprezentuje kolumnę nr i w bieżącym wierszu tabeli IDataReader. | |
M | przechodzi do następnego wiersza tabeli IDataReader. Zwraca wartość logiczną True, jeśli odczyt się powiódł, a w przeciwnym razie – False. | |
M | zamyka tabelę IDataReader. | |
M | GetBoolean(i): zwraca wartość logiczną z kolumny nr i bieżącego wiersza tabeli IDataReader. Inne analogiczne metody to: GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString. | |
M | Getvalue(i): zwraca wartość z kolumny nr i bieżącego wiersza tabeli IDataReader jako typ object. | |
M | IsDBNull(i) zwraca True, jeśli kolumna nr i w bieżącym wierszu tabeli IDataReader nie ma wartości, co jest oznaczone wartością SQL NULL. |
Wykorzystanie obiektu IDataReader często wygląda następująco:
W poprzedniej architekturze,
![]() |
Złącze [ADO.NET] jest powiązane z SGBD. Zatem klasą implementującą interfejs [IDbConnection] jest:
- klasa [MySQLConnection] dla SGBD MySQL
- klasa [SQLConnection] dla SGBD i SQLServer
Warstwa [dao] jest zatem zależna od używanej warstwy SGBD. Niektóre frameworki (Linq, Ibatis.net, NHibernate) eliminują to ograniczenie, dodając dodatkową warstwę pomiędzy warstwą [dao] a łącznikiem [ADO.NET] używanego SGBD. W tym przypadku wykorzystamy framework [NHibernate].
![]() |
W powyższym przykładzie warstwa [dao] nie komunikuje się już z łącznikiem [ADO.NET], lecz z frameworkiem NHibernate, który udostępnia jej interfejs niezależny od używanego łącznika [ADO.NET]. Architektura ta pozwala na zmianę łącznika SGBD bez konieczności zmiany warstwy [dao]. W takim przypadku należy zmienić jedynie łącznik [ADO.NET].
1.2. Przykładowa baza danych
Aby pokazać, jak pracować z NHibernate, wykorzystamy następującą bazę danych MySQL [dbpam_nhibernate]:
![]() |
- W bazie [1] znajdują się trzy tabele:
- [employes]: tabela zawierająca dane pracownic żłobka
- [cotisations]: tabela zawierająca stawki składek na ubezpieczenie społeczne
- [indemnites]: tabela zawierająca informacje umożliwiające obliczenie wynagrodzenia pracownic
Tabela [employes]
![]() |
- w [2] znajduje się tabela pracowników, a w [3] – znaczenie jej pól
Zawartość tabeli mogłaby wyglądać następująco:
Tabela [cotisations]
![]() |
- w [4] – tabela składek, a w [5] – znaczenie jej pól
Zawartość tabeli mogłaby wyglądać następująco:
Tabela [indemnites]
![]() |
- w [6] znajduje się tabela odszkodowań, a w [7] – znaczenie jej pól
Zawartość tabeli mogłaby wyglądać następująco:
Eksport struktury bazy danych do pliku SQL daje następujący wynik:
Warto zwrócić uwagę, że w wierszach 6, 20 i 36 klucze główne ID mają atrybut autoincrement. Oznacza to, że MySQL automatycznie wygeneruje wartości kluczy głównych przy każdym dodaniu rekordu. Programista nie musi się tym martwić.
1.3. Projekt demonstracyjny w języku C#
Aby zaprezentować konfigurację i wykorzystanie NHibernate, zastosujemy następującą architekturę:
![]() |
Program konsolowy [1] będzie przetwarzał dane z poprzedniej bazy danych [2] za pośrednictwem frameworka [NHibernate] [3]. W związku z tym przedstawimy:
- pliki konfiguracyjne programu NHibernate
- API z NHibernate
Projekt w języku C# będzie wyglądał następująco:
![]() |
Elementy niezbędne do realizacji projektu to:
- w pliku [1] znajdują się pliki DLL, których potrzebuje projekt:
- [NHibernate]: plik DLL z frameworka NHibernate
- [MySql.Data]: DLL z łącznika ADO.NET z SGBD MySQL
- [log4net]: DLL z frameworka Log4net, umożliwiającego generowanie logów
- w formacie [2], klasy obrazów tabel bazy danych
- w [3], plik [App.config], który konfiguruje całą aplikację, w tym framework [NHibernate]
- w formacie [4], a także konsolowe aplikacje testowe
1.3.1. Konfiguracja połączenia z bazą danych
Wróćmy do architektury testowej:
![]() |
Jak widać powyżej, [NHibernate] musi mieć dostęp do bazy danych. W tym celu potrzebuje pewnych informacji:
- SGBD, który zarządza bazą (MySQL, SQLServer, Postgres, Oracle itp.). Większość SGBD dodała do języka SQL własne rozszerzenia. Znając SGBD, NHibernate może dostosować polecenia SQL, które wysyła do tego SGBD. NHibernate wykorzystuje pojęcie dialektu SQL.
- parametry połączenia z bazą danych (nazwa bazy, nazwa użytkownika będącego właścicielem połączenia, jego hasło)
Informacje te można umieścić w pliku konfiguracyjnym [App.config]. Oto przykład pliku, który będzie używany z bazą MySQL 5:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- sekcje konfiguracyjne -->
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
</configSections>
<!-- konfiguracja NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
<!-- Ta sekcja zawiera ustawienia konfiguracyjne log4net -->
<!-- NOTE IMPORTANTE: logi nie są domyślnie aktywne. Należy je aktywować programowo za pomocą instrukcji log4net.Config.XmlConfigurator.Configure();
! -->
<log4net>
<!-- Zdefiniuj moduł wyjściowy (miejsce, do którego będą kierowane logi) -->
<appender name="LogFileAppender" type="log4net.Appender.FileAppender, log4net">
<param name="File" value="log.txt" />
<param name="AppendToFile" value="false" />
<layout type="log4net.Layout.PatternLayout, log4net">
<param name="ConversionPattern" value="%d [%t] %-5p %c [%x] <%X{auth}> - %m%n" />
</layout>
</appender>
<appender name="LogDebugAppender" type="log4net.Appender.DebugAppender, log4net">
<layout type="log4net.Layout.PatternLayout, log4net">
<param name="ConversionPattern" value="%d [%t] %-5p %c [%x] <%X{auth}> - %m%n"/>
</layout>
</appender>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender, log4net">
<layout type="log4net.Layout.PatternLayout, log4net">
<param name="ConversionPattern" value="%d [%t] %-5p %c [%x] <%X{auth}> - %m%n"/>
</layout>
</appender>
<!-- Skonfiguruj kategorię główną, ustaw domyślny poziom priorytetu i dodaj moduły wyjściowe (miejsca, do których będą kierowane logi) -->
<root>
<priority value="INFO" />
<!--
<appender-ref ref="LogFileAppender" />
<appender-ref ref="LogDebugAppender"/>
-->
<appender-ref ref="ConsoleAppender"/>
</root>
<!-- Określ poziom dla niektórych konkretnych przestrzeni nazw -->
<!-- Poziom może mieć postać: ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF -->
<logger name="NHibernate">
<level value="INFO" />
</logger>
</log4net>
</configuration>
- wiersze 4–7: definiują sekcje konfiguracyjne w pliku [App.config]. Rozważmy wiersz 6:
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
Ten wiersz definiuje sekcję konfiguracyjną NHibernate w pliku [App.config]. Posiada dwa atrybuty: name i type.
- Atrybut [name] określa nazwę sekcji konfiguracyjnej. Sekcja ta musi być tutaj ograniczona tagami <name>...</name>, w tym przypadku <hibernate-configuration>...</hibernate-configuration> w wierszach 11–24.
- Atrybut [type=classe,DLL] określa nazwę klasy odpowiedzialnej za przetwarzanie sekcji zdefiniowanej przez atrybut [name], a także klasy DLL, w której znajduje się ta klasa. W tym przypadku klasa nosi nazwę [NHibernate.Cfg.ConfigurationSectionHandler] i znajduje się w klasie DLL [NHibernate.dll]. Przypomnijmy, że ta klasa DLL stanowi część referencji analizowanego projektu.
Rozważmy teraz sekcję konfiguracyjną pliku NHibernate:
<!-- konfiguracja NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
- wiersz 2: konfiguracja NHibernate znajduje się wewnątrz tagu <hibernate-configuration>. Atrybut xmlns (Xml NameSpace) określa wersję używaną do konfiguracji NHibernate. W miarę upływu czasu sposób konfiguracji NHibernate uległ zmianie. W tym przypadku używana jest wersja 2.2.
- wiersz 3: cała konfiguracja NHibernate znajduje się tutaj w tagu <session-factory> (wiersze 3 i 14). Sesja NHibernate jest narzędziem służącym do pracy z bazą danych zgodnie ze schematem:
- otwarcie sesji
- praca z bazą danych za pomocą metod API NHibernate
- zamknięcie sesji
Sesja jest tworzona przez factory, ogólny termin określający klasę zdolną do tworzenia obiektów. Wiersze 3–14 konfigurują tę klasę factory.
- wiersze 4, 6, 8, 9: konfigurują połączenie z docelową bazą danych. Najważniejsze informacje to nazwa używanego obiektu SGBD, nazwa bazy danych, identyfikator użytkownika oraz jego hasło.
- wiersz 4: określa dostawcę połączenia, czyli podmiot, do którego kierowane jest żądanie połączenia z bazą danych. Wartością właściwości [connection.provider] jest nazwa klasy NHibernate. Właściwość ta nie zależy od używanego obiektu SGBD.
- wiersz 6: sterownik ADO.NET, który ma być używany. Jest to nazwa klasy NHibernate wyspecjalizowanej dla danego SGBD, w tym przypadku MySQL. Wiersz 6 został skomentowany, ponieważ nie jest niezbędny.
- Wiersz 8: właściwość [dialect] określa dialekt SQL, który ma być używany z SGBD. W tym przypadku jest to dialekt SGBD MySQL.
Jeśli zmienimy plik na SGBD, to w jaki sposób ustalić, jaki jest jego dialekt NHibernate? Wróćmy do poprzedniego projektu C# i kliknijmy dwukrotnie na DLL [NHibernate] w zakładce [References]:
![]() |
- w [1], w zakładce [Explorateur d'objets] wyświetla się pewna liczba elementów DLL, w tym te, do których odwołuje się projekt.
- w [2], DLL [NHibernate]
- w [3], DLL oraz [NHibernate]. Znajdują się tam różne przestrzenie nazw (namespace), które zostały tam zdefiniowane.
- w pliku [4] znajduje się przestrzeń nazw [NHibernate.Dialect], w której znajdują się klasy definiujące różne dialekty SQL, z których można korzystać.
- w [5] klasa dialektu SGBD MySQL 5.
![]() |
- w [6], przestrzeń nazw klasy [MySqlDataDriver] użytej w wierszu 6 poniżej:
<!-- konfiguracja NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQLDialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
- wiersze 9–11: ciąg połączenia z bazą danych. Ciąg ten ma postać „param1=val1;param2=val2; ...”. Zestaw tak zdefiniowanych parametrów umożliwia sterownikowi klasy SGBD nawiązanie połączenia. Forma tego ciągu połączenia zależy od używanego modułu SGBD. Ciągi połączeń dla głównych modułów SGBD można znaleźć na stronie [http://www.connectionstrings.com/]. W tym przypadku ciąg „Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;” jest ciągiem połączeniowym dla serwera SGBD MySQL. Oznacza to, że:
- Server=localhost;: serwer SGBD znajduje się na tym samym komputerze co klient próbujący nawiązać połączenie
- Database=dbpam_nhibernate; : docelowa baza danych MySQL
- Uid=root;: użytkownikiem otwierającym połączenie jest użytkownik root
- Pwd=;: ten użytkownik nie ma hasła (szczególny przypadek w tym przykładzie)
- wiersz 12: właściwość [show_sql] określa, czy NHibernate ma wyświetlać w swoich logach polecenia SQL, które wysyła do bazy danych. Na etapie rozwoju warto ustawić tę właściwość na [true], aby dokładnie wiedzieć, co robi NHibernate.
- wiersz 13: aby zrozumieć tag <mapping>, wróćmy do architektury aplikacji:
![]() |
Gdyby program konsolowy był bezpośrednim klientem łącznika ADO.NET i chciał uzyskać listę pracowników, zleciłby łącznikowi wykonanie polecenia SQL Select, a w odpowiedzi otrzymałby obiekt typu IDataReader, który musiałby przetworzyć, aby uzyskać pierwotnie pożądaną listę pracowników.
W powyższym przykładzie program konsolowy jest klientem NHibernate, a NHibernate jest klientem łącznika ADO.NET. W dalszej części zobaczymy, że API z NHibernate umożliwi programowi konsolowemu zażądanie listy pracowników. NHibernate przetworzy to żądanie na polecenie SQL Select, które następnie zleci wykonaniu łącznikowi ADO.NET. Ten ostatni zwróci mu obiekt typu IDataReader. Na podstawie tego obiektu Nhibernate musi być w stanie utworzyć żądaną listę pracowników. Jest to możliwe dzięki odpowiedniej konfiguracji. Każdej tabeli w bazie danych przypisana jest klasa C#. W ten sposób na podstawie wierszy z tabeli [employes] zwróconych przez IDataReader, NHibernate będzie w stanie utworzyć listę obiektów reprezentujących pracowników i zwrócić ją do programu konsolowego. Te relacje między tabelami a klasami są tworzone w plikach konfiguracyjnych. NHibernate używa terminu „mapping” do określenia tych relacji.
Wróćmy do wiersza 13 poniżej:
<!-- konfiguracja NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
Wiersz 13 wskazuje, że pliki konfiguracyjne relacji tabele <--> klasy będą znajdować się w zestawie [pam-nhibernate-demos]. Zestaw to plik wykonywalny lub DLL powstały w wyniku kompilacji projektu. W tym przypadku pliki mapowania zostaną umieszczone w zestawie projektu przykładowego. Aby poznać nazwę tego zestawu, należy sprawdzić właściwości projektu:
![]() |
- w [1], właściwości projektu
- w zakładce [Application] [2], nazwa zestawu [3], który zostanie wygenerowany.
- ponieważ typem wyjściowym jest [Application console] [4], plik wygenerowany podczas kompilacji projektu będzie nosił nazwę [pam-nhibernate-demos.exe]. Gdyby typ wyjściowy brzmiał [Bibliothèque de classes] [5], plik wygenerowany podczas kompilacji projektu nosiłby nazwę [pam-nhibernate-demos.dll]
- plik assembly jest generowany w folderze [bin/Release] projektu [6].
Z powyższego wyjaśnienia wynika, że pliki tabel mapowania <--> klasy powinny znajdować się w pliku [pam-nhibernate-demos.exe] [6].
1.3.2. Konfiguracja pliku 'u mapowania tabel <--> klas
Wróćmy do architektury analizowanego projektu:
![]() |
- w pliku [1] program konsolowy korzysta z metod zawartych w pliku API należącym do frameworka NHibernate. Te dwa bloki wymieniają między sobą obiekty.
- W [2], NHibernate korzysta z API za pośrednictwem łącznika .NET. Wysyła polecenia SQL do docelowego SGBD.
Program konsolowy będzie operował na obiektach odzwierciedlających tabele bazy danych. W tym projekcie obiekty te oraz powiązania łączące je z tabelami bazy danych zostały umieszczone w poniższym folderze [Entites]:
![]() |
- każda tabela bazy danych odpowiada jednej klasie oraz plikowi mapującemu te dwa elementy
Tabela | Klasa | Mapowanie |
składki | Cotisations.cs | Cotisations.hbm.xml |
pracownicy | Employe.cs | Employe.hbm.xml |
odszkodowania | Indemnites.cs | Indemnites.hbm.xml |
1.3.2.1. Mapowanie tabeli [cotisations]
Rozważmy tabelę [cotisations]:
![]() |
|
Wiersz tej tabeli może być zawarty w obiekcie typu [Cotisations.cs] w następującej postaci:
namespace PamNHibernateDemos {
public class Cotisations {
// właściwości automatyczne
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual double CsgRds { get; set; }
public virtual double Csgd { get; set; }
public virtual double Secu { get; set; }
public virtual double Retraite { get; set; }
// konstruktorzy
public Cotisations() {
}
// ToString
public override string ToString() {
return string.Format("[{0}|{1}|{2}|{3}]", CsgRds, Csgd, Secu, Retraite);
}
}
}
Dla każdej kolumny tabeli [cotisations] utworzono właściwość automatyczną. Każda z tych właściwości musi być zadeklarowana jako wirtualna (virtual), ponieważ klasa NHibernate będzie dziedziczyć tę klasę i nadpisywać (override) jej właściwości. Dlatego też właściwości te muszą być wirtualne.
Warto zauważyć, że w wierszu 1 klasa należy do przestrzeni nazw [PamNHibernateDemos].
Plik mapowania [Cotisations.hbm.xml] łączący tabelę [cotisations] z klasą [Cotisations] ma następującą postać:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
<class name="Cotisations" table="COTISATIONS">
<id name="Id" column="ID" unsaved-value="0">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="CsgRds" column="CSGRDS"/>
<property name="Csgd" column="CSGD"/>
<property name="Retraite" column="RETRAITE"/>
<property name="Secu" column="SECU"/>
</class>
</hibernate-mapping>
- plik mapowania jest plikiem XML zdefiniowanym wewnątrz tagu <hibernate-mapping> (wiersze 2 i 14)
- wiersz 4: tag <class> łączy tabelę bazy danych z klasą. W tym przypadku tabela [COTISATIONS] (atrybut table) i klasa [Cotisations] (atrybut name). W pliku .NET klasa musi być zdefiniowana poprzez jej pełną nazwę (wraz z przestrzenią nazw) oraz poprzez assembly, w którym się znajduje. Te dwie informacje podano w wierszu 3. Pierwszą z nich (przestrzeń nazw) można znaleźć w definicji klasy. Druga (assembly) to nazwa zestawu projektu. Wskazaliśmy już, jak znaleźć tę nazwę.
- wiersze 5–7: tag <id> służy do zdefiniowania mapowania klucza głównego tabeli [cotisations].
- wiersz 5: atrybut name wskazuje pole klasy [Cotisations], które będzie przechowywać klucz główny tabeli [cotisations]. Atrybut column określa kolumnę tabeli [cotisations], która pełni funkcję klucza głównego. Atrybut unsaved-value służy do zdefiniowania klucza głównego, który nie został jeszcze wygenerowany. Ta wartość pozwala NHibernate określić, w jaki sposób zapisać obiekt [Cotisations] w tabeli [cotisations]. Jeśli obiekt ten ma pole Id=0, zostanie wykonana operacja SQL INSERT, w przeciwnym razie zostanie wykonana operacja SQL UPDATE. Wartość unsaved-value zależy od typu pola Id w klasie [Cotisations]. W tym przypadku jest to typ int, a domyślną wartością typu int jest 0. Obiekt typu [Cotisations], który nie został jeszcze zapisany (a zatem nie ma klucza głównego), będzie miał więc w polu Id wartość 0. Gdyby pole Id było typu Object lub pochodnego, zapisano by unsaved-value=null.
- wiersz 6: gdy NHibernate ma zapisać obiekt [Cotisations] z polem Id=0, musi wykonać w bazie danych operację INSERT, w trakcie której musi uzyskać wartość klucza głównego rekordu. Większość obiektów SGBD posiada własną metodę automatycznego generowania tej wartości. Tag <generator> służy do zdefiniowania mechanizmu, który ma być użyty do generowania klucza głównego. Tag <generator class="native"> wskazuje, że należy użyć domyślnego mechanizmu obiektu SGBD, z którego korzystamy. W punkcie 1.2 widzieliśmy, że klucze główne w naszych trzech tabelach MySQL miały atrybut autoincrement. Podczas operacji INSERT, NHibernate nie poda wartości do kolumny ID dodawanego rekordu, pozostawiając wygenerowanie tej wartości mechanizmowi MySQL.
- wiersz 8: tag <version> służy do zdefiniowania kolumny tabeli (a także odpowiadającego jej pola klasy), która umożliwia „wersjonowanie” rekordów. Początkowo wartość wersji wynosi 1. Jest ona zwiększana przy każdej operacji UPDATE. Z drugiej strony, każda operacja UPDATE lub DELETE jest wykonywana z filtrem WHERE ID= id AND VERSION=v1. Użytkownik może zatem zmodyfikować lub usunąć obiekt tylko wtedy, gdy posiada jego właściwą wersję. W przeciwnym razie przez NHibernate zgłaszany jest wyjątek.
- wiersz 9: tag <property> służy do zdefiniowania mapowania zwykłej kolumny (ani klucza głównego, ani kolumny wersji). Zatem wiersz 9 wskazuje, że kolumna CSGRDS z tabeli [COTISATIONS] jest powiązana z właściwością CsgRds klasy [Cotisations].
1.3.2.2. Mapowanie tabeli [indemnites]
Rozważmy tabelę [indemnites]:
![]() |
|
Wiersz tej tabeli można zawrzeć w obiekcie typu [Indemnites] w następujący sposób:
namespace PamNHibernateDemos {
public class Indemnites {
// właściwości automatyczne
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual int Indice { get; set; }
public virtual double BaseHeure { get; set; }
public virtual double EntretienJour { get; set; }
public virtual double RepasJour { get; set; }
public virtual double IndemnitesCp { get; set; }
// konstruktorzy
public Indemnites() {
}
// tożsamość
public override string ToString() {
return string.Format("[{0}|{1}|{2}|{3}|{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
}
}
}
Plik mapowania tabeli [indemnites] <--> klasa [Indemnites] mógłby wyglądać następująco (Indemnites.hbm.xml):
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
<class name="Indemnites" table="INDEMNITES">
<id name="Id" column="ID" unsaved-value="0">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="Indice" column="INDICE" unique="true"/>
<property name="BaseHeure" column="BASE_HEURE" />
<property name="EntretienJour" column="ENTRETIEN_JOUR" />
<property name="RepasJour" column="REPAS_JOUR" />
<property name="IndemnitesCp" column="INDEMNITES_CP" />
</class>
</hibernate-mapping>
Nie ma tu nic nowego w porównaniu z plikiem mapowania omówionym wcześniej. Jedyna różnica znajduje się w wierszu 9. Atrybut unique="true" wskazuje, że w tabeli [indemnites] istnieje ograniczenie unikalności dla kolumny [INDICE]: nie mogą istnieć dwa wiersze o tej samej wartości w kolumnie [INDICE].
1.3.2.3. Mapowanie tabeli [employes]
Rozważmy tabelę [employes]:
![]() |
|
Nowością w porównaniu z poprzednimi tabelami jest obecność klucza obcego: kolumna [INDEMNITE_ID] stanowi klucz obcy do kolumny [ID] w tabeli [INDEMNITES]. Pole to odwołuje się do wiersza w tabeli [INDEMNITES], który ma być wykorzystany do obliczenia wynagrodzeń pracownika.
Klasa [Employe] przedstawiająca tabelę [employes] mogłaby wyglądać następująco:
namespace PamNHibernateDemos {
public class Employe {
// właściwości automatyczne
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual string SS { get; set; }
public virtual string Nom { get; set; }
public virtual string Prenom { get; set; }
public virtual string Adresse { get; set; }
public virtual string Ville { get; set; }
public virtual string CodePostal { get; set; }
public virtual Indemnites Indemnites { get; set; }
// konstruktorzy
public Employe() {
}
// ToString
public override string ToString() {
return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
}
}
}
Plik mapowania [Employe.hbm.xml] mógłby wyglądać następująco:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
<class name="Employe" table="EMPLOYES">
<id name="Id" column="ID" unsaved-value="0">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="SS" column="SS"/>
<property name="Nom" column="NOM"/>
<property name="Prenom" column="PRENOM"/>
<property name="Adresse" column="ADRESSE"/>
<property name="Ville" column="VILLE"/>
<property name="CodePostal" column="CP"/>
<many-to-one name="Indemnites" column="INDEMNITE_ID" cascade="save-update" lazy="false"/>
</class>
</hibernate-mapping>
Nowością jest pojawienie się w wierszu 15 nowego tagu: <many-to-one>. Ten tag służy do mapowania kolumny klucza obcego [INDEMNITE_ID] z tabeli [EMPLOYES] na właściwość [Indemnites] klasy [Employe]:
namespace PamNHibernateDemos {
public class Employe {
// właściwości automatyczne
..
public virtual Indemnites Indemnites { get; set; }
...
}
}
Tabela [EMPLOYES] posiada klucz obcy [INDEMNITE_ID], który odwołuje się do kolumny [ID] w tabeli [INDEMNITES]. Wiele (many) wierszy tabeli [EMPLOYES] może odnosić się do tego samego (one) wiersza tabeli [INDEMNITES]. Stąd nazwa tagu <many-to-one>. Tag ten posiada tutaj następujące atrybuty:
- column: wskazuje nazwę kolumny w tabeli [EMPLOYES], która jest kluczem obcym w tabeli [INDEMNITES]
- name: wskazuje właściwość klasy [Employe] powiązaną z tą kolumną. Typ tej właściwości musi być klasą powiązaną z tabelą docelową klucza obcego, w tym przypadku tabelą [INDEMNITES]. Wiemy, że jest to klasa [Indemnites], opisana już wcześniej. Odzwierciedla to wiersz 5 powyżej. Oznacza to, że gdy NHibernate pobierze z bazy obiekt [Employe], pobierze również powiązany z nim obiekt [Indemnites].
- kaskada: ten atrybut może przyjmować różne wartości:
- save-update: operacja wstawienia (save) lub aktualizacji (update) obiektu [Employe] musi zostać przeniesiona na zawarty w nim obiekt [Indemnites].
- delete: usunięcie obiektu [Employe] musi zostać przeniesione na zawarty w nim obiekt [Indemnites].
- all: propaguje operacje wstawiania (save), aktualizacji (update) i usuwania (delete).
- none: nie propaguje żadnych operacji
Na koniec przypomnijmy konfigurację obiektu NHibernate w pliku [App.config]:
<!-- konfiguracja NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
Wiersz 13 wskazuje, że pliki mapowania *.hbm.xml będą znajdować się w zestawie [pam-nhibernate-demos]. Nie jest to ustawione domyślnie. Należy to skonfigurować w projekcie C#:
![]() |
- w [1] wybieramy właściwości pliku mapowania
- na [2], a akcją generowania musi być [Ressource incorporée] [3]. Oznacza to, że podczas generowania projektu plik mapowania musi zostać włączony do wygenerowanego zestawu.
1.4. API z NHibernate
Wróćmy do architektury naszego przykładowego projektu:
![]() |
W poprzednich akapitach skonfigurowaliśmy plik NHibernate na dwa sposoby:
- w pliku [App.config] skonfigurowaliśmy połączenie z bazą danych
- dla każdej tabeli w bazie zapisaliśmy klasę odpowiadającą tej tabeli oraz plik mapowania, który umożliwia przejście z klasy do tabeli i odwrotnie.
Pozostaje nam jeszcze zapoznać się z metodami oferowanymi przez NHibernate do operowania danymi z bazy: wstawianie, aktualizacja, usuwanie, wyświetlanie listy.
1.4.1. Obiekt SessionFactory
Każda operacja NHibernate odbywa się w ramach sesji. Typowa sekwencja operacji NHibernate wygląda następująco:
- otwarcie sesji NHibernate
- rozpoczęcie transakcji w sesji
- wykonywanie operacji związanych z trwałością danych w ramach sesji (Load, Get, Find, CreateQuery, Save, SaveOrUpdate, Delete)
- zatwierdź (commit) lub cofnij (rollback) transakcję
- zamknięcie sesji NHibernate
Sesję uzyskuje się z fabryki typu [SessionFactory]. Jest to fabryka skonfigurowana za pomocą tagu <session-factory> w pliku konfiguracyjnym [App.config]:
<!-- konfiguracja NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
W kodzie C# obiekt SessionFactory można uzyskać w następujący sposób:
ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();
Klasa Configuration jest klasą frameworku NHibernate. Powyższa instrukcja wykorzystuje sekcję konfiguracyjną NHibernate w pliku [App.config]. Uzyskany obiekt [ISessionFactory] zawiera wówczas:
- informacje niezbędne do nawiązania połączenia z docelową bazą danych
- pliki mapowania między tabelami bazy danych a klasami trwałymi obsługiwanymi przez NHibernate.
1.4.2. Sesja NHibernate
Po utworzeniu obiektu SessionFactory (odbywa się to jednorazowo) można uzyskać sesje umożliwiające wykonywanie operacji trwałości NHibernate. Typowy kod wygląda następująco:
try{
// logowanie
using (ISession session = sessionFactory.OpenSession())
{
// rozpoczęcie transakcji
using (ITransaction transaction = session.BeginTransaction())
{
........................ opérations de persistance
// zatwierdzenie transakcji
transaction.Commit();
}
}
}catch (Exception ex){
....
}
- wiersz 3: sesja jest tworzona na podstawie SessionFactory w klauzuli using. Po wyjściu z klauzuli using sesja zostanie automatycznie zamknięta. Bez klauzuli using konieczne byłoby jawne zamknięcie sesji (session.Close()).
- wiersz 6: operacje trwałości będą wykonywane w ramach transakcji. Albo wszystkie zakończą się powodzeniem, albo żadna z nich nie zakończy się powodzeniem. Wewnątrz klauzuli using, transakcja jest zatwierdzana za pomocą Commit (wiersz 10). Jeśli w ramach transakcji operacja zapisu wygeneruje wyjątek, transakcja zostanie automatycznie unieważniona przez instrukcję Rollback po zakończeniu instrukcji using.
- Blok try/catch w wierszach 1 i 13 pozwala przechwycić ewentualny wyjątek wygenerowany przez kod wewnątrz bloku try (sesja, transakcja, operacja zapisu).
1.4.3. Interfejs ISession
Przedstawiamy teraz niektóre metody interfejsu ISession zaimplementowanego przez sesję NHibernate:
rozpoczyna transakcję w sesji ITransaction tx=session.BeginTransaction(); | |
opróżnia sesję. Obiekty, które zawierała, stają się odłączone. session.Clear(); | |
zamyka sesję. Obiekty w niej zawarte są synchronizowane z bazą danych. Ta operacja synchronizacji jest również wykonywana na końcu transakcji. Ten ostatni przypadek jest najczęstszy. session.Close(); | |
tworzy zapytanie HQL (Hibernate Query Language) do późniejszego wykonania. IQuery query=session.createQuery("select e from Employe e); | |
usuwa obiekt. Obiekt ten może należeć do sesji (przypisany) lub nie (nieprzypisany). Podczas synchronizacji sesji z bazą danych na tym obiekcie zostanie wykonana operacja SQL DELETE. // ładujemy pracownika z BD Pracownik e = session.Get<Pracownik>(143); // usuwamy go session.Delete(e); | |
wymusza synchronizację sesji z bazą danych. Zawartość sesji nie ulega zmianie. session.Flush(); | |
pobiera z bazy obiekt T o kluczu głównym id. Jeśli obiekt ten nie istnieje, zwraca wskaźnik null. // ładujemy pracownika z BD Pracownik e = session.Get<Pracownik>(143); | |
umieszcza obiekt obj w sesji. Obiekt ten nie ma klucza głównego przed operacją Save. Po Save już go posiada. Podczas synchronizacji sesji w bazie zostanie wykonana operacja SQL INSERT. // tworzymy pracownika Pracownik e = new Pracownik(){...}; // zapisujemy go e = session.Save(e); | |
wykonuje operację Save, jeśli obiekt nie ma klucza głównego, lub operację Update, jeśli już go posiada. | |
aktualizuje obiekt obj w bazie danych. Następnie w bazie wykonywana jest operacja SQL UPDATE. // ładujemy pracownika z operacji BD Pracownik e = session.Get<Pracownik>(143); // zmieniamy jego imię e.Nom = ...; // aktualizujemy go w bazie danych session.Update(e); |
1.4.4. Interfejs IQuery
Interfejs IQuery umożliwia wysyłanie zapytań do bazy danych w celu pobrania danych. Widzieliśmy już, jak utworzyć jego instancję:
Parametrem metody createQuery jest zapytanie HQL (Hibernate Query Language) – język analogiczny do języka SQL, ale wykorzystujący klasy zamiast tabel. Powyższe zapytanie zwraca listę wszystkich pracowników. Oto kilka przykładów zapytań HQL:
select e from Employe e where e.Nom like 'A%'
select e from Employe order by e.Nom asc
select e from Employe e where e.Indemnites.Indice=2
Poniżej przedstawiamy niektóre metody interfejsu IQuery:
zwraca wynik zapytania w postaci listy obiektów typu T IList<Pracownik> pracownicy = session.createQuery("select e from Pracownik e order by e.Nom asc").List<Pracownik>(); | |
zwraca wynik zapytania w postaci listy, gdzie każdy element listy reprezentuje wiersz wyniku z Select w postaci tablicy obiektów. IList wiersze = session.createQuery("select e.Nom, e.Prenom, e.SS from Pracownik").List(); lignes[i][j] reprezentuje kolumnę j w wierszu i w typie object. Tak więc lignes[10][1] jest typem object reprezentującym imię osoby. Zazwyczaj konieczne jest przeprowadzenie konwersji typów, aby pobrać dane w ich dokładnym typie. | |
zwraca pierwszy obiekt z wyniku zapytania Employe e=session.createQuery("select e from Employe e where e.Nom='MARTIN'").UniqueResult<Employe>(); |
Zapytanie HQL można skonfigurować w następujący sposób:
W zapytaniu HQL w wierszu 3, :num jest parametrem, który musi otrzymać wartość przed wykonaniem zapytania. W powyższym przykładzie wykorzystano do tego celu metodę SetString. Interfejs IQuery udostępnia różne metody typu Set służące do przypisywania wartości do parametrów:
- - SetBoolean(string name, bool value)
- - SetSingle(nazwa typu string, wartość typu single)
- - SetDouble(nazwa typu string, wartość typu double)
- - SetInt32(nazwa typu łańcuchowego, wartość typu int32)
- ..
1.5. Kilka przykładów kodu
Poniższe przykłady opierają się na architekturze omówionej wcześniej i przedstawionej poniżej. Bazą danych jest baza danych MySQL [dbpam_nhibernate], która również została przedstawiona. Przykłady to programy konsolowe [1] wykorzystujące framework NHibernate [3] do obsługi bazy danych [2].
![]() |
Projekt w języku C#, w ramach którego znajdują się poniższe przykłady, to ten, który został już przedstawiony:
![]() |
- w [1] znajdują się pliki DLL, których potrzebuje projekt:
- [NHibernate]: plik DLL z frameworka NHibernate
- [MySql.Data]: plik DLL z łącznika ADO.NET dla SGBD MySQL 5
- [log4net]: DLL z narzędzia służącego do generowania logów
- w [2], klasach obrazów tabel bazy danych
- w [3], plik [App.config], który konfiguruje całą aplikację, w tym framework [NHibernate]
- w formacie [4], a także konsolowe aplikacje testowe. To właśnie je przedstawimy częściowo.
1.5.1. Pobieranie zawartości bazy danych
Program [ShowDataBase.cs] umożliwia wyświetlenie zawartości bazy danych:
using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;
namespace PamNHibernateDemos
{
public class ShowDataBase
{
private static ISessionFactory sessionFactory = null;
// program główny
static void Main(string[] args)
{
// inicjalizacja fabryki NHibernate
sessionFactory = new Configuration().Configure().BuildSessionFactory();
try
{
// wyświetlanie zawartości bazy
Console.WriteLine("Affichage base -------------------------------------");
ShowDataBase1();
}
catch (Exception ex)
{
// wyświetlanie wyjątku
Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
}
finally
{
if (sessionFactory != null)
{
sessionFactory.Close();
}
}
// oczekiwanie na wprowadzenie danych z klawiatury
Console.ReadLine();
}
// test1
static void ShowDataBase1()
{
// logowanie
using (ISession session = sessionFactory.OpenSession())
{
// rozpoczęcie transakcji
using (ITransaction transaction = session.BeginTransaction())
{
// pobieranie listy pracowników
IList<Employe> employes = session.CreateQuery(@"select e from Employe e order by e.Nom asc").List<Employe>();
// wyświetlanie listy
Console.WriteLine("--------------- liste des employés");
foreach (Employe e in employes)
{
Console.WriteLine(e);
}
// pobieranie listy dodatków
IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i order by i.Indice asc").List<Indemnites>();
// wyświetlanie listy
Console.WriteLine("--------------- liste des indemnités");
foreach (Indemnites i in indemnites)
{
Console.WriteLine(i);
}
// pobieranie listy składek
Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
Console.WriteLine("--------------- tableau des taux de cotisations");
Console.WriteLine(cotisations);
// zatwierdź transakcję
transaction.Commit();
}
}
}
}
}
Objaśnienia:
- wiersz 19: tworzony jest obiekt SessionFactory. To właśnie on pozwoli nam uzyskać obiekty Session.
- wiersz 24: wyświetlana jest zawartość bazy
- wiersze 31–37: obiekt SessionFactory jest zamykany w klauzuli finally obiektu try.
- wiersz 43: metoda wyświetlająca zawartość bazy
- wiersz 46: uzyskuje się Session z SessionFactory.
- wiersz 49: rozpoczyna się transakcja
- wiersz 52: zapytanie HQL w celu pobrania listy pracowników. Ze względu na klucz obcy łączący encję Employe z encją Indemnite, dla każdego pracownika otrzymamy jego odszkodowanie.
- wiersz 60: zapytanie HQL w celu uzyskania listy odszkodowań.
- wiersz 68: zapytanie HQL w celu uzyskania pojedynczego wiersza z tabeli składek.
- wiersz 72: koniec transakcji
- wiersz 73: koniec użycia Itransaction z wiersza 49 – transakcja zostaje automatycznie zamknięta
- wiersz 74: koniec instrukcji „using Isession” z wiersza 46 – sesja zostaje automatycznie zamknięta.
Wyświetlony ekran:
Warto zwrócić uwagę na wiersze 3 i 4, gdzie po wyszukaniu pracownika uzyskano również informacje o jego odszkodowaniu.
1.5.2. Wprowadzanie danych do bazy
Program [FillDataBase.cs] umożliwia wprowadzanie danych do bazy:
using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;
namespace PamNHibernateDemos
{
public class FillDataBase
{
private static ISessionFactory sessionFactory = null;
// program główny
static void Main(string[] args)
{
// inicjalizacja fabryki NHibernate
sessionFactory = new Configuration().Configure().BuildSessionFactory();
try
{
// usuwanie zawartości bazy danych
Console.WriteLine("Effacement base -------------------------------------");
ClearDataBase1();
Console.WriteLine("Affichage base -------------------------------------");
ShowDataBase();
Console.WriteLine("Remplissage base -------------------------------------");
FillDataBase1();
Console.WriteLine("Affichage base -------------------------------------");
ShowDataBase();
}
catch (Exception ex)
{
// wyświetlanie wyjątku
Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
}
finally
{
if (sessionFactory != null)
{
sessionFactory.Close();
}
}
// oczekiwanie na wprowadzenie danych z klawiatury
Console.ReadLine();
}
// test1
static void ShowDataBase()
{
// zobacz poprzedni przykład
}
// ClearDataBase1
static void ClearDataBase1()
{
// logowanie
using (ISession session = sessionFactory.OpenSession())
{
// rozpoczęcie transakcji
using (ITransaction transaction = session.BeginTransaction())
{
// pobieramy listę pracowników
IList<Employe> employes = session.CreateQuery(@"select e from Employe e").List<Employe>();
// usuwanie wszystkich pracowników
Console.WriteLine("--------------- suppression des employés associés");
foreach (Employe e in employes)
{
session.Delete(e);
}
// pobieranie listy dodatków
IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i").List<Indemnites>();
// usuwanie świadczeń
Console.WriteLine("--------------- suppression des indemnités");
foreach (Indemnites i in indemnites)
{
session.Delete(i);
}
// pobieranie listy składek
Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
Console.WriteLine("--------------- suppression des taux de cotisations");
if (cotisations != null)
{
session.Delete(cotisations);
}
// zatwierdź transakcję
transaction.Commit();
}
}
}
// FillDataBase
static void FillDataBase1()
{
// zalogowanie
using (ISession session = sessionFactory.OpenSession())
{
// rozpoczęcie transakcji
using (ITransaction transaction = session.BeginTransaction())
{
// tworzymy dwa dodatki
Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
Indemnites i2 = new Indemnites() { Id = 0, Indice = 2, BaseHeure = 2.1, EntretienJour = 2.1, RepasJour = 3.1, IndemnitesCp = 15 };
// tworzy się dwóch pracowników
Employe e1 = new Employe() { Id = 0, SS = "254104940426058", Nom = "Jouveinal", Prenom = "Marie", Adresse = "5 rue des oiseaux", Ville = "St Corentin", CodePostal = "49203", Indemnites = i1 };
Employe e2 = new Employe() { Id = 0, SS = "260124402111742", Nom = "Laverti", Prenom = "Justine", Adresse = "La Brûlerie", Ville = "St Marcel", CodePostal = "49014", Indemnites = i2 };
// tworzy się stawki składek
Cotisations cotisations = new Cotisations() { Id = 0, CsgRds = 3.49, Csgd = 6.15, Secu = 9.39, Retraite = 7.88 };
// zapisano wszystko
session.Save(e1);
session.Save(e2);
session.Save(cotisations);
// zatwierdzenie transakcji
transaction.Commit();
}
}
}
}
}
Objaśnienia
- wiersz 19: tworzony jest program SessionFactory
- wiersze 37–43: jest on zamykany w klauzuli finally programu try
- wiersz 55: metoda ClearDataBase1, która opróżnia bazę danych. Zasada działania jest następująca:
- pobieramy wszystkich pracowników (wiersz 64) do listy
- usuwamy ich pojedynczo (wiersze 67–70)
- wiersz 93: metoda FillDataBase1 wstawia pewne dane do bazy danych
- tworzymy dwa rekordy Indemnites (wiersze 102, 103)
- tworzymy dwóch pracowników z tymi dodatkami (wiersze 105, 106)
- w wierszu 108 tworzony jest obiekt Cotisations.
- wiersze 110, 111: obie encje typu „Pracownik” są zapisywane w bazie danych
- wiersz 112: z kolei zapisano w bazie danych encję „Składki”
- Można się dziwić, że encje Indemnités z wierszy 102 i 103 nie zostały zapisane. W rzeczywistości zostały one zapisane razem z encjami Employe. Aby to zrozumieć, należy powrócić do mapowania jednostki Employe:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
<class name="Employe" table="EMPLOYES">
<id name="Id" column="ID" unsaved-value="0">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="SS" column="SS"/>
<property name="Nom" column="NOM"/>
<property name="Prenom" column="PRENOM"/>
<property name="Adresse" column="ADRESSE"/>
<property name="Ville" column="VILLE"/>
<property name="CodePostal" column="CP"/>
<many-to-one name="Indemnites" column="INDEMNITE_ID" cascade="save-update" lazy="false"/>
</class>
</hibernate-mapping>
Wiersz 15, który mapuje relację klucza obcego łączącą encję Employe z encją Indemnites, posiada atrybut cascade= „save-update”, co powoduje, że operacje „save ” oraz „update” dla encji Employe są propagowane do encji wewnętrznej Indemnites.
Wyświetlony ekran:
Effacement base -------------------------------------
--------------- suppression des employés et des indemnités associées
--------------- suppression des indemnités restantes
--------------- suppression des taux de cotisations
Affichage base -------------------------------------
--------------- liste des employés
--------------- liste des indemnités
--------------- tableau des taux de cotisations
Remplissage base -------------------------------------
Affichage base -------------------------------------
--------------- liste des employés
[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[2|2,1|2,1|3,1|15]]
[260124402111742|Laverti|Justine|La Brûlerie|St Marcel|49014|[1|1,93|2|3|12]]
--------------- liste des indemnités
[1|1,93|2|3|12]
[2|2,1|2,1|3,1|15]
--------------- tableau des taux de cotisations
[3,49|6,15|9,39|7,88]
1.5.3. Wyszukiwanie pracownika
Program [Program.cs] zawiera różne metody ilustrujące dostęp do danych w bazie oraz operacje na nich. Przedstawiamy kilka z nich.
Metoda [FindEmployee] umożliwia wyszukanie pracownika na podstawie jego numeru ubezpieczenia społecznego:
// FindEmployee
static void FindEmployee() {
try {
// zalogowanie
using (ISession session = sessionFactory.OpenSession()) {
// rozpoczęcie transakcji
using (ITransaction transaction = session.BeginTransaction()) {
// wyszukiwanie pracownika na podstawie jego numeru SS
String numSecu = "254104940426058";
IQuery query = session.CreateQuery(@"select e from Employe e where e.SS=:numSecu");
Employe employe = query.SetString("numSecu", numSecu).UniqueResult<Employe>();
if (employe != null) {
Console.WriteLine("Employe[" + numSecu + "]=" + employe);
} else {
Console.WriteLine("Employe[" + numSecu + "] non trouvé...");
}
numSecu = "xx";
employe = query.SetString("numSecu", numSecu).UniqueResult<Employe>();
if (employe != null) {
Console.WriteLine("Employe[" + numSecu + "]=" + employe);
} else {
Console.WriteLine("Employe[" + numSecu + "] non trouvé...");
}
// zatwierdzenie transakcji
transaction.Commit();
}
}
} catch (Exception e) {
Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
}
}
Objaśnienia
- wiersz 10: zapytanie Select skonfigurowane przez metodę numSecu, które ma zostać wykonane
- wiersz 11: przypisanie wartości do parametru numSecu oraz wykonanie metody UniqueResult w celu uzyskania pojedynczego wyniku.
Uzyskany wynik na ekranie:
Recherche d'un employé -------------------------------------
Employe[254104940426058]=[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[2|2,1|2,1|3,1|15]]
Employe[xx] non trouvé...
1.5.4. Wstawianie nieprawidłowych encji
Poniższa metoda próbuje zapisać niezinicjalizowany element [Employe].
// SaveEmptyEmployee
static void SaveEmptyEmployee() {
try {
// zalogowanie
using (ISession session = sessionFactory.OpenSession()) {
// rozpoczęcie transakcji
using (ITransaction transaction = session.BeginTransaction()) {
// tworzy się pustego pracownika
Employe e = new Employe();
// tworzy się nieistniejące świadczenie
Indemnites i = new Indemnites() { Id = 0, Indice = 3, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
// które przypisujemy do pracownika
e.Indemnites = i;
// zapisano pracownika, pozostawiając pozostałe pola puste
session.Save(e);
// zatwierdza się transakcję
transaction.Commit();
}
}
} catch (Exception e) {
Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
}
}
Wyjaśnienia
Przypomnijmy kod klasy [Employe]:
namespace PamNHibernateDemos {
public class Employe {
// automatyczne właściwości
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual string SS { get; set; }
public virtual string Nom { get; set; }
public virtual string Prenom { get; set; }
public virtual string Adresse { get; set; }
public virtual string Ville { get; set; }
public virtual string CodePostal { get; set; }
public virtual Indemnites Indemnites { get; set; }
// konstruktory
public Employe() {
}
// ToString
public override string ToString() {
return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
}
}
}
Niezinicjalizowany obiekt [Employe] będzie miał wartość null we wszystkich polach typu string. Podczas wstawiania rekordu do tabeli [employes] obiekt NHibernate pozostawi puste kolumny odpowiadające tym polom. Jednak w tabeli [employes] wszystkie kolumny mają atrybut „not null”, co uniemożliwia istnienie kolumn bez wartości. Sterownik ADO.NET wygeneruje wówczas wyjątek:
1.5.5. Tworzenie dwóch dodatków o tym samym indeksie w ramach jednej transakcji
W tabeli [indemnites] kolumna [indice] została zadeklarowana z atrybutem „unique”, co uniemożliwia istnienie dwóch wierszy o tym samym indeksie. Poniższa metoda tworzy dwa świadczenia o tym samym indeksie w ramach jednej transakcji:
// CreateIndemnites1
static void CreateIndemnites1() {
try {
// logowanie
using (ISession session = sessionFactory.OpenSession()) {
// rozpoczęcie transakcji
using (ITransaction transaction = session.BeginTransaction()) {
// tworzymy dwa świadczenia o tym samym wskaźniku
Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
Indemnites i2 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
// zapisano je
session.Save(i1);
session.Save(i2);
// zatwierdzenie transakcji
transaction.Commit();
}
}
} catch (Exception e) {
Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
}
}
Wyjaśnienia
- w wierszach 9 i 10 tworzone są dwa obiekty Indemnites o tym samym indeksie. Jednak w bazie danych kolumna INDICE posiada atrybut UNIQUE.
- W wierszach 12 i 13 obie encje Indemnites są umieszczane w kontekście trwałości. Kontekst ten jest synchronizowany z bazą danych podczas zatwierdzania transakcji w wierszu 15. Synchronizacja ta spowoduje utworzenie dwóch encji INSERT. Drugi z nich spowoduje wyjątek ze względu na unikalność kolumny INDICE. Ponieważ znajdujemy się w ramach transakcji, pierwszy obiekt INSERT zostanie cofnięty.
Otrzymany wynik jest następujący:
W wierszu 9 widać, że tabela [indemnites] jest pusta. Nie dokonano żadnego wstawienia.
1.5.6. Tworzenie dwóch świadczeń o tym samym indeksie poza transakcją
Poniższa metoda tworzy dwa świadczenia o tym samym indeksie bez użycia transakcji:
// CreateIndemnites2
static void CreateIndemnites2() {
try {
// logowanie
using (ISession session = sessionFactory.OpenSession()) {
// tworzy się dwa świadczenia o tym samym wskaźniku
Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
Indemnites i2 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.94, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
// zapisuje się je
session.Save(i1);
session.Save(i2);
}
} catch (Exception e) {
Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
}
}
Wyjaśnienia
- Mamy ten sam kod co poprzednio, ale bez transakcji.
- Synchronizacja kontekstu trwałości z bazą danych nastąpi przy zamknięciu tego kontekstu, w linii 13 (zamknięcie Session). Synchronizacja spowoduje wygenerowanie dwóch INSERT. Drugi z nich zakończy się niepowodzeniem ze względu na unikalność kolumny INDICE. Ponieważ jednak nie jesteśmy w transakcji, pierwszy INSERT nie zostanie cofnięty.
Otrzymany wynik jest następujący:
Przed wykonaniem metody baza była pusta. W wierszu 6 widać, że tabela [indemnites] zawiera jeden wiersz.























