Skip to content

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:

  1. Język C# 2008: [Nauka języka C# w wersji 3.0 z wykorzystaniem platformy .NET 3.5 (2008)]
  1. [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:

  1. nawiązuje połączenie ze źródłem danych
  2. współpracuje ze źródłem danych w trybie odczytu/zapisu
  3. 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
ConnectionString
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.
Open
M
otwiera połączenie z bazą zdefiniowaną przez ConnectionString
Close
M
zamyka połączenie
BeginTransaction
M
rozpoczyna transakcję.
State
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:

1
2
3
IDbConnection connexion=new Connection();
connexion.ConnectionString=...;
connexion.Open();

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
CommandType
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
CommandText
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
Connection
P
połączenie IDbConnection, które ma zostać użyte do wykonania zlecenia SQL
Transaction
P
transakcja IDbTransaction, w ramach której ma zostać wykonane zlecenie SQL
Parameters
P
lista parametrów zdefiniowanego polecenia SQL. Polecenie update articles set price=price*1.1 where id=@id posiada parametr @id.
ExecuteReader
M
w celu wykonania polecenia SQL Select. Otrzymujemy obiekt IDataReader reprezentujący wynik polecenia Select.
ExecuteNonQuery
M
w celu wykonania polecenia SQL Update, Insert, Delete. Uzyskuje się liczbę wierszy, których dotyczy operacja (zaktualizowanych, wstawionych, usuniętych).
ExecuteScalar
M
w celu wykonania polecenia SQL. Polecenie Select zwraca tylko jeden wynik, tak jak w przypadku: select count(*) from articles.
CreateParameter
M
w celu utworzenia parametrów IDbParameter dla zdefiniowanego polecenia SQL.
Prepare
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ć:

// nawiązanie połączenia 
IDbConnection connexion=...
connexion.Open();
// przygotowanie zlecenia
IDbCommand commande=new Command();
commande.Connection=connexion;
// wykonanie polecenia select
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// wykonanie polecenia update, insert, delete
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// zamknięcie połączenia
connexion.Close();

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
FieldCount
P
liczba kolumn w tabeli IDataReader
GetName
M
GetName(i) zwraca nazwę kolumny nr i z tabeli IDataReader.
Item
P
Element [i] reprezentuje kolumnę nr i w bieżącym wierszu tabeli IDataReader.
Read
M
przechodzi do następnego wiersza tabeli IDataReader. Zwraca wartość logiczną True, jeśli odczyt się powiódł, a w przeciwnym razie – False.
Close
M
zamyka tabelę IDataReader.
GetBoolean
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.
Getvalue
M
Getvalue(i): zwraca wartość z kolumny nr i bieżącego wiersza tabeli IDataReader jako typ object.
IsDBNull
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:

// otwarcie połączenia 
IDbConnection connexion=...
connexion.Open();
// przygotowanie polecenia
IDbCommand commande=new Command();
commande.Connection=connexion;
// wykonanie polecenia select
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// przetwarzanie wyników
while(reader.Read()){
     // przetwarzanie bieżącej linii
        ...
}
// zamknięcie czytnika
reader.Close();
// zamknięcie połączenia
connexion.Close();

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:

#
# Struktura tabeli `cotisations`: 
#

CREATE TABLE `cotisations` (
  `ID` bigint(20) NOT NULL auto_increment,
  `SECU` double NOT NULL,
  `RETRAITE` double NOT NULL,
  `CSGD` double NOT NULL,
  `CSGRDS` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;

#
# Struktura tabeli `indemnites`: 
#

CREATE TABLE `indemnites` (
  `ID` bigint(20) NOT NULL auto_increment,
  `ENTRETIEN_JOUR` double NOT NULL,
  `REPAS_JOUR` double NOT NULL,
  `INDICE` int(11) NOT NULL,
  `INDEMNITES_CP` double NOT NULL,
  `BASE_HEURE` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `INDICE` (`INDICE`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;

#
# Struktura tabeli `employes`: 
#

CREATE TABLE `employes` (
  `ID` bigint(20) NOT NULL auto_increment,
  `PRENOM` varchar(20) NOT NULL,
  `SS` varchar(15) NOT NULL,
  `ADRESSE` varchar(50) NOT NULL,
  `CP` varchar(5) NOT NULL,
  `VILLE` varchar(30) NOT NULL,
  `NOM` varchar(30) NOT NULL,
  `VERSION` int(11) NOT NULL,
  `INDEMNITE_ID` bigint(20) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `SS` (`SS`),
  KEY `FK_EMPLOYES_INDEMNITE_ID` (`INDEMNITE_ID`),
  CONSTRAINT `FK_EMPLOYES_INDEMNITE_ID` FOREIGN KEY (`INDEMNITE_ID`) REFERENCES `indemnites` (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;

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] &lt;%X{auth}&gt; - %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] &lt;%X{auth}&gt; - %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] &lt;%X{auth}&gt; - %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]:

ID
klucz główny typu autoincrement
VERSION
numer wersji rekordu
SECU
stawka (procentowa) składki na ubezpieczenie społeczne
RETRAITE
stawka składki emerytalnej
CSGD
stawka składki na ogólny podatek socjalny podlegający odliczeniu
CSGRDS
stawka składki na ogólną składkę społeczną oraz składkę na spłatę długu społecznego

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

ID
klucz główny typu autoincrement
VERSION
numer wersji rekordu
BASE_HEURE
koszt jednej godziny dyżuru w euro
ENTRETIEN_JOUR
dodatek w euro za dzień dyżuru
REPAS_JOUR
dodatek na posiłki w euro za dzień dyżuru
INDEMNITES_CP
dodatki z tytułu płatnego urlopu. Jest to procent, który należy zastosować do wynagrodzenia podstawowego.

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

ID
klucz podstawowy typu autoincrement
VERSION
numer wersji rekordu
PRENOM
imię pracownika
NOM
jego nazwisko
ADRESSE
jego adres
CP
jego kod pocztowy
VILLE
jego miasto
INDEMNITE_ID
klucz obcy na INDEMNITES(ID)

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:

ITransaction BeginTransaction()
rozpoczyna transakcję w sesji
ITransaction tx=session.BeginTransaction();
void Clear()
opróżnia sesję. Obiekty, które zawierała, stają się odłączone.
session.Clear();
void Close()
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();
IQuery CreateQuery(string queryString)
tworzy zapytanie HQL (Hibernate Query Language) do późniejszego wykonania.
IQuery query=session.createQuery("select e from Employe e);
void Delete(object obj)
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);
void Flush()
wymusza synchronizację sesji z bazą danych. Zawartość sesji nie ulega zmianie.
session.Flush();
T Get<T>(object id)
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);
object Save(object obj)
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);
SaveOrUpdate(object obj)
wykonuje operację Save, jeśli obiekt nie ma klucza głównego, lub operację Update, jeśli już go posiada.
void Update(object obj)
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ę:

IQuery query=session.createQuery("select e from Employe e);

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:

IList<T> List<T>()
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>();
IList List()
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.
T UniqueResult<T>()
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:

1
2
3
string numSecu;
...
Employe e=session.createQuery("select e from Employe e where e.SS=:num").SetString("num",numSecu).UniqueResult<Employe>();

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:

Affichage base -------------------------------------
--------------- liste des employés
[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[1|1,93|2|3|12]]
[260124402111742|Laverti|Justine|La Brûlerie|St Marcel|49014|[2|2,1|2,1|3,1|15]]

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

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:

sauvegarde d'un employé vide -------------------------------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Employe][SQL: INSERT INTO EMPLOYES (VERSION, SS, NOM, PRENOM, ADRESSE, VILLE, CP, INDEMNITE_ID) VALUES (?, ?, ?, ?, ?, ?, ?, ?)]

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:

Effacement base -------------------------------------
--------------- usunięcie pracowników
--------------- usunięcie świadczeń
--------------- usunięcie stawek składek
Création de deux indemnités de même indice dans une transaction --------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Indemnites][SQL: INSERT INTO INDEMNITES (VERSION, INDICE, BASE_HEURE, ENTRETIEN_JOUR, REPAS_JOUR, INDEMNITES_CP) VALUES (?, ?, ?, ?, ?, ?)]
Affichage base -------------------------------------
--------------- lista pracowników
--------------- lista świadczeń
--------------- tabela stawek składek

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:

1
2
3
4
5
6
7
Création de deux indemnités de même indice sans transaction --------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Indemnites][SQL: INSERT INTO INDEMNITES (VERSION, INDICE, BASE_HEURE, ENTRETIEN_JOUR, REPAS_JOUR, INDEMNITES_CP) VALUES (?, ?, ?, ?, ?, ?)]
Affichage base -------------------------------------
--------------- lista pracowników
--------------- lista dodatków
[1|1,93|2|3|12]
--------------- tabela stawek składek

Przed wykonaniem metody baza była pusta. W wierszu 6 widać, że tabela [indemnites] zawiera jeden wiersz.