fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
9 minut

NHibernateStarter – zaczątek aplikacji z NHibernate, NHibernate.Linq, Fluent NHibernate, nUnit i SQLite


20.11.2009

Jakiś czas temu opublikowałem garść porad dla naprawdę zaczynających z NHibernate – od zera. Była to raczej wysokopoziomowa teoria pomieszana z linkami. Tym razem zajrzymy w kod i pokażę w jaki sposób można zacząć tworzyć i testować aplikację z NHibernate jeszcze przed zaplanowaniem struktury bazy danych czy nawet przed wyborem docelowego serwera baz danych.

Paczka do ściągnięcia w dziale Samples.


Każdy może sobie ściągnąć kod (jest go raczej minimalna ilość), podłubać i wyrobić własne zdanie na jego temat. Poniżej przedstawię kilka podstawowych założeń, które stanowią “core” mojego podejścia:

1) NHibernate jest częścią aplikacji

Dokładnie tak.

Kiedyś wydawało mi się, że coś takiego jak dostęp do danych MUSI być ukryte pod własną warstwą repozytoriów, serwisów, czegokolwiek. Po co? Ano… żeby można ją było łatwo podmienić. Albo żeby cały kod odpowiedzialny za bazę danych znajdował się w osobnej dllce. Albo “bo to przecież osobna warstwa”. Albo z jeszcze jakichś innych powodów.

Doświadczenie nauczyło, że korzystanie z ORMappera i CHOWANIE go gdzieś całkowicie pod spodem jest po prostu niepraktyczne (ORMapper JEST warstwą dostępu do danych!). Szczególnie jeśli chodzi o NH, które oprócz operacji na bazie oferuje masę innych możliwości (na przykład cache czy walidację). Zamiast stosować rozdęte “repozytoria” lepiej zainteresować się chociażby Command/Query Separation.

Postanowiłem zatem przestać udawać, że ISession nie istnieje i najzwyczajniej w świecie zacząć korzystać z ORMa, a nie własnej na niego nakładki.

Otwieranie nowej sesji udostępniane jest przez statyczną klasę o wiele mówiącej nazwie DataAccessFacade.

  1:  ISession session = DataAccessFacade.OpenSession();

Wygląda bardzo prosto, bo jak skomplikowana może być implementacja OpenSession()? Ano… może.

2) Func<ISession> zamiast ISession

Implementacja logiki otwierania nowej sesji wygląda tak:

  1:  public static class DataAccessFacade
  2:  {
  3:  	[ThreadStatic]
  4:  	private static Func<ISession> _openSession;
  5:  
  6:  	public static Func<ISession> OpenSession
  7:  	{
  8:  		set { _openSession = value; }
  9:  		get { return _openSession ?? _defaultOpenSession; }
 10:  	}

Co daje nam takie zamotanie? Otóż niekiedy (a dokładniej – w testach jednostkowych, ale o tym za chwilę) może najść nas potrzeba podmiany instrukcji otwierających nową sesję. W takim przypadku każdy wątek z osobna (za sprawą ThreadStaticAttribute) wstawi tam sobie własną logikę i szlus. A jeśli żadna podmiana nie nastąpi, to wykonana zostanie domyślna implementacja:

  1:  private static readonly Func<ISession> _defaultOpenSession =
  2:  	() =>
  3:  	{
  4:  		if (_sessionFactory == null)
  5:  		{
  6:  			lock (_syncRoot)
  7:  			{
  8:  				if (_sessionFactory == null)
  9:  					Configure();
 10:  			}
 11:  		}
 12:  
 13:  		return _sessionFactory.OpenSession();
 14:  	};

W tym przypadku – całkowity standard. Jedna SessionFactory per aplikacja.

Krótkie wyjaśnienie:

Ten sam efekt (różne logiki otwierania połączenia w zależności od scenariusza) można oczywiście uzyskać poprzez abstrakcję tej czynności do interfejsu ISessionProvider z metodą Open(). “Prawdziwa” implementacja byłaby albo dostarczana przez framework Dependency Injection podczas działania aplikacji, natomiast w testach ręcznie przekazywany byłby mock zwracający sesję w pełni przeze mnie kontrolowaną.

ALE.

Oczywistym jest, że baaaardzo duża część systemu potrzebuje nowej sesji, a co za tym idzie: masa konstruktorów musiałaby przyjmować w parametrze ISessionProvider. Niby jest to podejście całkowicie poprawne, lecz w tym konkretnym przypadku skrót w postaci klasy statycznej, eliminujący masę jednakowych i nicniewnoszących zależności sprawdza się moim zdaniem po prostu lepiej. Be pragmatic! Podobnie zresztą jak w przypadku klasy obsługującej logi czy (w niektórych przypadkach) dostarczającej konfigurację.

Dobra, trochę zboczyłem z uzasadnieniami, więc dość na ten temat. Kiedyś może rozwinę się bardziej. I nigdy się nie kończę, mięciutki jak kaczuszka…

3) Skrót do transakcji: DataAccessFacade.InTransaction(…)

W aplikacjach NHibernate bardzo często powtarza się blok kodu analogiczny do tego:

  1:  using (var session = DataAccessFacade.OpenSession())
  2:  {
  3:  	using (var transaction = session.BeginTransaction())
  4:  	{
  5:  		//... instructions
  6:  
  7:  		transaction.Commit();
  8:  	}
  9:  }

Mierziło mnie to nieco, zatem mam metodę pomocniczą:

  1:  public static void InTransaction(Action<ISession> operation)
  2:  {
  3:  	using (var session = OpenSession())
  4:  	{
  5:  		using (var tx = session.BeginTransaction())
  6:  		{
  7:  			operation(session);
  8:  
  9:  			tx.Commit();
 10:  		}
 11:  	}
 12:  }

Teraz wykonanie czegoś w transakcji wygląda tak:

  1:  DataAccessFacade.InTransaction(
  2:  	session =>
  3:  	{
  4:  		session.Save(user1);
  5:  		session.Save(user2);
  6:  	});

Wieeele linii kodu dało się dzięki temu zepchnąć w piekielne czeluście, †KYSZSZSZ† !!!

4) Testy jednostkowe – założenie

Pragnieniem moim jest taką metodę:

  1:  public void AddUser(User user)
  2:  {
  3:  	DataAccessFacade.InTransaction(session => session.Save(user));
  4:  }

przetesować w taki sposób:

  1:  [Test]
  2:  public void AddsNewUser()
  3:  {
  4:  	var newUser = new User()
  5:  	              	{
  6:  	              		UserName = RandomValues.String(),
  7:  	              		Age = RandomValues.Number(),
  8:  	              	};
  9:  
 10:  	new UsersService().AddUser(newUser);
 11:  
 12:  	User fetched;
 13:  	using (var session = DataAccessFacade.OpenSession())
 14:  	{
 15:  		fetched = session.Linq<User>().Where(x => x.UserName == newUser.UserName).SingleOrDefault();
 16:  	}
 17:  
 18:  	Assert.IsNotNull(fetched);
 19:  	Assert.AreEqual(newUser.Age, fetched.Age);
 20:  }

I chcę to robić używając SQLite, które pozwala na tworzenie bazy danych w pamięci. Jest to naprawdę megabłyskawiczny proces – dzięki temu każden jeden test może otrzymać nową, świeżą, specjalnie dla niego utworzoną bazę. I trwa to mgnienie oka.

5) Mechanizm testów jednostkowych

O testowaniu jednostkowym NHibernate z użyciem SQLite pisało wiele osób (wystarczy zajrzeć w Google). Mimo to… musiałem trochę pokombinować aby uzyskać pożądany przeze  mnie efekt.

Wróćmy na chwilę do tego CO chcę osiągnąć. Zależy mi na tym, aby testowana logika SAMA dostarczała sobie sesję. Nie chcę tworzyć w tym miejscu sztucznych zależności o których pisałem wcześniej. Mało tego – metoda ta może zrobić z uzyskaną sesją co jej się żywnie podoba. Między innymi (co oczywiste): wywołać na niej Dispose(). A należy wiedzieć, że baza SQLite tworzona w pamięci żyje tyle, ile połączenie, które ją utworzyło. Wniosek jest prosty: zamknięcie sesji == zamknięcie połączenia == zniszczenie bazy. Normalna sytuacja przedstawia się tak: testowana metoda uzyskuje sesję podłączoną do bazy w pamięci -> wykonuje operacje -> zamyka sesję -> niszczy bazę… i tyleśmy ją widzieli. Test jednostkowy MOŻE mieć dostęp do tej samej sesji (pamiętamy o możliwości modyfikacji metody OpenSession(), prawda?), ale co z tego, skoro jest ona disposed…?

Po dość długim eksperymentowaniu skończyło się na rozwiązaniu, które w pełni mnie satysfakcjonowało. Każdy test jednostkowy wymagający bazy danych posiada własną instancję klasy InMemoryDatabase. Każda instancja podczas konstrukcji tworzy nową bazę danych i wypełnia jej strukturę na podstawie mapowań (dzięki klasie NH SchemaExport). Dodatkowo (standardowo) statycznie konfigurowana jest SessionFactory. Przedstawione wcześniej założenia WYMUSZAJĄ możliwość wielokrotnego skorzystania z jednego obiektu implementującego ISession: zarówno w testowanej logice, jak i w samym teście. Wymaga to oczywiście modyfikacji mechanizmu otwierającego sesje, co osiągnąłem w trzech krokach.

Po pierwsze, w konstruktorze InMemoryDatabase tworzę prawdziwą sesję NHibernate, jak Bozia przykazała – to ona koniec końców posłuży do zbudowania bazy danych i operacji na niej:

  1:  _session = _sessionFactory.OpenSession();

Po drugie, dopilnuję, że z metody DataAccessFacade.OpenSession() zwracana jest właśnie ta jedna sesja; ale wcześniej – uwaga – wywołuję na niej Clear(), aby wyczyścić first-level cache, w przeciwnym wypadku wszystkie dane wykorzystane w operacjach byłyby zapamiętane i podczas faktycznego wykonywania testu NH wcale nie wędrowałoby do bazy:

  1:  DataAccessFacade.OpenSession =
  2:  	() =>
  3:  		{
  4:  			_session.Clear();
  5:  			return new UndisposableSession(_session);
  6:  		};

I wreszcie po trzecie: dopilnuję, aby Dispose() czy Close() nie zamykało połączenia, nie niszczyło bazy, nie powodowało bezużyteczności tej sesji. Do jej “odświeżenia” wystarczy w zupełności pokazane Clear(). Do tego celu zaimplementuję widocznego w powyższym snippecie, banalnego dekoratora sesji:

  1:  public class UndisposableSession : ISession
  2:  {
  3:  	private readonly ISession _session;
  4:  
  5:  	public UndisposableSession(ISession session)
  6:  	{
  7:  		_session = session;
  8:  	}
  9:  
 10:  	public void Dispose()
 11:  	{
 12:  		_session.Clear();
 13:  	}
 14:  
 15:  	public IDbConnection Close()
 16:  	{
 17:  		_session.Clear();
 18:  		return null;
 19:  	}
 20:  
 21:  	//... all other methods delegated to the real session
 22:

I śmiga aż miło. Oczywiście nie zabezpiecza to przed wszystkimi możliwymi scenariuszami (np. można ręcznie dogrzebać się do połączenia poprzez obiekt sesji i je zamknąć, tylko… wiem że tego nie robię).


I to tyle. Na pewno można to ulepszyć / uprościć. Jestem bardzo ciekaw waszych opinii, zatem zachęcam do ściągnięcia kodu źródłowego i zostawiania komentarzy. Zadawajcie też pytania, dzięki nim da się ciekawie zgłębić ten temat.

0 0 votes
Article Rating
17 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
^simon
15 years ago

Ad 1
Zgadzam się, że nie ma sensu ukrywać NHibernate. W aplikacji z modelem domeny chciałbym jedynie, aby sam model był niezależny od NH. Cała reszta może i POWINNA korzystać z dobrodziejstw, które oferuje nam NHibernate.
W aktualnym projekcie próbowaliśmy się od NH odciąć warstwą abstrakcji, która teoretycznie pozwoliłaby przejść na EF4, ale efekt był tylko taki, że odcięliśmy sobie możliwość skorzystania z optymalizacji typu Load zamiast Find.

Ad 2
Rozważałeś użycie "ambient/current session"? To takie poor man’s dependency injection, ale jak dla mnie sprawdza się znakomicie. Podając obiekt ISessionFactory (który można sobie wstrzyknąć) dostajesz sesję z "kontekstu" skojarzoną z tą fabryką. Do implementacji są provider-y, która odpowiadają za przekazanie tej kontekstowej sesji (np. dla aplikacji ASP.NET itp).

Ad 3
W tym wypadku preferuję albo wzorzec Command i przechwytywanie wywołania Execute() na komendzie, które dodaje transakcyjność, albo proste AOP (np. z Unity). Oficjalny guideline jest taki, aby wszystkie operacje w ramach komendy robić w transakcji (a więc odczyty także), dlatego tak jest mi łatwiej.

Ad 4 i 5
SQLite wymiata w testach jednostkowych:)))

biz
biz
15 years ago

Nie do końca bym się tu zgodził. Ja jednak preferuję podejście z zestawem obiektów DAO w których są odwołania do ISession. Niekoniecznie trzeba DAL odseparowywać fizycznie do innej DLL-ki, ale taki logiczny podział się przyda. Serwisy warstwy biznesowej wywołują metody obiektów DAO nie mając świadomości co jest pod spodem tj. NHibernate, zapytania SQL, wywołania procedur składowanych, itp. Takiego podejścia używam w projektach różnej wielkości i sprawdza się. Co do obsługi transakcji (bo jest to kluczowe w tym kontekście), to świetnie jest to zaimplementowane w Spring.NET (link: http://www.springframework.net/doc-latest/reference/html/transaction.html), a i samemu można coś podobnego napisać.

Kod typu:

using (var tx = session.BeginTransaction())
{
operation(session);
tx.Commit();
}

czy nawet typu:

DataAccessFacade.InTransaction(
session =>
{
session.Save(user1);
session.Save(user2);
});

jak dla mnie wygląda … trochę koszmarnie;).

Darek
15 years ago

1: private static readonly Func<ISession> _defaultOpenSession =
2: () =>
3: {
4: if (_sessionFactory == null)
5: {
6: lock (_syncRoot)
7: {
8: if (_sessionFactory == null)
9: Configure();
10: }
11: }
12:
13: return _sessionFactory.OpenSession();
14: };

Dlaczego wybrałeś rozwiązanie z lockowaniem? Z punktu widzenia wydajności taki wybór nie jest najlepszy.

procent
15 years ago

@simon:
2) Session Factory, Session Provider czy cokolwiek innego… nie chce tego wrzucac przez DI. Chce to w jakis sposob ‘statyczny’ pobierac, bez przedstawiania jawnej zaleznosci.
3) Transakcja per komenda to za malo – chce miec mozliwosci objecia transakcja wiecej niz 1 komendy. Poza tym staram sie unikac ‘atrybutowego’ czy w inny sposob ‘magicznego’ AOP – to co napisalem to tez na swoj sposob AOP, tyle ze aspekt jest zaimplementowany za pomoca metody a nie przechwycenia wywolania w runtime :).

procent
15 years ago

@biz:
Tez stosowalem DAO w roznej wielkosci projektach i… teraz widze ze tak naprawde nigdy sie to nie sprawdzalo. Dlaczego warstwa biznesowa ma ‘nie wiedziec’ ze pod spodem sa ‘jakies dane’? Nie mowie zeby konstruowac w nich sql i pykac bezposrednio do bazy, ale moim zdaniem interakcja z ISession na tym poziomie nie jest niczym zlym. Powiem wiecej – brak tej interakcji ogranicza uzycie NH, powoduje eksplozje jednorazowych metod w ‘warstwie dostepu do danych’, wymusza implementacje wielu zbednych zaleznosci…
Nie jestem zadna wyrocznia i tak naprawde caly czas ‘badam’ ten obszar, ale na moj aktualny poziom doswiadczenia daje takie a nie inne wnioski. Byc moze za tydzien czy za miesiac okaze sie ze jestem teraz w bledzie:).
Co do implementacji transakcji… zdecydowanie nie chce, aby bylo to sterowane atrybutami – potrzebuje JAWNEGO okreslenia transakcyjnosci. Atrybut nie jest dla moich oczu…jawny:). A z tego co widze to w Spring podejscie ‘programistyczne’ jest wlasciwie takie samo jak u mnie: TransactionTemplate.Execute([delegate]).

procent
15 years ago

@Darek:
Chyba nie do konca rozumiem:). Jest lock, bo jednoczesnie moze sie do tej metody odwolac wiecej niz 1 wątek. A wydajnosc tego akurat kawalka kodu nie ma zadnego znaczenia – wykonuje sie on tylko raz. I co bym tam nie zepsul, i tak inicjalizacja SessionFactory (w domysle – wykonywana w Configure()) bedzie trwala dluzej niz moje instrukcje.

biz
biz
15 years ago

Co do transakcyjności w Spring.NET, to rzeczywiście nie określiłem się jasno, tj. miałem na myśli deklaracyjną obsługę transakcji. A to springowe podejście ‘programistyczne’ wygląda trochę … nie po mojemu;). Ja nie potrzebuję jawnej obsługi transakcji. Powiem, że wręcz nie lubię jak taki kod miesza się w logikę biznesową. Stosuję transakcje na poziomie metod serwisów biznesowych. W Spring.NET wystarczy opatrzeć metodę odpowiednim atrybutem, a we frameworku którego używam w firmie (wewnętrzny produkt firmy) autorzy poszli jeszcze o krok dalej i transakcje są zakładane na metody serwisów niejawnie (nawet nie potrzeba dekorować metody atrybutem).
Co do izolacji ISession z warstwy biznesowej, to pewnie masz rację, że powodować to może ekspozję jednorazowych metod w obiektach DAO, ale coś kosztem czegoś. Uważam, że warto ponieść taką cenę, za choćby namiastkę odseparowania tych dwóch warstw. Oczywiście nie popadam tu w paranoje i nie jestem zwolennikiem tworzenia osobnych DLL-ek z obiektami DAO i zestawem interfejsów do nich.
To świetnie, że pomimo stosowania pewnych standardów czy wzorców projektowych każdy ma swoje spostrzeżenia oraz doświedczenia i można je ze sobą wymienić.

procent
15 years ago

@biz:
Świetnie, indeed:). Chodzi mi po głowie zamysł napisania kilkuodcinkowego cyklu postów zachęcającego do wymiany doświadczeń w takich właśnie ogólnych programistycznych kwestiach. Mam pomysł na parę tematów do poruszenia, mam swoje poglądy które mógłbym przedstawić jako wyjściowy punkt dyskusji, mam nawet tytuł cyklu… tylko czasu na ładne spisanie brak:). Ale może kiedyś się uda.

dario-g
15 years ago
procent
15 years ago

@dario-g:
Miejsce zamieszkania wszelkich wewnętrznych serwisów, repozytoriów, komend, modelu domeny itd to dla mnie komponent udostępniający odpowiednie funkcjonalności przez WCF. ASP.NET, client-side javascript, WinForms, WPF czy cokolwiek innego jest tylko klientem, więc nie ma mowy o NHibernate w kontekście asp.net.
Serwowanie nowych sesji NH w kontekscie asp.net jest wystarczająco opisane w manualu do NH:).

Szymon Pobiega
15 years ago

Jeśli chodzi o transakcje, to jestem zwolennikiem minimum jawności. Tzn twarde założenie, że wszystko co jest w warstwie usług/aplikacji jest transakcyjne mi wystarczy. Ale to oczywiście kwestia budowy aplikacji, więc nie twierdze, że moje podejście jest jedynie-słuszne. Twierdze, że jest słuszne, jeśli aplikacja wygląda tak, jak te, które ostatnio budowałem;)

@%:
Przyjmuje "wyzwanie" do dyskusji — postaram się coś skrobnąć na temat tego jak ja widzę ten temat. Może ktoś się jeszcze przyłączy do tej blogo-dyskusji?

biz
biz
15 years ago

@procent:
Czekam na taki cykl, bo taka dyskysja może często być lepsza niż sucha teoria.

Artur
Artur
14 years ago

"Jeden kod wart więcej niż tysiąc postów" ;) Bardzo ciekawy wpis.

procent
14 years ago

@Artur:
No to i git, wkrotce kolejny post z paczką do sciagniecia.

Am
Am
14 years ago

Uzywac sesji ThreadStatic w aplikacjach webowych?
Zły pomysł.
http://piers7.blogspot.com/2005/11/threadstatic-callcontext-and_02.html

Am
Am
14 years ago

Pardon, nie zauważyłem poprzedniego komentarza na temat kontekstu wykorzystania NH. Czy to oznacza, że każdą aplikację oddzielasz od bazy warstwą WCF, z całym jej narzutem serializacji?

procent
14 years ago

@Am:
Sesja nie jest [ThreadStatic] – tym atrybutem oznaczona jest operacja ZWRACAJACA nowa sesje (Func<ISession>). W normalnym scenariuszu atrybut ten nie ma zadnego wplywu na dzialanie systemu, jego zadanie to umozliwienie rownoleglego wykonywania testow jednostkowych (ktore tak czy siak w wiekszosci runnerow sa wykonywane sekwencyjnie?).
Co do drugiego pytania: bardzo dawno nie pisalem juz aplikacji "czysto webowej", w sensie: asp+db. Wyodrebnienie osobnej warstwy do serwisu udostepnianego innym komponentom przez WCF w bardzo wielu przypadkach bardziej zlozonych systemow jak najbardziej ma sens. Komunikacja przez net.tcp i serializacja binarna z pewnoscia nie spowoduja zauwazalnego narzutu, o ile oczywiscie sa madrze zaimplementowane.

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również