6. Wprowadzenie do ORM NHibernate
Niniejszy rozdział stanowi zwięzłe wprowadzenie do NHibernate, odpowiednika frameworka Java Hibernate dla .NET. 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 wydawania 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:
- Język C# 2008: [Apprentissage du langage C# Version 3.0 avec le Framework .NET 3.5 ]
- [Spring IoC], dostępny w URL i [Spring IoC pour .NET ]. 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 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 (stan na 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, aby ułatwić korzystanie 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 frameworka JUnit dla platformy Java w przypadku .NET.
- 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]:
![]() |
6.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 SGBD za pośrednictwem API i ADO.NET (patrz punkt 3.3).W poprzedniej architekturze łącznik [ADO.NET] jest powiązany z SGBD. Zatem klasą implementującą interfejs [IDbConnection] jest:
- klasa [MySQLConnection] dla SGBD i 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ę modułu SGBD bez konieczności zmiany warstwy [dao]. W takim przypadku należy zmienić jedynie łącznik [ADO.NET].
6.2. Przykładowa baza danych
Aby pokazać, jak pracować z NHibernate, wykorzystamy następującą bazę danych MySQL [dbpam_nhibernate], opisaną w punkcie 3.1. Eksport struktury bazy 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ć.
6.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]: plik DLL z łącznika ADO.NET, który jest częścią SGBD i 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 [4], konsolowe aplikacje testowe
6.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] wskazuje nazwę klasy odpowiedzialnej za obsługę sekcji zdefiniowanej przez atrybut [name], a także klasy DLL zawierającej tę klasę. W tym przypadku klasa nosi nazwę [NHibernate.Cfg.ConfigurationSectionHandler] i znajduje się w 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 na SGBD, to w jaki sposób ustalić dialekt NHibernate dla tego elementu? 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 plików DLL, w tym te, do których odwołuje się projekt.
- w [2], DLL [NHibernate]
- w [3], DLL oraz rozbudowany [NHibernate]. Znajdują się tam różne przestrzenie nazw (namespace), które zostały tam zdefiniowane.
- w [4] 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 znaków służący do połączenia z bazą danych. Ciąg ten ma postać „param1=val1;param2=val2; ...”. Zestaw tak zdefiniowanych parametrów umożliwia sterownikowi 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łączenia 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].
6.3.2. Konfiguracja pliku 'ów mapujących tabele <--> klasy
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 |
6.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 określa pole klasy [Cotisations], które będzie zawierało 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 więc nie ma klucza głównego), będzie miał zatem 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. W punkcie 6.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 generowanie tej wartości mechanizmowi MySQL.
- wiersz 8: znacznik <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].
6.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].
6.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 wynagrodzenia pracownika.
Klasa [Employe] – obraz tabeli [employes] mógłby 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 znacznika: <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ć rozpropagowana 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
- w [2], akcja generowania musi być ustawiona na [Ressource incorporée] [3]. Oznacza to, że podczas generowania projektu plik mapowania musi zostać włączony do wygenerowanego zestawu.
6.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.
6.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.
6.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 zapisu do bazy danych będą wykonywane w ramach transakcji. Albo wszystkie zakończą się powodzeniem, albo żadna z nich nie powiedzie się. W ramach klauzuli using, transakcja jest zatwierdzana przez Commit (wiersz 10). Jeśli w ramach transakcji operacja zapisu do bazy danych 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).
6.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 operacji Save obiekt ten otrzymuje klucz pierwotny. Podczas synchronizacji sesji w bazie danych 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); |
6.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 e").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(string name, single value)
- - SetDouble(string name, double value)
- - SetInt32(string name, int32 value)
- ..
6.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 te ostatnie zamierzamy częściowo przedstawić.
6.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 z klawiatury
Console.ReadLine();
}
// test1
static void ShowDataBase1()
{
// logowanie
using (ISession session = sessionFactory.OpenSession())
{
// rozpoczęcie transakcji
using (ITransaction transaction = session.BeginTransaction())
{
// pobierana jest lista 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 dodatku.
6.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 z klawiatury
Console.ReadLine();
}
// test1
static void ShowDataBase()
{
// zobacz poprzedni przykład
}
// ClearDataBase1
static void ClearDataBase1()
{
// zalogowanie
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)
- tworzy się 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 bazie danych. W rzeczywistości zostały one zapisane jednocześnie z encjami Employe. Aby to zrozumieć, należy powrócić do mapowania encji 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” w 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]
6.5.3. Wyszukiwanie pracownika
Program [Program.cs] zawiera różne metody ilustrujące dostęp do danych w bazie oraz ich przetwarzanie. 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é...
6.5.4. Wstawianie nieprawidłowych elementów
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 {
// 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; }
// 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:
6.5.5. Utworzenie 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.
6.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 po zamknięciu tego kontekstu, w wierszu 13 (zamknięcie Session). Synchronizacja spowoduje wygenerowanie dwóch operacji INSERT. Drugi 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.


















