31

Jak hermetyzować zapisywanie więcej niż jednej jednostki w sposób transakcyjny za pomocą wzorca repozytorium? Na przykład, co jeśli chciałbym dodać zamówienie i zaktualizować status klienta na podstawie tego zamówienia, ale zrobić to tylko, jeśli zamówienie zakończyło się pomyślnie? Pamiętaj, że w tym przykładzie zamówienia nie są zbiorem wewnątrz klienta. Są ich własną istotą.Transakcje w sygnaturze repozytorium

To tylko wymyślny przykład, więc nie obchodzi mnie, czy zamówienia powinny lub nie powinny znajdować się w obiekcie klienta, czy nawet w tym samym ograniczonym kontekście. Naprawdę nie obchodzi mnie, jaka będzie podstawowa technologia (nHibernate, EF, ADO.Net, Linq, itp.). Chcę tylko zobaczyć, jak może wyglądać kod wywołujący w tym, wprawdzie spreparowanym, przykładzie operacji "wszystko albo nic".

Odpowiedz

7

Chciałbym przyjrzeć się użyciu pewnego rodzaju systemu Zakres/Kontekst transakcji. Więc możesz mieć następujący kod, który jest w przybliżeniu oparty na .Net & C#.

public class OrderService 
{ 

public void CreateNewOrder(Order order, Customer customer) 
{ 
    //Set up our transactional boundary. 
    using (TransactionScope ts=new TransactionScope()) 
    { 
    IOrderRepository orderRepos=GetOrderRespository(); 
    orderRepos.SaveNew(order); 
    customer.Status=CustomerStatus.OrderPlaced; 

    ICustomerRepository customerRepository=GetCustomerRepository(); 
    customerRepository.Save(customer) 
    ts.Commit(); 
    } 
} 
} 

TransactionScope mogą zagnieździć więc powiedzmy, że trzeba było działanie, które przekroczyły aplikacja stworzy TransactionScope jak również wiele usług. Teraz w bieżącej .net, jeśli korzystasz z TransactionScope, masz ryzyko ewakuacji do DTC, ale to zostanie rozwiązane w przyszłości.

Stworzyliśmy własną klasę TransactionScope, która w zasadzie zarządzała naszymi połączeniami DB i korzystała z lokalnych transakcji SQL.

+0

Nie sądzę, że jest to rozwiązanie w duchu DDD. Zasadniczo stworzyłeś skrypt transakcji, który wykonuje zadanie Modelu Domeny. Usługa nie powinna na przykład zmieniać statusu klienta. –

+1

Coś w kodzie musi obsłużyć tę regułę biznesową, czy to na tym poziomie, czy na wyższym poziomie, chodziło o to, aby dokonać zmian w ramach jednego TransactionScope, umożliwiając transakcje lokalne lub transakcje rozproszone do obsługi transakcji.Jeśli reguła biznesowa mówi o aktualizacji klienta przy każdym złożeniu zamówienia, jest to dobre miejsce, aby poradzić sobie z tym, ponieważ wszystkie zamówienia przechodzą tutaj. – JoshBerke

3

Chcesz spojrzeć na wdrażanie wzoru jednostki pracy. Istnieją implementacje dla NHibernate. Jeden z nich znajduje się w projekcie Rhino Commons, jest tam także Machine.UoW.

5

Korzystanie Spring.NET AOP + NHibernate Możesz napisać swoje repozytorium klasy jako normalne i skonfigurować swoje transakcje w niestandardowym pliku XML:

public class CustomerService : ICustomerService 
{ 
    private readonly ICustomerRepository _customerRepository; 
    private readonly IOrderRepository _orderRepository; 

    public CustomerService(
     ICustomerRepository customerRepository, 
     IOrderRepository orderRepository) 
    { 
     _customerRepository = customerRepository; 
     _orderRepository = orderRepository; 
    } 

    public int CreateOrder(Order o, Customer c) 
    { 
     // Do something with _customerRepository and _orderRepository 
    } 
} 

w pliku XML wybranej metody, które chciałbyś być wykonywane wewnątrz transakcja:

<object id="TxProxyConfigurationTemplate" 
      abstract="true" 
      type="Spring.Transaction.Interceptor.TransactionProxyFactoryObject, Spring.Data"> 

    <property name="PlatformTransactionManager" ref="HibernateTransactionManager"/> 

    <property name="TransactionAttributes"> 
     <name-values> 
     <add key="Create*" value="PROPAGATION_REQUIRED"/> 
     </name-values> 
    </property> 
    </object> 

    <object id="customerService" parent="TxProxyConfigurationTemplate"> 
    <property name="Target"> 
     <object type="MyNamespace.CustomerService, HibernateTest"> 
      <constructor-arg name="customerRepository" ref="customerRepository" /> 
      <constructor-arg name="orderRepository" ref="orderRepository" /> 
     </object> 
    </property> 

    </object> 

I w kodzie uzyskać instancję klasy customerservice tak:

ICustomerService customerService = (ICustomerService)ContextRegistry 
    .GetContent() 
    .GetObject("customerService"); 

Spring.NET zwróci ci proxy klasy CustomerService, która zastosuje transakcję, gdy zadzwonisz do metody CreateOrder. W ten sposób nie ma żadnego specyficznego dla transakcji kodu wewnątrz twoich klas usług. AOP zajmuje się tym. Więcej szczegółów można znaleźć w dokumentacji Spring.NET.

2

Jak hermetyzacji oszczędność więcej niż jeden podmiot w sposób transakcyjnej używając repozytorium wzór? Na przykład, jeśli chcę dodać zamówienie i zaktualizować status klienta w oparciu o to zamówienie na zamówienie , ale zrobić to tylko, jeśli zlecenie zakończyło się pomyślnie? Pamiętaj, że w tym przykładzie zamówienia to , a nie kolekcja wewnątrz klienta. Są to ich własne istoty.

Nie należy do repozytorium, zwykle jest to coś, co dzieje się na wyższym poziomie.Chociaż powiedziałeś, że nie interesujesz się konkretnymi technologiami, myślę, że warto przymocować rozwiązania, na przykład przy korzystaniu z NHibernate z aplikacją internetową prawdopodobnie rozważasz użycie numeru session-per request.

Więc jeśli można zarządzać transakcjami na wyższym poziomie, a następnie moje dwie opcje byłyby:

  1. Upfront check - Na przykład w usłudze koordynowanie zachowań zdecydować, czy chcesz kontynuować zadając Zlecenie/klient, jeśli ktoś twierdzi, że nie, to nawet nie próbuje zaktualizować żadnego z nich.
  2. Wycofanie - Po prostu kontynuuj aktualizację klienta/zamówienia, a jeśli coś się nie powiedzie, wycofaj transakcję bazy danych.

Jeśli wybierzesz opcję drugą, pytanie, co dzieje się z obiektami znajdującymi się w pamięci, oznacza, że ​​Twój klient może pozostawać w niespójnym stanie. Jeśli to ma znaczenie, i pracuję w scenariuszach, w których nie, ponieważ obiekt został załadowany tylko do tego wniosku, wtedy rozważałbym kontrolę wstępną, jeśli jest to możliwe, ponieważ jest o wiele łatwiejsza niż alternatywy (cofanie w -memory zmienia lub przeładowuje obiekty).

+1

Dlaczego to nie jest odpowiedzialność repozytorium? Czy cały pomysł nie polega na abstrakcyjnym operowaniu bazami danych poza modelem domeny? Dla mnie repozytorium jest najlepszym miejscem do umieszczenia tego wsparcia transakcyjnego. –

12

Dziś rano, pracując nad komputerem, stanąłem przed problemem, nad którym pracuję. Miałem kilka pomysłów, które doprowadziły do ​​następującego projektu - a komentarze byłyby więcej niż niesamowite. Niestety projekt zaproponowany przez Josha nie jest możliwy, ponieważ muszę pracować ze zdalnym serwerem SQL i nie mogę włączyć usługi Distribute Transaction Coordinator.

Moje rozwiązanie opiera się na kilku, ale prostych zmianach w istniejącym kodzie.

Po pierwsze, mam wszystkie moje repozytoria zaimplementować prosty interfejs markera:

/// <summary> 
/// A base interface for all repositories to implement. 
/// </summary> 
public interface IRepository 
{ } 

Po drugie, niech wszyscy moi transakcyjne włączone repozytoria wdrożyć następujący interfejs:

/// <summary> 
/// Provides methods to enable transaction support. 
/// </summary> 
public interface IHasTransactions : IRepository 
{ 
    /// <summary> 
    /// Initiates a transaction scope. 
    /// </summary> 
    void BeginTransaction(); 

    /// <summary> 
    /// Executes the transaction. 
    /// </summary> 
    void CommitTransaction(); 
} 

Chodzi o to, że w wszystkie moje repozytoria, zaimplementowałem ten interfejs i dodałem kod, który wprowadza transakcję bezpośrednio w zależności od faktycznego dostawcy (dla fałszywych repozytoriów sporządziłem listę delegatów, która zostanie wykonana przy zatwierdzeniu). Dla LINQ to SQL byłoby łatwe do wdrożenia, takie jak:

#region IHasTransactions Members 

public void BeginTransaction() 
{ 
    _db.Transaction = _db.Connection.BeginTransaction(); 
} 

public void CommitTransaction() 
{ 
    _db.Transaction.Commit(); 
} 

#endregion 

Wymaga to oczywiście, że nowa klasa repozytorium jest tworzony dla każdego wątku, ale jest to uzasadnione dla mojego projektu.

Każda metoda używająca repozytorium musi wywoływać BeginTransaction() i EndTransaction(), jeśli repozytorium implementuje IHasTransactions. Aby ułatwić to połączenie, pojawiły się następujące rozszerzenia:

/// <summary> 
/// Extensions for spawning and subsequently executing a transaction. 
/// </summary> 
public static class TransactionExtensions 
{ 
    /// <summary> 
    /// Begins a transaction if the repository implements <see cref="IHasTransactions"/>. 
    /// </summary> 
    /// <param name="repository"></param> 
    public static void BeginTransaction(this IRepository repository) 
    { 
     var transactionSupport = repository as IHasTransactions; 
     if (transactionSupport != null) 
     { 
      transactionSupport.BeginTransaction(); 
     } 
    } 

    public static void CommitTransaction(this IRepository repository) 
    { 
     var transactionSupport = repository as IHasTransactions; 
     if (transactionSupport != null) 
     { 
      transactionSupport.CommitTransaction(); 
     } 
    } 
} 

Komentarze są za mile widziane!

+0

Można również użyć wariantu i utworzyć instancję repozytorium dla każdej transakcji, umieścić ją w instrukcji using i pozwolić transakcji Dispose(). To abstrakcja musiał wiedzieć o transakcji w metodzie wywołującego. –

+0

To jest całkiem słodkie. –

+0

Bardzo interesujące rozwiązanie ... –