Пишу небольшой утиль для управления складом, в системе работает порядка 100 пользователей, кладовщиков, продавцов и т.д. Есть WCF сервис (InstanceContextMode = PerSession), который предоставляет функционал для типовых складских операций, внутри использует NHibernate. Возник вопрос, как сделать блокировки данных при изменении одной и той же записи на складе разными пользователями одновременно. Например кейс: есть на складе 100 полотенец, два продавца одновременно оформляют продажу один на 40, второй на 60, при оформлении продажи у сервиса вызывается метод, что-то типа такого: Sell(Sell value, params SellEntry [ ] entries); возникает вопрос, что будет происходить в NHibernate в случае такой реализации метода Sell (см. ниже). Конкретно интересует как заставить NHibernate кидать исключение и отменять транзакцию при попытке продать товара больше чем есть?
[OperationContract]
public Sell Sell(Sell value, params SellEntry [ ] entries)
{
decimal totalCost = 0.0M;
using ( ISession session = DataEngine.Factory.OpenSession ( ) )
{
using ( ITransaction transaction = session.BeginTransaction ( ) )
{
value.Saller = GetCurrentUser ( );
value.Date = DateTime.Now;
StringBuilder builder = new StringBuilder ( 128 );
foreach ( var current in entries )
builder.AppendFormat ( "{0},", current.Id );
string s = builder.ToString ( );
string text = string.Format ( "Select * from Warehouse where Warehouse.Id in (0)", s.Remove ( s.Length - 1, 1 ) );
IEnumerable<WarehouseEntry> whEntries = session.CreateSQLQuery ( text ).List<WarehouseEntry> ( );
foreach ( var current in entries )
{
foreach ( var refreshed in whEntries )
if ( current.WhEntry.Id == refreshed.Id )
{
if ( refreshed.Quantity < current.Quantity )
{
string message = string.Format ( "Невозможно продать {0} в количестве {1} штук. Остаток на складе {2} штук",
current.Product.Name, current.Quantity, refreshed.Quantity );
throw new FaultException<ArgumentException> ( new ArgumentException ( message ) );
}
current.WhEntry = refreshed;
break;
}
}
session.Save ( value );
foreach ( var current in entries )
{
current.Sale = value;
current.WhEntry.Quantity -= current.Quantity;
totalCost += current.Cost;
session.Update ( current.WhEntry );
session.Save ( current );
}
Location refreshed = session.CreateSQLQuery ( "Select * from Locations where Locations.Id = :lId" )
.AddEntity ( typeof ( Location ) )
.SetInt64 ( "lId", value.Location.Id )
.List<Location> ( )
.FirstOrDefault ( );
if ( refreshed == null )
{
throw new FaultException<InvalidOperationException> ( new InvalidOperationException (
string.Format ( "Локация {0} не является действительной", value.Location.Name ) ) );
}
refreshed.Cash += totalCost;
session.Update ( refreshed );
value.Location = refreshed;
transaction.Commit ( );
session.Flush ( );
}
}
return value;
}
[DataContract]
public class Sell
{
[DataMember]
public virtual long Id
{
get;
set;
}
[DataMember]
public virtual DateTime Date
{
get;
set;
}
[DataMember]
public virtual User Saller
{
get;
set;
}
[DataMember]
public virtual Price Price
{
get;
set;
}
[DataMember]
public virtual Location Location
{
get;
set;
}
}
[DataContract]
public class SellEntry
{
[DataMember]
public virtual long Id
{
get;
set;
}
[DataMember]
public virtual Sell Sell
{
get;
set;
}
[DataMember]
public virtual Product Product
{
get;
set;
}
[DataMember]
public virtual int Quantity
{
get;
set;
}
[DataMember]
public virtual decimal Cost
{
get;
set;
}
[DataMember]
public virtual WarehouseEntry WhEntry
{
get;
set;
}
}
Здравствуйте, Visor2004, Вы писали:
V>Пишу небольшой утиль для управления складом, в системе работает порядка 100 пользователей, кладовщиков, продавцов и т.д. Есть WCF сервис (InstanceContextMode = PerSession), который предоставляет функционал для типовых складских операций, внутри использует NHibernate. Возник вопрос, как сделать блокировки данных при изменении одной и той же записи на складе разными пользователями одновременно. Например кейс: есть на складе 100 полотенец, два продавца одновременно оформляют продажу один на 40, второй на 60, при оформлении продажи у сервиса вызывается метод, что-то типа такого: Sell(Sell value, params SellEntry [ ] entries); возникает вопрос, что будет происходить в NHibernate в случае такой реализации метода Sell (см. ниже). Конкретно интересует как заставить NHibernate кидать исключение и отменять транзакцию при попытке продать товара больше чем есть?
В Nhibernate имеется встроенный механизм оптимистической блокировки. В этом случае для каждой сущности в таблице заводится служебное поле "Version" (название может быть любое). В мэппинге оно указывается как именно поле для хранения версии записи.
Далее сущность считывается, модифицируется и сохраняется.
При сохранении Nhibernate проверяет, что версия в базе та же самая что у сохраняемой сущности и если нет — то выкидывает StaleDataException. Если та же — то сущность сохраняется а version при этом увеличивается на 1.
Соответсвенно, если получил StateDataException на апдейте — значит между тем как ты взял сущность и проверил в ней количество, изменил и сохранил — кто-то успел изменить эту сущность параллельно (например, продал часть товара. Или наоборот — добавил). Значит, надо обновить данные и посмотреть что делать дальше.
Я с NHibernate не работал, но я бы в первую очередь почитал например этот документ: Transactions and Concurrency
а так же погуглил по запросу типа "optimistic locking with NHibernate".
По поводу отмены транзакции в случае исключения, возможно в методе Dispose() транзакции будет произведен откат транзакции в случае если она не была явно принята или отменена.
PS. Вы вообще достаточно знакомы с темой concurrency control? Если нет, то советую еще отдельно почитать на эту тему (например у Фаулера в Архитектуре Корпоративных Приложений, глава 16), иначе в системе с сотней пользователей можно натворить дел..
Здравствуйте, MozgC, Вы писали:
MC>Я с NHibernate не работал, но я бы в первую очередь почитал например этот документ: MC>Transactions and Concurrency MC>а так же погуглил по запросу типа "optimistic locking with NHibernate". MC>По поводу отмены транзакции в случае исключения, возможно в методе Dispose() транзакции будет произведен откат транзакции в случае если она не была явно принята или отменена.
MC>PS. Вы вообще достаточно знакомы с темой concurrency control? Если нет, то советую еще отдельно почитать на эту тему (например у Фаулера в Архитектуре Корпоративных Приложений, глава 16), иначе в системе с сотней пользователей можно натворить дел..
С многопоточностью и конкурентностью никаких проблем нет вообще. Интересует конкретная реализация в NHibernate, неохота перекапывать их исходники если можно спросить на форуме
Т.е. адгоритм получается такой:
1) читаем сущности из БД
2) делаем проверки и модификации
3) пробуем сохранить, если получаем исключение, то в п.1
В идеале хотелось бы иметь что-то типа:
1) получили сущности из кэша
2) сделали изменения
3) попробовали записать, используя какие-то ограничители, типа: итоговое кол-во товара в базе не может быть меньше нуля,
а не просто версию ентити, потому что если на складе было 110 полотенец можно было бы завершить обе транзакции нормально.
4) в случае ошибки ( несоблюдение ограничений заданных в п.3 ) выкинуть исключение и показать пользователю сообщение о необходимости
скорректировать данные продажи.
Здравствуйте, MozgC, Вы писали:
MC>Может я не в тему напишу, не знаю как такое лучше сделать в NHibernate, но вообще я обычно такое делаю так: MC>
UPDATE warehouse SET Quantity = Quantity - X WHERE ID = Y AND Quantity >= X
MC>и проверяю что была изменена 1 строка, а не 0. Если 0 — выбрасываю ConcurrencyException.
Ну, если писать запросы руками, то лучше имхо вообще отказаться от NHibernate и использовать тот же BLToolkit.
+ В вашем подходе надо задавать ограничение в БД и обрабатывать соответствующие ADOException, что тоже выглядит неудобно.
Здравствуйте, Visor2004, Вы писали:
V>+ В вашем подходе надо задавать ограничение в БД и обрабатывать соответствующие ADOException, что тоже выглядит неудобно.
Можно подробнее, что вы имеете в виду?
Здравствуйте, MozgC, Вы писали:
MC>Здравствуйте, Visor2004, Вы писали:
V>>+ В вашем подходе надо задавать ограничение в БД и обрабатывать соответствующие ADOException, что тоже выглядит неудобно. MC>Можно подробнее, что вы имеете в виду?