First NHW poprzednim wpisie o NHibernate chciałem cię szybko wprowadzić w zagadnienia podstawowe. Po co w ogóle jest potrzebna technologia pośrednicząca między aplikacją obiektową a relacyjną bazą danych. Właśnie o tym możesz tutaj poczytać.

 

W Internecie istnieje wiele poradników w języku angielskim do NHibernate . Jednak niektóre z nich wymagają posiadania dużych umiejętności z innych zakresów wiedzy jak np. o NUnit. Ale ja przecież chcę się uczyć NHibernate. Uśmiech z językiem

No cóż, NHibernate to poważna technologia stosowana w dużych firmach, jeśli ktoś uczył się tego na studiach to miał farta. Wpisanie sobie NHibernate do CV jest jednak grą wartą świeczki.
Ten wpis nie będzie świadczył o stopniu zaawansowania i może wydawać się wzorowany na innych źródłach, ale sam się teraz uczę.

Prosta aplikacja, która zaraz zostanie opisana jest w wersji konsolowej oraz WPF.

Instalacja NHibernate

Instalacja NHibernate, a raczej jego użycie polega na dodaniu jej biblioteki do naszego programu. Czyli NHibernate nie wymaga konkretnej wersji Visual Studio albo czegoś w tym rodzaju . Wyczytałem też, że NHibernate działa z .NET od wersji 2.0. Jednym słowem nie ma tutaj żadnego stresu czy nerwów.
Oto strona, na której można pobrać najnowszą wersje NHibernate.
W wypakowanym pliku .rar znajdują się następujące katalogi.

Wypakowany NHibernate 3.1

W katalogu “Required_Bins” znajduje się najważniejsza biblioteka, którą będziemy później dodawać, czyli “NHibernate.dll”. W tym katalogu znajdują się też pliki określające schemat plików konfiguracyjnych oraz mapujących.

Requierd bin folder
W katalogu “Configuration_Templates” są zawarte przykłady plików konfiguracji do różnych baz danych. Naprawdę ułatwiające życie rzeczy.

Configuration Templates

W katalogu “Test” znajdują się pliki pomocne do testowania aplikacji NHibernate w NUnit. O tym, czym jest Nunit i jak go używać napisałem w tym wpisie.

Test folder

Zanim przejdziemy do napisania jakiegokolwiek kodu w VS chce poruszyć mały problem związany ze starymi wersjami NHibernate.

Uwaga do starych Poradników NHibernate

Sam całkiem niedawno uczyłem się NHibernate. Większość poradników czy tutoriali jest napisana w wersji NHibernate 2.0 albo 1.2. Ogólnie nie ma tutaj problemu ze zgodnością starego kodu. Nie, nie, nie problem polega na tym, że NHibernate od wersji 2.1 używa “dynamic proxy”. Ten feature wymaga dodatkowych bibliotek z folderu “Required_For_LazyLoading”. Oczywiście na początku tego nie wiedziałem a raczej nie skojarzyłem faktów. To, o czym teraz piszę może być wczytane z pliku tekstowego “HowInstall.txt”.

Zatem więc moja pierwsza aplikacja sypała się i nawet mnie poprosiła o jakiś plik ".cs”. Dopiero po długiej analizie aplikacji zdałem sobie sprawę, że po prostu w aplikacji brakuje pewnych bibliotek.

Jednak nie chciałbym, aby ktoś, kto chce się samodzielnie nauczyć tej technologii, bo akurat pracodawca tego wymaga Puszczam oczko. miał taki sam problem, jak ja.

Jeśli zamierzasz używasz NHibernate od wersji 2.1 i wyżej to oprócz biblioteki NH musisz jeszcze wybrać jedną z bibliotek do “dynamic proxy”. Ja wybrałem biblioteki z folderu “Castle” . Szczerze kiedyś próbowałem znaleźć jakąś informację, jaka jest różnica pomiędzy tymi bibliotekami, ale nie znalazłem nic konkretnego.

Required_For_LazyLoading

Dodanie referencji

Utworzyłem w Visual Studio projekt aplikacji konsolowej. Do tego przykładu nie będzie potrzebne zaawansowane UI. Naturalnie, aby zacząć swoją przygodę z NHibernate dodałem następujące bibliotek do projektu : NHibernate.dll , Castle.Core.dll oraz NHibernate.ByteCode.Castle.dll. W poprzednim punkcie opisałem, gdzie te pliki się znajdują.

Aby używać NHibernate dodałem następując przestrzenie nazw.

using NHibernate;
using NHibernate.Cfg;
using System.Reflection;

Teraz do aplikacji wypadałoby przygotować bazę danych i tabele, na których będę pracował.

Baza danych

Postanowiłem dodać bazę danych jako plik, który będzie zawarty w projekcie. W ten sposób moja aplikacja będzie mogła działać na każdym komputerze z Serwerem SQL 2008.

Z menu, które pojawia się po naciśnięciu prawego przyciska myszki w projekcie wybrałem “Add new Item”. Potem z okna dialogowego wybrałem plik .mdf i pominąłem kreatora “DataSet” i “Entity”. Do bazy danych, którą nazwałem “PlayedGames” dodałem tablice “Games” oraz następujące pola.

Utworzenie tabeli Game

Tabela będzie zawierać tytuł gry, komentarz, klucz do podobnej gry oraz wartość bitową, czy dana gra została zagrana.

Definiowanie obiektu biznesowego

Klasa Games ma takie same pole jak tabela w bazie danych. Zauważ, że przykład używa pól publicznych, a nie właściwości. Zrobiłem to tylko po to, aby kod był krótszy i nie jest to zawsze dobra praktyka.

Domain

Teraz będziemy w stanie stworzyć integracyjną instancję tej encji z bazy danych. Każda instancja tej encji w domenie będzie odpowiadała każdemu wierszowi z tabeli w bazie.

class Games 
{
        public int id;
        public string Name;
        public string Comment;
        public bool Played;
        public Games SimilarGame;

        public string Say()
        {
            if (Played== true)
                return "Gratulacje przeszedłeś gre " + Name;
            else return "Nie ukończyłeś gry " + Name;
        }
}

Jednak do całej magii jeszcze brakuje mapowania. Mapowanie może być wykonane za pomocą dokumentu xml bądź za pomocą dodania do klasy atrybutów encji. W tym przykładzie encja zostanie zmapowana za pomocą pliku xml.

Tworzenie pliku mapującego

Do projektu dodałem nowy plik xml i nazwałem go “Games.hbm.xml”. Tak “.hbm” jest częścią nazwy pliku. W ten sposób NHibernate może rozpoznać plik mapujący. Aby działał on poprawnie trzeba w jego właściwościach ustawić “Bulid Action” na “Embedded Resource” , bo w innym wypadku będziemy otrzymywać wyjątek aplikacji.

Tutaj właśnie można użyć schematu pliku xml, o którym mówiłem wcześniej. W oknie właściwości pliku XML, które pojawia się przy jego edycji możemy dodać schemat.


Schemat pliku mapującego
Plik mapujący mówi NHibernate jak klasa Games ma być przetransformowana na tabele Games. Pole id odpowiada kolumnie, która nazwa się id. A “SimilarGame” jest właściwością powiązaną “wielu do jednego” (wiele rekordów może mieć tą samą, ale jedną “podobną grę”) z własną klasą, jednak z mapowania nie można tego odczytać, ale z klasy encji już tak. Z pliku xml możemy też odczytać, że id jest uzupełniane automatycznie.

W nagłówku “class” podajemy nazwę klasy w tym wypadku wraz z przestrzenią nazw gdzie ona się znajduje. Przestrzeń nazw może być dodana w nagłówku głównym “hibernate-mapping” i na pewno w przyszłych aplikacjach tak zrobię.

<?xml version="1.0"?> 
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="true">
          <class name="NHibernate01Simple.Games, NHibernate01Simple" lazy="false"> 
                 <id name="id" access="field"> <generator class="native" /> </id>             
                 <property name="Name" access="field" column="Name"/> 
                 <property name="Comment" access="field"  column="Comment"   /> 
                 <property name="Played" access="field" column="Played" /> 
                 <many-to-one access="field" name="SimilarGame" column="SimilarGame" cascade="all"/ > 
          </class> 
</hibernate-mapping>

Jak widać plik xml nie jest taki trudny do zrozumienia, w późniejszych wpisach na pewno do tego będę jeszcze wracał.

Konfiguracja Aplikacji

Jeśli wcześniej pisałeś aplikacje .NET , które korzystały z bazy danych to wiesz i znasz idee z trzymaniem adresu połączenia w pliku konfiguracyjnym web.config albo app.config.

Dodałem więc plik konfiguracyjny do mojej aplikacji i uzupełniłem ją następującym kodem. Ach zapomniałbym, tak jak w poprzednim przypadku, mam dostępny schemat pliku xml dla konfiguracji.

<?xml version="1.0" encoding="utf-8" ?> 
<configuration> 
      <configSections> 
          <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" /> 
      </configSections> 
      <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.SqlClientDriver
                    </property> 
                    <property name="connection.connection_string"> 
                               Data Source=.\SQLEXPRESS; AttachDbFilename=D:\Projekty\Blog\NHibernate01Simple\NHibernate01Simple\PlayedGames.mdf ;Trusted_Connection=true;Integrated Security=True;User Instance=True
                    </property> 
                    <property name="dialect">
                                NHibernate.Dialect.MsSql2005Dialect
                     </property> 
                     <property name="show_sql"> 
                                 true
                     </property> 
                     <property name='proxyfactory.factory_class'> 
                                 NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle
                     </property> 
              </session-factory> 
      </hibernate-configuration> 
</configuration>

To dość duży kod XML! Ale pamiętaj, że NHibernate jest elastyczny i może być ustawiony na wiele sposobów. Najważniejsze opcje są we właściwościach session factory. Tutaj powiedziałem aplikacji, aby wyświetlała wysyłane zapytania w aplikacji konsolowej. Mówię tutaj o właściwości “show_sql”.

W connection.string skorzystałem z możliwości odwołania się do absolutnej ścieżki bazy danych za pomocą wyrazu “AttachDbFilename”. Jeśli aplikacja zmigruje na inny komputer trzeba będzie tą ścieżkę zmienić.
We właściwości “proxyfactory” opisałem, z jakich przestrzeni nazw korzystam.

Dodanie nowego rekordu

Zrobiłem tyle rzeczy a jeszcze niczego nie dodałem do bazy, ani nie uruchomiłem aplikacji. Teraz jednak wszystko powinno być już gotowe do działania.

Wzorowałem ten kod na przykładzie z książki “NHibernate in Action”. Chyba nawet aż za bardzo.

Do program.cs dodałem dwie metody statyczne. Metody statycznej “OpenSession” nie będę omawiał, ponieważ interface ISession oddzielnie wymaga kolejnego dużego wpisu. Sam kod jest prymitywny i mało wydajny nie powinno się go stosować w poważnych aplikacjach. Sam obiekt ISessionFactory jest OGROMNY więc powinien być tylko jeden taki w aplikacji.

W metodzie statyczne “CreateGameAndSaveIt” tworzymy nowy obiekt klasy Games . Wiem, że w bazie danych rekord Games musi mieć uzupełnione konieczne dwa pola “Name” i “Played”. Pole id samo się uzupełnia więc niemusze go obsługiwać.

static void CreateGameAndSaveIt()
{
    Games Mortal = new Games();
    Mortal.Name = "Mortal Kombat I";
    Mortal.Played = true;
    using (ISession session = OpenSession())
    {
        using (ITransaction transaction = session.BeginTransaction())
        {
            session.Save(Mortal);
            transaction.Commit();
        }
        Console.WriteLine("Saved Game to the database");
    }

}

static ISession OpenSession()
{
    if (factory == null)
    {
        Configuration c = new Configuration();
        c.AddAssembly(Assembly.GetCallingAssembly());
        factory = c.BuildSessionFactory();
    }
    return factory.OpenSession();
}
static ISessionFactory factory;

Po otworzeniu sesji mogę dokonać transakcji za pomocą kolejnego interface “ITransaction’’. W obiekcie sesji za pomocą metody save obiekt Mortal zostaje zapisany, czyli moja nowa gra jest zapisana do bazy danych.

Wyświetlenie rekordów

Kod, który ma wyciągnąć wszystkie gry wygląda mnie więcej tak. Tutaj zastosowałem zapytanie w języku Hibernate Query Language. Oczywiście obiekt session umożliwia wyciąganie danych za pomocą normalnych zapytań SQL.

static void LoadGames()
{
    using (ISession session = OpenSession())
    {
        IQuery query = session.CreateQuery(
        "from Games as G order by G.Name asc");
        IList<Games> foundGames = query.List<Games>();
        Console.WriteLine("\n{0} znalezione gry:",
        foundGames.Count);
        foreach (Games game in foundGames)
            Console.WriteLine(game.Say());
    }
}

Ten kod wyciąga wszystkie rekordy z tabeli, po czym, ponieważ dane już są obiektami klasy Games, to mogę zastosować na nich metodę Say().
Jeśli chodzi o wyciąganie pojedynczych rekordów lepiej zastosować metodę Get<klasa>(id).

Games nr1 = session.Get<Games>(1);
transaction.Commit();

Aktualizacja istniejących rekordów

Ten kod działa przy założeniu, że wcześniej dwa razy wykonałem metodę “CreateGameAndSaveIt: i mam dwa rekordy z grą “Mortal Kombat I” w tabeli.

static void UpdateGame()
{
  using (ISession session = OpenSession())
  {
     using (ITransaction transaction = session.BeginTransaction())
     {
          IQuery q = session.CreateQuery(
         "from Games where name = 'Mortal Kombat I'");
          Games MK2 = q.List<Games>().First();
          MK2.Name = "Mortal Kombat II";
          MK2.SimilarGame = q.List<Games>().Skip(1).First();
          transaction.Commit();

      }
  }
}

Ten kod zmienia tytuł pierwszego rekordu na drugą część gry, po czym w polu podobna gra dodaje obiekt pierwszej części gry. W przykładzie zastosowałem proste wyrażenia LINQ . First() wyciąga z listy pierwszy obiekt a Skip() pomija pierwszy obiekt z listy.

Mortal Kombat similar game

Efekt działania kodu.

 

Jak widzicie wszelkie zmiany w obiektach z listy są zatwierdzane za pomocą wyrażenia transaction.Commit(). Prawda, że to łatwe.

Jeśli chcemy zmienić tylko pojedynczy rekord i znamy jego id lepiej zastosować metodę Update(Game,id).

Games d =  new Games(){Name="a",Played=false};
session.Update(d,2);
transaction.Commit();

Kasowanie rekordów

W sumie kasowanie można zrobić na kilka sposobów. Mogę wyciągnąć listę wszystkich obiektów i skasować ich z listy po czym zrobić commint. Mogę po prostu stworzyć zapytanie kasujące albo zastosować metodę delete z obiektu sesji. Akurat metoda delete oferuje nawet możliwość skasować elementów zwróconych przez dane zapytanie.

static void DeleteGames()
{
    using (ISession session = OpenSession())
    {
        using (ITransaction transaction = session.BeginTransaction())
        {
            session.Delete("from Games");
            transaction.Commit();
        }
    }
}

Ten kod skasuje wszystkie rekordy z bazy. A to efekt zapytania w konsoli .NHibernate przetłumaczyło zapytania z języka NQL na SQL. Jak widać na początku wykonało się zapytanie select a potem kilka zapytań delete.

Wykonane zapytanie Deleted przetlumaczone SQL w NH

Co dalej?

Jak widać do płynnego korzystania z NHibernate należy dobrze poznać jego architekturę.