Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Simon says...

Szymon Pobiega o architekturze i inżynierii oprogramowania
NHibernate, NServiceBus i transakcje
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)

    1 /// <summary>

    2 /// Cargo booking service.

    3 /// </summary>

    4 public interface IBookingService

    5 {

    6    /// <summary>

    7    /// Registers a new cargo in the tracking system, not yet routed.

    8    /// </summary>

    9    /// <param name="origin">Cargo origin UN locode.</param>

   10    /// <param name="destination">Cargo destination UN locode.</param>

   11    /// <param name="arrivalDeadline">Arrival deadline.</param>

   12    /// <returns>Cargo tracing id for referencing this cargo.</returns>

   13    TrackingId BookNewCargo(UnLocode origin, UnLocode destination, DateTime arrivalDeadline);

   14    //...//


   15 /// <summary>

   16 /// Implementation of <see cref="IBookingService"/>.

   17 /// </summary>

   18 public class BookingService : IBookingService

   19 {     

   20    public TrackingId BookNewCargo(UnLocode originUnLocode, UnLocode destinationUnLocode, DateTime arrivalDeadline)

   21    {

   22       Location origin = _locationRepository.Find(originUnLocode);

   23       Location destination = _locationRepository.Find(destinationUnLocode);

   24       //...


Korzystam z tego w kodzie konfigurującym kontener DI:

    1 container.Configure<Interception>()

    2    //Ustaw sposób przechwytywania

    3    .SetInterceptorFor<IBookingService>(new InterfaceInterceptor())

    4    .SetInterceptorFor<IHandlingEventService>(new InterfaceInterceptor())

    5    //Dodaj nową "politykę"

    6    .AddPolicy("Transactions")

    7    //Wykorzystującą aspekt obsługi transakcji

    8    .AddCallHandler<TransactionCallHandler>()

    9    //I podłącz do wszystkich interfejsów z assembly

   10    .AddMatchingRule(new AssemblyMatchingRule("DDDSample.Application"));


Dzięki temu każde wywołanie fasad z warstwy Application automatycznie opakowywane jest w transakcje. Pozostaje jeszcze wyjaśnić, jak wygląda wspomniany aspekt. Oto kluczowy kawałek kodu:

    1 public class TransactionCallHandler : ICallHandler

    2 {

    3    private readonly ISessionFactory _sessionFactory;

    4    public TransactionCallHandler(ISessionFactory sessionFactory)

    5    {

    6       _sessionFactory = sessionFactory;

    7    }

    8    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)

    9    {

   10       using (ITransaction tx = _sessionFactory.GetCurrentSession().BeginTransaction())

   11       {

   12          IMethodReturn result = getNext()(input, getNext);

   13          if (result.Exception == null)

   14          {

   15             tx.Commit();

   16          }

   17          return result;

   18       }

   19    }

   20    public int Order { get; set;}     

   21 }


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.

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:

    8    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)

    9    {

   10       IMethodReturn result;

   11       SqlConnection sqlConnection = _sessionFactory.GetCurrentSession().Connection as SqlConnection;

   12       using (TransactionScope tx = new TransactionScope())

   13       {           

   14          if (sqlConnection == null)

   15          {

   16             throw new NotSupportedException("Only SqlConnection is supported.");

   17          }

   18          sqlConnection.EnlistTransaction(Transaction.Current);

   19          result = getNext()(input, getNext);

   20          if (result.Exception == null)

   21          {

   22             _sessionFactory.GetCurrentSession().Flush();

   23             tx.Complete();

   24          }                       

   25       }

   26       sqlConnection.EnlistTransaction(null);

   27       return result;        

   28    }


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).

Opublikowane 13 stycznia 2010 18:19 przez simon

Filed under: ,

Komentarze:

# Simon says... : NHibernate, NServiceBus i transakcje @ 13 stycznia 2010 19:16

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

dotnetomaniak.pl

Komentarze anonimowe wyłączone