Posts tagged NHibernate
Oswajanie Fluent NHibernate
Apr 22nd
O FluentNHibernate napisano już całkiem sporo, jednak niestety duża część z informacji krążących po Sieci jest już nieaktualna z powodu zmian w API. Postanowiłem więc podzielić się z Wami wnioskami z moich wczorajszych zmagań z FNH. Zanim jednak przejdę do konkretów, jeśli ktoś nigdy nie używał tej biblioteki, prawdopodobnie powinien zacząć o tych postów Procenta.
Enumy
Jak Procent zauważył, mapowanie enumów za pomocą właściwego im typu całkowitoliczbowego realizuje się za pomocą klauzuli CustomType:
Map(x => x.Gender).CustomType<Gender>();
Jako leniwy programista chciałbym jednak, aby FluentNHibernate zrobił to za mnie. Nauka FNH odbywa się za pomocą konwencji. Jedyne, co należy stworzyć to własną konwencję mapowania typów enum. Oto ona:
public class EnumAsIntConvention : IUserTypeConvention
{
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => x.Property.PropertyType.IsEnum);
}
public void Apply(IPropertyInstance target)
{
target.CustomType(target.Property.PropertyType);
}
}
Nie muszę chyba tłumaczyć jak działa, ponieważ wszystko widać na pierwszy rzut oka. Klasę tą znalazłem gdzieś na StackOverflow.
varchar a nvarchar
Nie zamierzam się wdawać w filozoficzne dywagacje na temat przewagi jednego nad drugim (ani drugiego nad pierwszym). Po prostu chciałem, aby moje napisy były przechowywane w bazie jako varchar. W Sieci znalazłem wiele rozwiązań, ale żadne mnie nie satysfakcjonowało, ponieważ nie można go było zautomatyzować za pomocą konwencji. Wtedy, przeglądając listę typów wspieranych przez NHibernate natrafiłem na AnsiString. Pomyślałem, że spróbuję. Tak zrodziła się konwencja
public class StringAsVarcharConvention : IUserTypeConvention
{
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => x.Property.PropertyType == typeof(string));
}
public void Apply(IPropertyInstance target)
{
target.CustomType("AnsiString");
}
}
która uczy FNH, aby wszystkie napisy mapował do kolumn varchar o odpowiedniej długości.
varchar(max)
A propos długości, pewnie wiecie, ale na wszelki wypadek: zarówno przy użyciu klasycznych plików hbm.xml, jak i płynnych mapowań, aby wymusić na NHibernate zastosowanie typu varchar(max) (i analogicznego — varbinary(max)) należy podać długość pola większą niż 8000. Ja stosuje 8001 wraz z odpowiednim komentarzem.
Decimal
Kolejna kwestia to standaryzacja reprezentacji kwot w projekcie. W moim wypadku zdecydowaliśmy się na zastosowanie decimal(18,2) na poziomie bazy danych. Poniższa konwencja uczy FluentNHibernate respektowania naszej decyzji:
public class DecimalConvention : IUserTypeConvention
{
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => x.Property.PropertyType == typeof(decimal));
}
public void Apply(IPropertyInstance target)
{
target.Scale(2);
target.Precision(18);
}
}
Stereotypy
W przypadku inżynierii oprogramowania są niewątpliwie dobrym zjawiskiem. Jeśli 90% moich pól tekstowych reprezentuje jedną z dwóch kategorii, mogę pokusić się o zdefiniowanie, za pomocą metod rozszerzających, odpowiednich stereotypów.
Map(x => x.SendersReference).AsReference(); Map(x => x.CustomerSpecifiedReference).AsReference(); Map(x => x.BeneficiaryInformation).AsTextualInfo(); Map(x => x.RemittanceInformation).AsTextualInfo();
Ich implementacja wygląda następująco:
public static PropertyPart AsReference(this PropertyPart propertyPart)
{
return propertyPart.Length(16).Not.Nullable();
}
public static PropertyPart AsTextualInfo(this PropertyPart propertyPart)
{
return propertyPart.Length(144).Not.Nullable();
}
Dodatkową korzyścią ze stosowania takich rozszerzeń jest fakt, że nadajemy naszemu mapowaniu sens biznesowy. Zarówno Reference, jak i TextualInfo są pojęciami pochodzącymi z domeny problemu, które są elementem naszego wszędobylskiego języka (ubiquitous language).
Z drugiej (technicznej) strony, na podobnej zasadzie można stworzyć rozszerzenie AsVarcharMax(), które przypisze tę nieszczęsną długość 8001 i ukryje ten kod w jednym miejscu.
Konwencje, konwencje
W żadnym razie nie narzucam Wam moich konwencji. Jeśli Wam odpowiadają, bierzcie i stosujcie. Jeśli nie — stwórzcie własne. Miejcie jednak jakieś. Naprawdę, warto mieć konwencje.
NHibernate, NServiceBus i transakcje
Jan 13th
Dziś chciałbym podzielić się z Wami moimi refleksjami na temat sposobu zarządzania transakcjami w NHibernate, ze szczególnym uwzględnieniem nietrywialnego przypadku, kiedy w ramach jednej transakcji wykorzystujemy zarówno NHibernate, jak i NServiceBus. Posłużę się w tym celu kodem DDDSample.Net.
Aby wprowadzić Was w klimat, przypomnę jak wygląda architektura DDDSample. Począwszy od najwyższego poziomu, występują tam następujące warstwy:
- WebUI (ASP.NET MVC) – prezentacja danych, interfejs
- Application – stanowi fasadę dla modelu domeny udostępniając interfejs poszczególnych akcji/komend
- Domain – logika biznesowa żyje tutaj
- Domain.Persistence.NHibernate – mapowania NHibernate oraz implementacje repozytoriów
Która warstwa odpowiada zatem za transakcje? Oczywiście Application. Dlaczego? Ponieważ jednostką izolacji są akcje/komendy — każda taka jednostka wykonywana jest w ramach osobnej transakcji.
Ponieważ transakcyjność postrzegam (zwykle) jako jedno z wymagań niefunkcjonalnych, implementuje ją za pomocą aspektów. Korzystam przy tym z możliwości mojego ulubionego kontenera Unity, jednak to samo można zrobić za pomocą niemal dowolnego innego kontenera. Moja warstwa Application składa się par (interfejs, klasa), przy czym interfejs zdefiniowany jest nie po to, aby umożliwić zmianę implementacji, ale tylko i wyłącznie po to, aby umożliwić AOP (tak, wiem, że można to samo osiągnąć za pomocą metod wirtualnych)
public interface IBookingService
{
TrackingId BookNewCargo(UnLocode origin, UnLocode destination, DateTime arrivalDeadline);
//...
public class BookingService : IBookingService
{
public TrackingId BookNewCargo(UnLocode originUnLocode, UnLocode destinationUnLocode, DateTime arrivalDeadline)
{
Location origin = _locationRepository.Find(originUnLocode);
Location destination = _locationRepository.Find(destinationUnLocode);
//...
container.Configure<Interception>()</pre>
//Ustaw sposób przechwytywania
.SetInterceptorFor<IBookingService>(new InterfaceInterceptor())
.SetInterceptorFor<IHandlingEventService>(new InterfaceInterceptor())
//Dodaj nową "politykę"
.AddPolicy("Transactions")
//Wykorzystującą aspekt obsługi transakcji
.AddCallHandler<TransactionCallHandler>()
//I podłącz do wszystkich interfejsów z assembly
.AddMatchingRule(new AssemblyMatchingRule("DDDSample.Application"));</div>
<div>
Który wykorzystując API sesji kontekstowych tworzy nową transakcję, wywołuje właściwą metodę, po czym, jeśli nie wystąpił żaden wyjątek, zatwierdza transakcję. W przypadku wyjątku transakcja pozostaje niezatwierdzona, a wyjątek (nienaruszony) przelatuje do warstw wyższych.
public class TransactionCallHandler : ICallHandler
{
private readonly ISessionFactory _sessionFactory;
public TransactionCallHandler(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
using (ITransaction tx = _sessionFactory.GetCurrentSession().BeginTransaction())
{
IMethodReturn result = getNext()(input, getNext);
if (result.Exception == null)
{
tx.Commit();
}
return result;
}
}
public int Order { get; set;}
}
Bardzo lubię ten kawałek kodu. Niestety nie sprawdza się on w moim ulubionym scenariuszu: NHibernate + NServiceBus. W takim przypadku potrzebuje transakcji rozproszonej System.Transactions. Swego czasu Ayende pisał o tym, że NHibernate “automagicznie” współdziała z System.Transactions. Ostatnio jednak pojawił się w NHibernate koncept ITransactionFactory, którego zadaniem jest (chyba?) poprawa jakości tego współdziałania. Domyślna implementacja fabryki, AdoNetWithDistrubtedTransactionFactory, jak sama nazwa sugeruje wspiera transakcje rozproszone. Niestety nie udało mi się sprawić, aby działała w najprostszym scenariuszu integracji z NServiceBus. Zrezygnowałem więc z tego ficzera i powróciłem do “starego dobrego” AdoNetTransactionFactory (który teraz trzeba sobie skonfigurować samemu). Niezbędna była jednak modyfikacja mojego TransactionCallHandler:
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
IMethodReturn result;
SqlConnection sqlConnection = _sessionFactory.GetCurrentSession().Connection as SqlConnection;
if (sqlConnection == null)
{
throw new NotSupportedException("Only SqlConnection is supported.");
}
using (TransactionScope tx = new TransactionScope())
{
sqlConnection.EnlistTransaction(Transaction.Current);
result = getNext()(input, getNext);
if (result.Exception == null)
{
_sessionFactory.GetCurrentSession().Flush();
tx.Complete();
}
}
sqlConnection.EnlistTransaction(null);
return result;
}
Zamiast korzystać z API transakcji NHibernate, korzystam bezpośrednio z System.Transactions. Negatywnym skutkiem tego podejścia jest ograniczenie wspieranych baz do SQLServera 2005/2008. Niestety ADO.NET nie umożliwia niezależnego od sterownika bazy danych wpinania połączeń w transakcje rozproszone.
Pierwsze wywołanie EnlistTransaction wpina aktualne połączenie, na którym działa sesja NHibernate do transakcji rozproszonej. Drugie wywołanie (to z null-em) odłącza połączenie od transakcji. Jeśli transakcja ta nie została wcześniej zatwierdzona (tx.Complete()), zmiany są wycofywane na poziomie bazy danych.
Jest jeszcze jeden szkopuł. NHibernate domyślnie zwalnia połączenia związane z sesją najwcześniej, jak może. Jest to zachowanie optymalne z punktu widzenia wydajności, jednak w przypadku takiego zarządzania transakcjami, niepoprawne. Nie chcemy przecież, aby nasze połączenie, które podłączyliśmy do rozproszonej transakcji, zostało zamknięte. Aby temu zapobiec, musimy do konfiguracji NHibernate dodać następujący wpis:
<property name=“connection.release_mode“>on_close</property>
Na koniec jeszcze jedna niemiła informacja: powyższy sposób zarządzania transakcjami jest niekompatybilny z cache 2-go poziomu NHibernate. Co to oznacza? Otóż modyfikacje dokonane na danych w ramach tak zrealizowanych transakcji nie zostaną uwzględnione w cache 2-go poziomu. Nie sprawia to jednak problemu, jeśli nie modyfikujemy danych cache-owanych. Można więc próbować obejścia, polegającego na stosowaniu obu pokazanych wersji TransactionCallHandler w zależności od tego, czy transakcje rozproszone są wymagane.
Taka strategia powinna sprawdzać się dobrze, ponieważ wymaganie transakcji rozproszonych jest związane z odbieraniem / wysyłaniem komunikatów NServiceBus, a tego rodzaju akcje nie powinny modyfikować danych słownikowych (które są zwykle cache-owane).
Mentoring DDD: NHibernate Fetching Strategies
Jul 28th
Second version of NHibernate is based on the ‘event architecture’ (described here in section 11.2). Some of the most important NHiberate functionalities were rearchitected so that the main code is placed in a event listener. Thanks to this, NHibernate code base is now logically split into two parts: one that defines control flow (the ‘when’ and ‘what’) and one that does actual processing (‘how’). Users gain control over event registration so that every event handler in the system can be substituted by custom one. System is also not limited to one handler per event so one can register his own handler to pre- or postprocess data for the default handler. NHibernate, of course, comes with a set of default handlers which should be sufficient for 99% of cases.
Thanks to this event system, fetching strategy feature can be implemented in NHibernate 2 without modifying the codebase. It could be weaved in the save event processing replacing (in some cases) the default behaviour. My first approach was to write a custom event listener from scratch and register it to be used before the default listener. When special, fetching strategy-related, behaviour was to be used, my custom event handler was to be executed and the default one was to be skipped. Unfortunately, there is no option for prematurely exiting the event handling pipeline. Moreover, the default handler does not have a conditional statement for checking whether object was already fetched so my design forced fetching to be done two times. I had to think out a better approach.
Deriving from default handler seems to be ‘the way to go’ for substituting default fetching actions. I find this approach somewhat awkward since there is no possibility of combining more than one condition-based event handler. That is my first point: if only a premature exit (or to say it differently – cancelable events) capability would be added to the event system, one could seethe system as the Chain of Responsibility implementation.
Chain of Responsibility is in fact an event system (with somewhat static subscriptions usually) where individual chain links can play two roles (you can about roles in software design here): the Interceptor and the Handler. The Interceptor role is responsible for adding non-functional stuff to the system. It can be logging or security for example. The Handler role is responsible for the functional stuff – the main business goal of the system (like fetching data from database).
I see the main advantage of Chain of Responsibility in possibility of providing more than one Handler. NHibernate lacks this feature by default. Fortunately, it can be implemented by changing a little bit (using subclassing) the default handler to check, prior to executing the main code, whether instance was already fetched. If so, skip to the next link in the chain. The drawback of this approach is relying on client code (handler implementation) to properly handle skipping. There is also one more drawback which is inherent to CoR: there is no role separation of Handler and Interceptor. It can lead to interleaving functional and non-functional code in one class.
ASP.NET designers took a different way. They did their best to separate the roles. By doing so they sacrificed the flexibility of having more than one handler: the ASP.NET pipeline can consist of many IHttpModule’s – the Interceptors and precisely one IHttpHandler - the Handler. There is no easy workaround for that.
And one final though in the end: event driven architecture (such as the one of NHibernate or ASP.NET) can be an indication of both good and bad design. Events bring tremendous flexibility to the code: allow substitute almost anything. It is tempting to, when starting a project, bring events in only because of not knowing the actual requirements: I have events, I can plug everything everywhere. That is precisely what should be avoided when doing events. Use them to bring flexibility only where it is needed, not everywhere, not as a technical workaround for poor analysis. Usage of events should be derivative, not a substitute, of analysis. Both NHibernate and ASP.NET are in my opinion fine samples of the ‘good’ approach.



