Wstęp

NHibernate jest programem kategorii ORM, który pośredniczy w wymianie informacji z bazą danych. Pozwala na szybkie i wygodne stworzenie logiki programu odpowiedzialnej za komunikację z bazą, dodatkowo automatycznie tworzy w niej obiekty (tj. tabele, klucze główne, relacje). Współpracuje z następującymi bazami danych: FireBird, MSSQL, MySql, Oracle, PostgreSQL, SQLite.

Jednym z głównych atutów technologii NHibernate, jest możliwość odwzorowania modelu domeny (ang. Domain Model) na warstwę bazy danych. Co ważne nie musi to być odwzorowanie jeden do jednego, czyli klasy nie muszą posiadać swoich własnych tabel z kolumnami dokładnie odpowiadającymi polom klas. Do odwzorowania wykorzystywane są pliki xml lub specjalne atrybuty klas. Autor stosuje pliki xml, tak by jasno oddzielić logikę biznesową od bazy danych. Takie podejście nie wymusza „przerabiania” klas pod kątem NHibernate, dzięki czemu są bardziej czytelne i łatwiej je wykorzystać w innych projektach.

Takie odwzorowanie ma jeszcze jedną zaletę, można skorzystać z tzw. ang. Top-down development, czyli najpierw stworzyć logikę biznesową (encje) a później na tej podstawie wygenerować bazę danych. W aplikacji warstwa biznesowa(ang. business layer) jest najważniejsza nie powinna być ograniczana przez inne warstwy aplikacji. A raz prawidłowo zaprojektowany szablon można powtórnie wykorzystać w kolejnych projektach. Przykładem może być mechanizm zarządzania użytkownikami o nazwie Membership firmy Microsoft.

Zalety NHibernate

NHibernate posiada wiele mechanizmów pozwalających na tworzenie dobrego oprogramowania. Po pierwsze są to jednostkowe operacje (ang. unit of work). Kilka operacji jest traktowane jako jedna jednostkowa operacja. Jako przykład niech posłuży aplikacja do oceniania utworów muzycznych. Użytkownik ma przed sobą listę piosenek i każdą z nich może ocenić w skali od jeden do pięć. Ocena może być zapisywana za każdym razem do bazy danych – mało wydajne rozwiązanie, bowiem można kilkukrotnie zmienić ocenę. Aplikacja może też dokonać jednorazowego zapisu podczas zamknięcia okna. Co jest znacznie korzystniejszym scenariuszem. Od strony NHibernate wygląda to następująco: lista obiektów jest pobierana przez NHibernate, następnie śledzone są wszystkie operacje na nich wykonywane, tak by na końcu dokonać jednorazowego zapisu, czyli wykonać jednostkową operację, ang. unit of work.

Kolejne udogodnienie to tzw. ang. lazy loading. Parametr ten określa czy z tabeli mają być pobierane pojedyncze rekordy (lazy = true), czy dodatkowo rekordy połączone referencjami (lazy = false). Co w skrajnym przypadku może spowodować przesłanie do pamięci sporej ilości danych i znacznie obniżyć wydajność całego systemu.

Aplikacje powinny być tak zaprojektowane aby móc używać mechanizmu buforowania (ang. cache). Polega to na utrzymywaniu często używanych danych w pamięci RAM, czyli pewnego rodzaju abstrakcyjnej warstwie, pomiędzy aplikacją a bazą danych. Dzięki temu aplikacja oszczędza zasoby nie odwołując się do bazy danych za każdym razem kiedy potrzebne są dane. NHibernate udostępnia dwa rodzaje buforowania:

  • pierwszego poziomu w obrębie jednej sesji/transakcji (jednostkowej operacji), jest ono automatycznie włączone bez możliwości wyłączenia,
  • drugiego poziomu w obrębie jednego procesu, gdzie proces może składać się z kilku transakcji lub w obrębie jednego klastra (ang. cluster scope), gdzie klaster składa się z kilku procesów, programista decyduje o jego zastosowaniu. Do dyspozycji ma następujące tryby: read-only, nonstrict-read-write, read-write;

Aby buforowanie przyniosło zamierzone rezultaty dane muszą spełniać kilka kryteriów:

  • rzadko ulegać zmianom, są częściej odczytywane niż zapisywane,
  • nie mają krytycznego znaczenia dla całej aplikacji (jak np. dane finansowe),
  • nie są współdzielone z innymi aplikacjami,
  • klasy maja niewiele instancji;

Przykład użycia buforowania w trybie read-write:

<class name=”nazwa_klasy”>
  <cache usage=”read-write”/>
  ...
</class>

Klasy POCO

POCO (ang. Plain Old CLR Object) – to klasy wykorzystywane przez NHibernate do wymiany informacji z bazą danych. Na ich podstawie są tworzone tabele w bazie danych pod warunkiem że spełniają nasępujące wymagania:
  • bezparametrowy konstruktor,
  • właściwości (ang. properties) klasy odpowiadają kolumną tabeli pod warunkiem że są zadeklarowane jako virtual, oczywiście klasa może mieć zwykłe właściwości (niezadeklarowane jako virtual)
  • plik konfiguracyjny (w stylu nazwa_klasy.hbm.xml) służący do odwzorowania klasy na tabelę, lub odpowiednie atrybuty;
Przykładowa klasa POCO:
public class Province {
    virtual public int ProvinceID { get; set; }
    public virtual string Name { get; set; }
    public virtual int Population { get; set; }
}
Oraz odpowiadający jej plik konfiguracyjny Province.hbm.xml:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="TestNHibernate" namespace="TestNHibernate.Domain">
  <class name="TestNHibernate.Province, TestNHibernate" lazy="true" table="Provinces">
    <id name="ProvinceID">
      <generator class="identity"/>
    </id>
    <property name="Name" not-null="true" />
    <property name="Population" not-null="true" />
  </class>
</hibernate-mapping>
 
Parametry:
  • assembly i namespace należy dostosować do własnego projektu,
  • parametr class->name jest pełną nazwą mapowanej klasy w formacie "namespace.nazwa_klasy, namespace", jeżeli w węźle głównym podano parametry namespace i assembly to tu w zupełności wystarczy sama nazwa klasy,
  • table – określa nazwę tabeli,
  • lazy – opisane powyżej,
  • id – klucz główny jaki musi mieć każda tabela,
  • name – określa konkretne pole klasy,
  • generator – określa klasę która posłuży do automatycznego wygenerowania pola (przydatne w kluczach głównych),
  • property – oznacza pole tabeli inne niż klucz główny,
Uwaga: pliki *.hbm.xml powinny posiadać parametr Build action ustawiony na Embedded Resource (okno Properties). Spowoduje to dołączenie go do wynikowego pliku dll, tak by później mógł być łatwo odczytany przez NHibernate. Przedstawione wymagania powodują niewielkie różnice, dlatego takie podejście nie zaciemnia właściwej logiki aplikacji przez NHibernate.

Interfejs ISession

Interfejs ISession zawiera „wiele” metod odpowiedzialnych za zwracanie, zapisywanie, aktualizacje i usuwanie rekordów z bazy danych. Podczas pracy programu jest on wielokrotnie tworzony i usuwany, a w aplikacjach ASP.NET przy każdym odświeżeniu strony. Wymaga niewielkich zasobów, więc nie wpływa to na wydajność całej aplikacji. Można go używać tylko w jednym wątku.
Instancje interfejsu można uzyskać z innego interfejsu ISessionFactory, który wymaga znacznie większych zasobów dlatego tworzony jest tylko raz podczas startu aplikacji i posiada modyfikator static: private static ISessionFactory _sessionFactory; Może pracować z kilkoma wątkami jednocześnie, jednak tylko z jedną bazą danych. Posiada mechanizmy odpowiedzialne za buforowanie danych (ang. cache). Mowa tu o buforowaniu drugiego poziomu, z którego korzystają miedzy innymi sesje (ISession), co zostało już wstępnie opisane. Przykładowy kod z wykorzystaniem ISession, jak i ITransaction(opisanym poniżej) wygląda następująco:
using (ISession session = OpenSession()) 
    using (ITransaction transaction = session.BeginTransaction()) {
        session.Save(myEntity);
        transaction.Commit();
}

Interfejs ITransaction

Interfejs ITransaction służy jako zamiennik mechanizmu transakcji w bazie danych. Nie wymaga wielkich zasobów a jego wywołanie jest proste co pokazuje listing powyżej. Operacje na danych mogą obywać się bez jego udziału, np. implementując swój własny mechanizm transakcji, lub nie implementując go wcale np. podczas prostych operacji odczytu. Przykład nie używania interfejsu ITransaction:
using (ISession session = OpenSession()) 
    session.Save(myEntity);
 

Przykładowa struktura projektu

Aplikacje korzystającą z technologii NHibrnate, można zbudować w oparciu o następujące kroki.
W Visual Studio utworzyć nowy projekt typu „Console application” (assembly name: TestNHibernate, default namespace: TestNHibernate). Z strony: http://sourceforge.net/projects/nhibernate/należy pobrać aktualną wersję narzędzia NHibernate, rozpakować. W projektowanej aplikacji utworzyć referencje do następujących bibliotek z pakietu NHibernate:
  • NHibernate.dll – główna biblioteka,
  • NHibernate.ByteCode.LinFu.dll – klasa fabrykująca,
  • Iesi.Collections.dll – zawiera kolekcję ISet wymaganą przez NHibernate,
  • log4net.dll – wyświetla treść zapytań SQL,
  • Antlr3.Runtime.dll – dynamicznie generuje kod C#,
  • LinFu.DynamicProxy.dll – pozwalająca na dynamiczną zmianę kodu metod z atrybutem „virtual”;
W pakiecie tym znajdują się także pliki konfiguracyjne: „nhibernate-configuration.xsd” i „nhibernate-mapping.xsd”. Należy je skopiować do głównego katalogu projektu, definiują one strukturę plików xml wykorzystywanych do mapowania. Dodatkowo w katalogu głównym projektu należy umieścić plik hibernate.cfg.xml konfigurujący połączenie z bazą danych. NHiberante udostępnia kilka szablonów tego pliku w zależności od bazy danych. Autor skorzystał z bazy danych Microsoft SQL Server, a jego plik wygląda następująco.
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  <session-factory>
    <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
    <property name="dialect">NHibernate.Dialect.MsSql2008Dialect</property>
    <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
    <property name="connection.connection_string">Data Source=DELL\SQLExpress;Initial Catalog=geologDB;User ID=UserTest;Password=passwd</property>
    <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
    <property name="show_sql">true</property>
  </session-factory>
</hibernate-configuration>
Zmianie, w porównaniu do szablonu, uległa sekcja connection.connection_string która zawiera adres serwera bazy danych, nazwę bazy danych, nazwą użytkownika oraz jego hasło.
Konfiguracje klas POCO można teraz odczytać za pomocą następującej metody:
public static Configuration GetConfig() {
   return new Configuration()
          .Configure() //Odczytywanie konfiguracji z „hibernate.cfg.xml”
          .AddClass(typeof(Province));//oraz „Province.hbm.xml”
}
Mając gotowe klasy POCO oraz metodę do odczytywania ich konfiguracji można przystąpić do utworzenia sesji, ISession NHibernate.
private static ISessionFactory _sessionFactory;
 
//Tworzenie sesji, jeżeli takowej jeszcze nie ma
public static ISession OpenSession() {
   if (_sessionFactory == null) 
       _sessionFactory = GetConfig().BuildSessionFactory();           
   return _sessionFactory.OpenSession();
}

Podsumowanie

Tak przygotowany szablon projektu jest gotowy do rozbudowy o kolejne klasy POCO, oraz metody odpowiedzialne za przepływ danych. Zostało to omówione w osobnym artykule pt. [Praktyczne wprowadzenie do NHibernate]. Niniejszy artykuł opisuje skromną część możliwości NHibernate, dlatego warto zapoznać się z materiałami przedstawionymi poniżej.

Literatura