Ответ 1
Примечание 2. В настоящее время я никогда не буду писать транзакции записи внутри веб-проекта, но вместо этого используйте обмен сообщениями + очереди и у меня есть рабочий в фоновых сообщениях обработки, чтобы сделать транзакционную работу.
Тем не менее, я все же использовал транзакции для чтения для получения согласованных данных; вместе с изоляцией MVCC/Snapshot, из веб-проектов. В этом случае вы обнаружите, что сеанс за запрос за транзакцию отлично работает.Примечание 1 Идеи этого сообщения были помещены в "Замок Transactions" и мой новый NHibernate Facility.
ОК, вот общая идея. Предположим, вы хотите создать неконфигурированный заказ для клиента. У вас есть какой-то графический интерфейс, например. приложение браузера /MVC, которые создают новую структуру данных с соответствующей информацией (или вы получаете эту структуру данных из сети):
[Serializable]
class CreateOrder /*: IMessage*/
{
// immutable
private readonly string _CustomerName;
private readonly decimal _Total;
private readonly Guid _CustomerId;
public CreateOrder(string customerName, decimal total, Guid customerId)
{
_CustomerName = customerName;
_Total = total;
_CustomerId = customerId;
}
// put ProtoBuf attribute
public string CustomerName
{
get { return _CustomerName; }
}
// put ProtoBuf attribute
public decimal Total
{
get { return _Total; }
}
// put ProtoBuf attribute
public Guid CustomerId
{
get { return _CustomerId; }
}
}
Вам нужно что-то, чтобы справиться с этим. Вероятно, это будет обработчик команд в какой-либо служебной шине. Слово "обработчик команд" является одним из многих, и вы можете просто назвать его "службой" или "службой домена" или "обработчиком сообщений". Если бы вы выполняли функциональное программирование, это была бы ваша реализация в окне сообщений, или если вы делали Erlang или Akka, это был бы актер.
class CreateOrderHandler : IHandle<CreateOrder>
{
public void Handle(CreateOrder command)
{
With.Policy(IoC.Resolve<ISession>, s => s.BeginTransaction(), s =>
{
var potentialCustomer = s.Get<PotentialCustomer>(command.CustomerId);
potentialCustomer.CreateOrder(command.Total);
return potentialCustomer;
}, RetryPolicies.ExponentialBackOff.RetryOnLivelockAndDeadlock(3));
}
}
interface IHandle<T> /* where T : IMessage */
{
void Handle(T command);
}
Вышеприведенное показывает использование API, которое вы можете выбрать для данного заданного домена (состояние приложения/транзакция).
Реализация С:
static class With
{
internal static void Policy(Func<ISession> getSession,
Func<ISession, ITransaction> getTransaction,
Func<ISession, EntityBase /* abstract 'entity' base class */> executeAction,
IRetryPolicy policy)
{
//http://fabiomaulo.blogspot.com/2009/06/improving-ado-exception-management-in.html
while (true)
{
using (var session = getSession())
using (var t = getTransaction(session))
{
var entity = executeAction(session);
try
{
// we might not always want to update; have another level of indirection if you wish
session.Update(entity);
t.Commit();
break; // we're done, stop looping
}
catch (ADOException e)
{
// need to clear 2nd level cache, or we'll get 'entity associated with another ISession'-exception
// but the session is now broken in all other regards will will throw exceptions
// if you prod it in any other way
session.Evict(entity);
if (!t.WasRolledBack) t.Rollback(); // will back our transaction
// this would need to be through another level of indirection if you support more databases
var dbException = ADOExceptionHelper.ExtractDbException(e) as SqlException;
if (policy.PerformRetry(dbException)) continue;
throw; // otherwise, we stop by throwing the exception back up the layers
}
}
}
}
}
Как вы можете видеть, нам нужна новая единица работы; ISession каждый раз, когда что-то идет не так. Вот почему цикл находится за пределами используемых операторов/блоков. Наличие функций эквивалентно наличию экземпляров factory, за исключением того, что мы вызываем непосредственно экземпляр объекта, а не вызываем метод на нем. Это делает более приятным caller-API imho.
Мы хотим довольно плавно обрабатывать то, как мы выполняем повторные попытки, поэтому у нас есть интерфейс, который может быть реализован различными обработчиками, называемыми IRetryHandler. Должно быть возможно связать их для каждого аспекта (да, он очень близок к AOP), который вы хотите задействовать в потоке управления. Подобно тому, как работает АОП, возвращаемое значение используется для управления потоком управления, но только с истинным/ложным способом, что является нашим требованием.
interface IRetryPolicy
{
bool PerformRetry(SqlException ex);
}
AggregateRoot, PotentialCustomer - это объект со сроком службы. Это то, что вы будете сопоставлять с файлами *.hbm.xml/FluentNHibernate.
Он имеет метод, который соответствует 1:1 с отправленной командой. Это делает обработчики команд совершенно очевидными для чтения.
Кроме того, с динамическим языком с утиным типом, он позволит вам сопоставлять имена типов команд с методами, аналогичными тому, как это делает Ruby/Smalltalk.
Если вы выполняли поиск событий, обработка транзакций была бы одинаковой, за исключением того, что транзакция не будет взаимодействовать с NHibernate. Следствием является то, что вы сохранили бы события, созданные путем вызова CreateOrder (десятичный), и предоставили вашей организации механизм для повторного чтения сохраненных событий из хранилища.
Последний момент, чтобы заметить, что я переопределяю три метода, которые я создал. Это требование со стороны NHibernate, так как ему нужен способ узнать, когда сущность равна другой, если они находятся в наборах/сумках. Подробнее о моей реализации здесь. В любом случае, это пример кода, и сейчас я не забочусь о своем клиенте, поэтому я не реализую их:
sealed class PotentialCustomer : EntityBase
{
public void CreateOrder(decimal total)
{
// validate total
// run business rules
// create event, save into event sourced queue as transient event
// update private state
}
public override bool IsTransient() { throw new NotImplementedException(); }
protected override int GetTransientHashCode() { throw new NotImplementedException(); }
protected override int GetNonTransientHashCode() { throw new NotImplementedException(); }
}
Нам нужен метод создания политик повтора. Конечно, мы могли бы сделать это разными способами. Здесь я сочетаю свободный интерфейс с экземпляром того же объекта того же типа, что и тип статического метода. Я реализую интерфейс явно, чтобы никакие другие методы не были видны в свободном интерфейсе. Этот интерфейс использует только мои "примерные" реализации ниже.
internal class RetryPolicies : INonConfiguredPolicy
{
private readonly IRetryPolicy _Policy;
private RetryPolicies(IRetryPolicy policy)
{
if (policy == null) throw new ArgumentNullException("policy");
_Policy = policy;
}
public static readonly INonConfiguredPolicy ExponentialBackOff =
new RetryPolicies(new ExponentialBackOffPolicy(TimeSpan.FromMilliseconds(200)));
IRetryPolicy INonConfiguredPolicy.RetryOnLivelockAndDeadlock(int retries)
{
return new ChainingPolicy(new[] {new SqlServerRetryPolicy(retries), _Policy});
}
}
Нам нужен интерфейс для частичного полного вызова на свободный интерфейс. Это дает нам безопасность типов. Поэтому нам нужно два оператора разыменования (т.е. "Полная остановка" - (.)), От нашего статического типа, до завершения настройки политики.
internal interface INonConfiguredPolicy
{
IRetryPolicy RetryOnLivelockAndDeadlock(int retries);
}
Политика цепочки может быть решена. Его реализация проверяет, что все его дети возвращаются, и, как он проверяет это, он также выполняет в них логику.
internal class ChainingPolicy : IRetryPolicy
{
private readonly IEnumerable<IRetryPolicy> _Policies;
public ChainingPolicy(IEnumerable<IRetryPolicy> policies)
{
if (policies == null) throw new ArgumentNullException("policies");
_Policies = policies;
}
public bool PerformRetry(SqlException ex)
{
return _Policies.Aggregate(true, (val, policy) => val && policy.PerformRetry(ex));
}
}
Эта политика позволяет текущему потоку сбрасывать некоторое количество времени; иногда база данных перегружается, а наличие нескольких читателей/писателей, постоянно пытающихся читать, будет де-факто DOS-атакой в базе данных (см., что произошло несколько месяцев назад, когда фреймпад разбился, потому что их кеш-серверы все запросили свои базы данных в то же самое время время).
internal class ExponentialBackOffPolicy : IRetryPolicy
{
private readonly TimeSpan _MaxWait;
private TimeSpan _CurrentWait = TimeSpan.Zero; // initially, don't wait
public ExponentialBackOffPolicy(TimeSpan maxWait)
{
_MaxWait = maxWait;
}
public bool PerformRetry(SqlException ex)
{
Thread.Sleep(_CurrentWait);
_CurrentWait = _CurrentWait == TimeSpan.Zero ? TimeSpan.FromMilliseconds(20) : _CurrentWait + _CurrentWait;
return _CurrentWait <= _MaxWait;
}
}
Аналогично, в любой хорошей SQL-системе нам нужно обрабатывать взаимоблокировки. Мы не можем на самом деле планировать их подробно, особенно при использовании NHibernate, кроме ведения строгой политики транзакций - не подразумеваемых транзакций; и будьте осторожны с Open-Session-In-View. Существует также проблема декартовых продуктов /N + 1 выбирает проблему, которую вам нужно иметь в виду, если вы извлекаете много данных. Вместо этого у вас может быть ключевое слово Multi-Query или HQL 'fetch'.
internal class SqlServerRetryPolicy : IRetryPolicy
{
private int _Tries;
private readonly int _CutOffPoint;
public SqlServerRetryPolicy(int cutOffPoint)
{
if (cutOffPoint < 1) throw new ArgumentOutOfRangeException("cutOffPoint");
_CutOffPoint = cutOffPoint;
}
public bool PerformRetry(SqlException ex)
{
if (ex == null) throw new ArgumentNullException("ex");
// checks the ErrorCode property on the SqlException
return SqlServerExceptions.IsThisADeadlock(ex) && ++_Tries < _CutOffPoint;
}
}
Вспомогательный класс, чтобы код лучше читался.
internal static class SqlServerExceptions
{
public static bool IsThisADeadlock(SqlException realException)
{
return realException.ErrorCode == 1205;
}
}
Не забывайте обрабатывать сетевые сбои в IConnectionFactory (делегируя, возможно, через реализацию IConnection).
PS: Session-per-request - это сломанный шаблон, если вы не только читаете. Особенно, если вы делаете чтение с тем же ISession, с которым вы пишете, и вы не заказываете такие чтения, чтобы они были все, всегда, перед записью.