Транзакции в шаблоне репозитория - PullRequest
37 голосов
/ 22 февраля 2009

Как мне инкапсулировать сохранение более чем одной сущности транзакционным способом с использованием шаблона хранилища? Например, что если я захочу добавить заказ и обновить статус клиента на основе этого создания заказа, но сделать это только в том случае, если заказ завершен успешно? Имейте в виду, что для этого примера заказы не являются коллекцией внутри клиента. Они - их собственная сущность.

Это просто надуманный пример, поэтому мне все равно, должны ли заказы быть или не должны находиться внутри объекта клиента или даже в том же ограниченном контексте. Мне действительно все равно, какая базовая технология будет использоваться (nHibernate, EF, ADO.Net, Linq и т. Д.). Я просто хочу посмотреть, как может выглядеть некоторый вызывающий код в этом заведомо надуманном примере операции «все или ничего».

Ответы [ 6 ]

13 голосов
/ 23 февраля 2009

Загрузка моего компьютера этим утром Я столкнулся с точной проблемой для проекта, над которым я работаю. У меня были некоторые идеи, которые привели к следующему дизайну - и комментарии были бы более чем удивительными. К сожалению, предложенный Джошем дизайн невозможен, так как мне приходится работать с удаленным сервером SQL и не могу включить службу координатора распределенных транзакций, на которую он опирается.

Мое решение основано на нескольких еще простых изменениях в моем существующем коде.

Во-первых, у меня во всех моих репозиториях реализован простой интерфейс маркеров:

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

Во-вторых, я позволил всем своим репозиториям с поддержкой транзакций реализовать следующий интерфейс:

/// <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();
}

Идея состоит в том, что во всех моих репозиториях я реализую этот интерфейс и добавляю код, который вводит транзакцию, напрямую зависящую от фактического провайдера (для поддельных репозиториев я составил список делегатов, который выполняется при фиксации). Для LINQ to SQL было бы легко создать такие реализации, как:

#region IHasTransactions Members

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

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

#endregion

Это, конечно, требует создания нового класса репозитория для каждого потока, но это разумно для моего проекта.

Каждый метод, использующий хранилище, должен вызывать BeginTransaction() и EndTransaction(), если хранилище реализует IHasTransactions. Чтобы сделать этот вызов еще проще, я предложил следующие расширения:

/// <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();
        }
    }
}

Комментарии приветствуются!

8 голосов
/ 22 февраля 2009

Я бы посмотрел на использование какого-либо типа области действия / контекста транзакции. Таким образом, у вас может быть следующий код, который примерно основан на .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 может быть вложенным, поэтому предположим, что у вас есть действие, которое пересекает несколько служб, и ваше приложение также создаст TransactionScope. Теперь в текущем .net, если вы используете TransactionScope, вы рискуете перейти на DTC, но это будет решено в будущем.

Мы создали наш собственный класс TransactionScope, который в основном управлял нашими подключениями к БД и использовал локальные транзакции SQL.

5 голосов
/ 22 февраля 2009

Используя Spring.NET AOP + NHibernate, вы можете написать свой класс репозитория как обычно и настроить транзакции в пользовательском 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
    }
}

В файле XML вы выбираете, какие методы вы хотели бы выполнить внутри транзакции:

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

И в своем коде вы получаете экземпляр класса CustomerService, например:

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

Spring.NET вернет вам прокси-сервер класса CustomerService, который будет применять транзакцию при вызове метода CreateOrder. Таким образом, внутри ваших классов обслуживания нет кода, специфичного для транзакции. АОП позаботится об этом. Для более подробной информации вы можете взглянуть на документацию Spring.NET .

3 голосов
/ 22 февраля 2009

Как мне сохранить сохранение более одного лица в транзакционный способ с использованием шаблон хранилища? Например, что если бы я хотел добавить заказ и обновить статус клиента на основе этого создание заказа, но только если заказ выполнен успешно? Держать в помните, что для этого примера заказы не коллекция внутри клиента. Они являются их собственной сущностью.

Это не ответственность хранилища, это обычно что-то, что делается на более высоком уровне. Хотя вы и сказали, что не интересуетесь конкретными технологиями, я думаю, что стоит связать решения, например, при использовании NHibernate с веб-приложением, вы, вероятно, рассмотрите возможность использования сеанс на запрос .

Так что, если вы можете управлять транзакциями на более высоком уровне, тогда у меня будет два варианта:

  1. Предварительная проверка - Например, в службе, координирующей поведение, решите, хотите ли вы продолжить, спросив Заказ / Заказчика, если любой из них скажет, что нет, даже не пытайтесь обновить любой из них.
  2. Откат - Просто продолжите обновление Клиента / Заказа и, если что-то не получится, откатите транзакцию базы данных.

Если вы выберете второй вариант, тогда возникает вопрос: что происходит с объектами в памяти, ваш клиент может остаться в несогласованном состоянии. Если это имеет значение, и я работаю в сценариях, где это не так, поскольку объект был загружен только для этого запроса, то я бы рассмотрел предварительную проверку, если это возможно, потому что это намного проще, чем альтернативы (откат -память изменения или перезагрузить объекты).

3 голосов
/ 22 февраля 2009

Вы хотите посмотреть на реализацию шаблона единицы работы. Есть реализации для NHibernate. Один из них находится в проекте Rhino Commons, есть также Machine.UoW.

0 голосов
/ 01 июня 2019

Вы можете добавить параметр транзакции в конец методов, которые вы хотите запустить в транзакции, и присвоить ему значение по умолчанию, равное нулю. Таким образом, если вы не хотите запускать метод в существующей транзакции, не указывайте параметр end или явно передайте значение null.

Внутри этих методов вы можете проверить параметр на null, чтобы определить, создавать ли новую транзакцию или использовать другую переданную. Эта логика может быть передана, например, в базовый класс.

Это сохраняет ваши методы более чистыми, чем при использовании решения на основе контекста, хотя последнее, вероятно, лучше работает для универсальной библиотеки. Однако в автономном приложении вы знаете, какие методы необходимо связать в цепочку внутри транзакции, и это не все из них.

void Update(int itemId, string text, IDbTransaction trans = null) =>
   RunInTransaction(ref trans, () =>
   {
      trans.Connection.Update("...");
   });

void RunInTransaction(ref IDbTransaction transaction, Action f)
{
    if (transaction == null)
    {
        using (var conn = DatabaseConnectionFactory.Create())
        {
            conn.Open();

            using (transaction = conn.BeginTransaction())
            {
                f();

                transaction.Commit();
            }
        }
    }
    else
    {
        f();
    }
}

Update(1, "Hello World!");
Update(1, "Hello World!", transaction);

Тогда у вас может быть обработчик транзакций для вашего уровня обслуживания ...

public void RunInTransaction(Action<IDbTransaction> f)
{
   using (var conn = DatabaseConnectionFactory.Create())
   {
      conn.Open();

      using (var transaction = conn.BeginTransaction())
      {
         f(transaction);

         transaction.Commit();
      }
   }
}

А сервисный метод может выглядеть так ...

void MyServiceMethod(int itemId, string text1, string text2) =>
   transactionRunner.RunInTransaction(trans =>
   {
      repos.UpdateSomething(itemId, text1, trans);
      repos.UpdateSomethingElse(itemId, text2, trans);
   });

Что легко подделать для юнит-тестирования ...

public class MockTransactionRunner : ITransactionRunner
{
   public void RunInTransaction(Action<IDbTransaction> f) => f(null);
}
...