Что атомарно, а что нет
Как уже говорили другие, обновления в SQL Server являются атомарными операциями.Однако при обновлении данных с помощью NHibernate (или любого O / RM) вы обычно сначала select
данных, вносите изменения в объект, а затем update
базу данных с вашими изменениями.Эта последовательность событий не атомная.Даже если выбор и обновление были выполнены в течение миллисекунд друг от друга, существует вероятность того, что другое обновление проскользнет посередине.Если два клиента извлекли одну и ту же версию одних и тех же данных, они могли бы невольно перезаписать изменения друг друга, если бы предположили, что они единственные, кто редактировал эти данные в то время.
Иллюстрация проблемы
Если бы мы не защитились от этого сценария одновременного обновления, могут произойти странные вещи - хитрые ошибки, которые не должны казаться возможными.Предположим, у нас был класс, который моделировал изменения состояния воды:
public class BodyOfWater
{
public virtual int Id { get; set; }
public virtual StateOfMatter State { get; set; }
public virtual void Freeze()
{
if (State != StateOfMatter.Liquid)
throw new InvalidOperationException("You cannot freeze a " + State + "!");
State = StateOfMatter.Solid;
}
public virtual void Boil()
{
if (State != StateOfMatter.Liquid)
throw new InvalidOperationException("You cannot boil a " + State + "!");
State = StateOfMatter.Gas;
}
}
Допустим, в базе данных записан следующий водоем:
new BodyOfWater
{
Id = 1,
State = StateOfMatter.Liquid
};
Два пользователя выбирают эту запись изПримерно в то же время измените базу данных и сохраните изменения в базе данных.Пользователь А замерзает вода:
using (var transaction = sessionA.BeginTransaction())
{
var water = sessionA.Get<BodyOfWater>(1);
water.Freeze();
sessionA.Update(water);
// Same point in time as the line indicated below...
transaction.Commit();
}
Пользователь Б пытается кипятить воду (теперь лед!) ...
using (var transaction = sessionB.BeginTransaction())
{
var water = sessionB.Get<BodyOfWater>(1);
// ... Same point in time as the line indicated above.
water.Boil();
sessionB.Update(water);
transaction.Commit();
}
... и успешно !!!Какие?Пользователь А заморозил воду.Разве не должно быть выброшено исключение, говорящее «Вы не можете варить твердое тело!»?Пользователь B извлек данные до того, как Пользователь A сохранил свои изменения, поэтому для обоих пользователей вода изначально представляла собой жидкость, поэтому обоим пользователям было разрешено сохранять свои конфликтующие изменения состояния.
Solution
Чтобы предотвратить этот сценарий, мы можем добавить свойство Version
к классу и отобразить его в NHibernate с отображением <version />
:
public virtual int Version { get; set; }
Это просто числочто NHibernate будет увеличивать каждый раз, когда обновляет запись, и он проверит, чтобы никто не увеличил версию, пока мы не наблюдали.Вместо наивного параллельного обновления sql, например ...
update BodyOfWater set State = 'Gas' where Id = 1;
... NHibernate теперь будет использовать более умный запрос, подобный следующему:
update BodyOfWater set State = 'Gas', Version = 2 where Id = 1 and Version = 1;
Если на число строк влияетзапрос равен 0, тогда NHibernate знает, что что-то пошло не так - либо кто-то еще обновил строку, чтобы номер версии теперь был неправильным, либо кто-то удалил строку, чтобы этот Id больше не существовал.Затем NHibernate выдаст StaleObjectStateException
.
Специальное примечание о веб-приложениях
Чем больше промежуток времени между начальным select
данных и последующим update
, тем большешанс для этого типа проблемы параллелизма.Рассмотрим типичную форму редактирования в веб-приложении.Существующие данные для объекта выбираются из базы данных, помещаются в форму HTML и отправляются в браузер.Пользователь может потратить несколько минут на изменение значений в форме, прежде чем отправить их обратно на сервер.Может быть вполне реальный шанс, что кто-то еще редактировал ту же информацию одновременно, и они сохранили свои изменения раньше, чем мы.
Убедившись, что версия не меняется в течение тех нескольких миллисекунд, которые мы на самом делеСохранение изменений может быть недостаточно в таком сценарии.Чтобы решить эту проблему, вы можете отправить номер версии в браузер как скрытое поле вместе с остальными полями формы, а затем проверить, чтобы убедиться, что версия не изменилась, когда вы извлекаете сущность из базы данных перед сохранением.,Кроме того, вы можете ограничить промежуток времени между начальным select
и окончательным update
, предоставляя отдельные представления «view» и «edit» вместо того, чтобы просто использовать представление «edit» для всего.Чем меньше времени пользователь тратит на представление «редактирование», тем меньше вероятность того, что ему будет выдано досадное сообщение об ошибке, в котором говорится, что его изменения не могут быть сохранены.