środa, 1 września 2010

Zarządzanie sesjami w NHibernate

Sesje w NHibernate są kluczowymi obiektami, spośród wszystkich klas NHibernate'a to właśnie do nich najczęściej się odwołujemy. Od tworzenia i zwalniania sesji zależy prawidłowe działanie programu. Dlatego też istotne jest, aby odpowiednio wszystko poukładać, a jeszcze dobrze by było, aby korzystanie z NHibernate'a nie straciło przez to na wygodzie.

O co się rozchodzi?
W NHibernate podstawową "jednostką pracy" (Unit of work) jest obiekt implementującym interfejs ISession. To z jego poziomu możemy wykonywać szereg operacji powiązanych z bazą danych (pobieranie, dodawanie, edytowanie lub usuwanie danych, ogólnie CRUD), by na końcu wydać komendę zaktualizowania faktycznego stanu bazy. Obiekt sesji samodzielnie śledzi zmiany obiektów, wie które są nowo dodane, które zmodyfikowane i tak dalej. Dopiero podczas wywoływania funkcji Flush, za jednym zamachem wszystkie te zmiany zostają wprowadzone do faktycznej bazy.
Obiekt ISession możemy uzyskać z obiektu ISessionFactory przez wywołanie funkcji OpenSession(). Ten znów tworzony jest podczas konfigurowania NHibernate'a, która może być dość kosztowna obliczeniowo i nie ma konieczności przeprowadzania jej więcej niż jeden raz. Dlatego też popularnym podejściem jest przechowywanie tylko jednego obiektu ISessionFactory dla całej aplikacji i tworzenie za jego pomocą obiektów sesji w odpowiednich momentach. 
Są różne podejścia do tego jak często powinno się tworzyć obiekty sesji, na przykład jedna sesja dla całej aplikacji lub sesja dla każdego wątku. Od razu odrzuciłem możliwość korzystania z jednej sesji w całej aplikacji (jeśli wystąpi jakikolwiek wyjątek podczas pracy z sesją, nawet obsłużony przez nas, to sesja przestaje być stuprocentowo wiarygodna) i zdecydowałem się podzielić operacje CRUD na dwie grupy: modyfkujące dane (UPDATE, DELETE, INSERT) oraz jedynie pobierające dane (SELECT). Taki podział sugeruje użycie zasady Command/Query Separation, dlatego też dla obu zastosuję nieco inne podejście:
  • grupa modyfikująca (Command): podczas modyfikacji danych może zajść sporo nieprzewidzianych okoliczności: dane mogą być niezgodne ze stawianymi wymaganiami, ktoś inny może właśnie modyfikować rekord, albo może on już w ogóle nie istnieć. Dlatego dobrze jest skorzystać z transakcji, wtedy jeśli cokolwiek podczas modyfikowania danych pójdzie nie tak, to istnieje możliwość cofnięcia wszystkich zmian wprowadzonych od początku transakcji. Jednym słowem albo wszystko pójdzie dobrze albo nic. Trochę o transakcjach w następnym poście
  • grupa pobierająca (Query): w tym przypadku istotniejsze jest, aby móc wykorzystać potencjał mechanizmu Lazy Loadingu. Co to takiego właściwie jest? Już wyjaśniam.

Lazy Loading
W NHibernate Lazy Loading pozwala na pobieranie z bazy niektórych danych dopiero w momencie, gdy będą one potrzebne, w niektórych przypadkach pozwala to zaoszczędzić czas i zasoby oraz nie popaść w nieskończone zagnieżdzenie obiektów. Jako przykład weźmy fragment schematu bazy danych, którą mam zamiar wykorzystać w projekcie:

Obiekt klasy Album zawiera zestaw (ISet<>) obiektów klasy Photo. Każdy obiekt klasy Photo zawiera obiekt klasy Album do którego należy, który z kolei zawiera zestaw obiektów klasy Photo. I tak dalej aż po granice pamięci. Dzięki mechanizmowi "opóźnionego ładowania" (jeśli mogę to tak przetłumaczyć) taka niekorzystna sytuacja nie ma miejsca. Dane z obcych tabel są pobierane dopiero w momencie, kiedy mają zostać wykorzystane. Jeśli pobierzemy z bazy obiekt klasy Album i nie skorzystamy z jego własności Photos to lista zdjęć nie zostanie faktycznie pobrana. Oczywiście z pozostałych własności możemy korzystać do woli. W momenci pierwszego odwołania do Photos, zdjęcia należące do albumu zostają pobrane z bazy. Takie podejście byłoby szczególnie korzystne jeśli mielibyśmy bardzo dużo zdjęć w albumie a chcieli pobrać jedynie jego opis. 
Jednak żeby skorzystać z tego mechanizmu, sesja nie może być zamknięta przed skończeniem pracy z pobranym obiektem. Dlatego też, w tym przypadku nie można skorzystać z dyrektywy using. Co prawda jest ona bardzo wygodna, gdyż automatycznie zamyka sesję i zwalnia jej zasoby, jednak uniemożliwia załadowanie potrzebnych danych po wyjściu z funkcji. 

Moja implementacja
Odpowiedzialność za zarządzanie sesją spada na na obiekt SessionManager i będzie on wewnętrzną klasą data providera, dzięki temu inna klasa nie będzie nam grzebać w sesjach. Przechowuje on instancję obiektu ISessionFactory, tworzoną jedynie raz, w momencie pierwszego odwołania. Dostęp do niego zapewnia prywatna właściwość:
private ISessionFactory sessionFactory;
private ISessionFactory SessionFactory
{
 get
 {
    if (sessionFactory == null)
      lock (syncObj)
        if (sessionFactory == null)
          sessionFactory = (new NHibernate.Cfg.Configuration())
          .Configure().BuildSessionFactory();

  return sessionFactory;
 }
}
Na potrzeby wszystkich metod z grupy Query, dostępna jest funkcja zwracająca obiekt ISession, będący jedynie obudową dla ISessionFactory.OpenSession():
public ISession GetSession()
{
 return SessionFactory.OpenSession();
}
Najciekawsza jest funkcja przeznaczona dla metod typu Command. Opakowuje cały schemat wykonania transakcji w NHibernate, to znaczy:
  • zdobądź obiekt sesji
  • rozpocznij transakcję, czego wynikiem jest obiekt ITransaction
  • wykonaj na obiekcie sesji wszystkie składowe transakcji, łącznie z oczyszczeniem sesji (flush)
  • potwierdź transakcję do wykonania (commit)
  • w razie problemów wykonaj przywrócenie stanu bazy (rollback) do tego sprzed rozpoczęcia transakcji
  • na końcu zwolnij wszystkie wykorzystane zasoby
Tę sprytną funkcję zapożyczyłem od Procenta:
public void MakeTransaction(Action operation)
{
 using (var session = GetSession())
 {
  using (var tx = session.BeginTransaction())
  {
   try
   {
    operation(session);

    tx.Commit();
   }
   catch (NHibernate.HibernateException)
   {
    tx.Rollback();
    throw;
   }
  }
 }
}
Funkcja najpierw wykonuje standardowe przygotowania transakcji, następnie przekazuje sterowanie obiektem sesji delegatowi i w razie potrzeby obsługuje błędy i rzuca wyjątkiem. Wykorzystać ją można na przykład tak:
sessionManager.MakeTransaction(
 session =>;
 {
  session.Save(obj);
 });
Takie podejście jest niesłychanie wygodne i oszczędza sporo powtarzającego się kodu.

To w zasadzie tyle jeśli chodzi o moją implementację obiektu zarządzającego sesjami. W następnym poście opiszę całą klasę dostępu do danych i to będzie na razie koniec postów o NHibernate. Całość można już teraz podejrzeć na BitBucket.

7 komentarzy:

  1. Czy zamiast using (var session = GetSession()) można zastosować statyczny obiekt SessionFactory i otwierać sesję samemu? Jak to rozwiązanie ma się do ASP.NET, czy można trzymać sesję NHibernete w w sesji strony?

    OdpowiedzUsuń
  2. Czy wywołanie metody Flush() obiektu sesji w metodzie MakeTransaction(Action operation) jest konieczne? Czy nie wystarczy zatwierdzenie transakcji co spowoduje zapisanie w bazie danych wszelkich dokonanych zmian?

    OdpowiedzUsuń
  3. @Grzesiek: Ekspertem od NHibernate absolutnie nie jestem (tym bardziej od ASP.NET), ale postaram się napiszę co wiem (nie musi to być prawda).
    Nie ma chyba żadnych przeszkód w trzymaniu statycznej instancji SessionFactory, ważne jest żeby korzystać z jednej instancji. Takie podejście stosuje na przykład Procent [[http://www.maciejaniserowicz.com/post/2009/11/20/NHibernateStarter-zaczatek-aplikacji-z-NHibernate-NHibernateLinq-Fluent-NHibernate-nUnit-i-SQLite.aspx]], więc raczej jest to prawidłowe podejście. Ja rozważałem taką możliwość, ale ostatecznie stanęło na tym co jest teraz, bo takie podejście wymagało mniej refactoringu poprzedniej wersji.
    Co do ASP.NET, wydaje mi się że najpopularniejszym podejściem do zarządzania sesjami NHibernate jest właśnie przetrzymywanie obiektu ISession w obiekcie Session. Problem ten jest dokładnie omówiony w 13 sesji Summer of NHibernate: Managing Session Lifecycle in a Stateless Web Application. Poza tym całą masę przykładów można znaleźć w internecie, jest tego nawet za dużo, że aż trudno sie zdecyfować.

    OdpowiedzUsuń
  4. @Anonimowy: Sprawdziłem i działa. Myślałem, że tak będzie po prostu bardziej asekuracyjnie.
    Poszukałem teraz i znalazłem (http://nhforge.org/doc/nh/en/index.html#manipulatingdata-endingsession-commit), że jeśli korzysta się z ITrasaction, wywoływanie session.Flush() nie jest konieczne. Dzięki za zwrócenie na to uwagi.

    OdpowiedzUsuń
  5. Getter SessionFactory nie jest Thread Safe - powinno być np. jak u Procenta:
    if (sessionFactory == null)
    {
    lock (syncRoot)
    {
    if (sessionFactory == null)
    {
    ...
    }
    }
    }

    OdpowiedzUsuń
  6. Jawne wywoływanie metody Flush (chyba że w TYLKO niektórych przypadkach) nie powinno mieć miejsca. Jest to bardzo zła praktyka w przypadku developerów korzystających z NHibernate. Operując na sesji wystarczy wywołać metodę Commit - bo niby po co korzystasz z transakcji?

    OdpowiedzUsuń
  7. Zdecydowanie zastosuję się do Waszych sugestii, dziękuję za czujność, pomoc zawsze się przyda.
    Muszę też nieco zmienić klasę SessionManager, ostatecznie zrobię ją statyczną i przeniosę MakeTransaction do klasy dostępu do danych. Szczegóły w następnym poście.

    OdpowiedzUsuń