Транзакции в шаблоне репозитория
Как инкапсулировать сохранение более чем одной сущности транзакционным способом с использованием шаблона хранилища? Например, что если я захочу добавить заказ и обновить статус клиента на основе этого создания заказа, но сделать это только в том случае, если заказ выполнен успешно? Имейте в виду, что для этого примера заказы не являются коллекцией внутри клиента. Они их собственная сущность.
Это просто надуманный пример, поэтому мне все равно, должны ли заказы быть или не должны быть внутри объекта клиента или даже в том же ограниченном контексте. Мне действительно все равно, какая базовая технология будет использоваться (nHibernate, EF, ADO.Net, Linq и т. Д.). Я просто хочу посмотреть, как может выглядеть некоторый вызывающий код в этом заведомо надуманном примере операции "все или ничего".
7 ответов
Загружая свой компьютер этим утром, я столкнулся с точной проблемой для проекта, над которым я работаю. У меня были некоторые идеи, которые привели к следующему дизайну - и комментарии были бы более чем удивительными. К сожалению, предложенный Джошем дизайн невозможен, так как мне приходится работать с удаленным сервером 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();
}
}
}
Комментарии приветствуются!
Я хотел бы взглянуть на использование какого-либо типа области действия / контекста транзакции. Таким образом, у вас может быть следующий код, который примерно основан на.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.
Как инкапсулировать сохранение более чем одной сущности транзакционным способом с использованием шаблона хранилища? Например, что если я захочу добавить заказ и обновить статус клиента на основе этого создания заказа, но сделать это только в том случае, если заказ выполнен успешно? Имейте в виду, что для этого примера заказы не являются коллекцией внутри клиента. Они их собственная сущность.
Это не ответственность хранилища, это обычно что-то, что делается на более высоком уровне. Хотя вы сказали, что не интересуетесь конкретными технологиями, я думаю, что стоит связать решения, например, при использовании NHibernate с веб-приложением, вы, вероятно, рассмотрите возможность использования сеанса на запрос.
Так что, если вы можете управлять транзакциями на более высоком уровне, то у меня есть два варианта:
- Предварительная проверка - например, в службе, координирующей поведение, решите, хотите ли вы продолжить, спросив Заказ / Заказчика, если любой из них скажет, что нет, то даже не пытайтесь обновить ни одну из них.
- Откат - Просто продолжите обновление Customer/Order и, если что-то не получится, откатите транзакцию базы данных.
Если вы выберете второй вариант, вопрос в том, что происходит с объектами в памяти, ваш клиент может остаться в несогласованном состоянии. Если это имеет значение, и я работаю в сценариях, где это не так, поскольку объект был загружен только для этого запроса, то я рассмотрел бы предварительную проверку, если это возможно, потому что это намного проще, чем альтернативы (откат -смена памяти или перезагрузка объектов).
Используя 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.
Вы хотите посмотреть на реализацию шаблона единицы работы. Есть реализации для NHibernate. Один из них находится в проекте Rhino Commons, есть также Machine.UoW.
Вы можете добавить параметр транзакции в конец методов, которые вы хотите запустить в транзакции, и присвоить ему значение по умолчанию null. Таким образом, если вы не хотите запускать метод в существующей транзакции, не указывайте параметр 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);
}
От Эрика Эванса, DDD Book, CH 6, Repositories:
Оставьте управление транзакцией клиенту. Хотя РЕПОЗИТОРИЙ будет вставлять и удалять из базы данных, обычно он ничего не фиксирует. Например, возникает соблазн выполнить фиксацию после сохранения, но у клиента, по-видимому, есть контекст для правильного запуска и фиксации единиц работы. Управление транзакциями будет проще, если РЕПОЗИТОРИЙ будет держаться подальше.
Приятно позволить более высокому уровню выполнять транзакционное управление:
- При работе с двумя или более совокупными корнями вы должны убедиться, что оба находятся в согласованном состоянии.
- У клиентского кода обычно больше контекста, чтобы действовать в случае ошибки в работе репозитория.
- Репозиторий по-прежнему сосредоточен на задаче получения / обновления определенного объекта, обычно совокупного корня.