NHibernate в многопользовательской среде
От: Visor2004  
Дата: 10.08.10 20:41
Оценка:
Пишу небольшой утиль для управления складом, в системе работает порядка 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;
    }
  }
Помните!!! ваш говнокод кому-то предстоит разгребать.
Re: NHibernate в многопользовательской среде
От: fmiracle  
Дата: 10.08.10 21:18
Оценка:
Здравствуйте, 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 на апдейте — значит между тем как ты взял сущность и проверил в ней количество, изменил и сохранил — кто-то успел изменить эту сущность параллельно (например, продал часть товара. Или наоборот — добавил). Значит, надо обновить данные и посмотреть что делать дальше.
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Re: NHibernate в многопользовательской среде
От: MozgC США http://nightcoder.livejournal.com
Дата: 10.08.10 21:19
Оценка:
Я с NHibernate не работал, но я бы в первую очередь почитал например этот документ:
Transactions and Concurrency
а так же погуглил по запросу типа "optimistic locking with NHibernate".
По поводу отмены транзакции в случае исключения, возможно в методе Dispose() транзакции будет произведен откат транзакции в случае если она не была явно принята или отменена.

PS. Вы вообще достаточно знакомы с темой concurrency control? Если нет, то советую еще отдельно почитать на эту тему (например у Фаулера в Архитектуре Корпоративных Приложений, глава 16), иначе в системе с сотней пользователей можно натворить дел..
Re[2]: NHibernate в многопользовательской среде
От: Visor2004  
Дата: 10.08.10 21:25
Оценка:
Здравствуйте, MozgC, Вы писали:

MC>Я с NHibernate не работал, но я бы в первую очередь почитал например этот документ:

MC>Transactions and Concurrency
MC>а так же погуглил по запросу типа "optimistic locking with NHibernate".
MC>По поводу отмены транзакции в случае исключения, возможно в методе Dispose() транзакции будет произведен откат транзакции в случае если она не была явно принята или отменена.

MC>PS. Вы вообще достаточно знакомы с темой concurrency control? Если нет, то советую еще отдельно почитать на эту тему (например у Фаулера в Архитектуре Корпоративных Приложений, глава 16), иначе в системе с сотней пользователей можно натворить дел..


С многопоточностью и конкурентностью никаких проблем нет вообще. Интересует конкретная реализация в NHibernate, неохота перекапывать их исходники если можно спросить на форуме
Помните!!! ваш говнокод кому-то предстоит разгребать.
Re[2]: NHibernate в многопользовательской среде
От: Visor2004  
Дата: 10.08.10 21:35
Оценка:
Здравствуйте, fmiracle, Вы писали:

Т.е. адгоритм получается такой:
1) читаем сущности из БД
2) делаем проверки и модификации
3) пробуем сохранить, если получаем исключение, то в п.1

В идеале хотелось бы иметь что-то типа:
1) получили сущности из кэша
2) сделали изменения
3) попробовали записать, используя какие-то ограничители, типа: итоговое кол-во товара в базе не может быть меньше нуля,
а не просто версию ентити, потому что если на складе было 110 полотенец можно было бы завершить обе транзакции нормально.
4) в случае ошибки ( несоблюдение ограничений заданных в п.3 ) выкинуть исключение и показать пользователю сообщение о необходимости
скорректировать данные продажи.
Помните!!! ваш говнокод кому-то предстоит разгребать.
Re[3]: NHibernate в многопользовательской среде
От: MozgC США http://nightcoder.livejournal.com
Дата: 10.08.10 22:05
Оценка:
Может я не в тему напишу, не знаю как такое лучше сделать в NHibernate, но вообще я обычно такое делаю так:
UPDATE warehouse SET Quantity = Quantity - X WHERE ID = Y AND Quantity >= X

и проверяю что была изменена 1 строка, а не 0. Если 0 — выбрасываю ConcurrencyException.
Re[4]: NHibernate в многопользовательской среде
От: Visor2004  
Дата: 10.08.10 22:38
Оценка:
Здравствуйте, MozgC, Вы писали:

MC>Может я не в тему напишу, не знаю как такое лучше сделать в NHibernate, но вообще я обычно такое делаю так:

MC>
UPDATE warehouse SET Quantity = Quantity - X WHERE ID = Y AND Quantity >= X

MC>и проверяю что была изменена 1 строка, а не 0. Если 0 — выбрасываю ConcurrencyException.

Ну, если писать запросы руками, то лучше имхо вообще отказаться от NHibernate и использовать тот же BLToolkit.
+ В вашем подходе надо задавать ограничение в БД и обрабатывать соответствующие ADOException, что тоже выглядит неудобно.
Помните!!! ваш говнокод кому-то предстоит разгребать.
Re[5]: NHibernate в многопользовательской среде
От: MozgC США http://nightcoder.livejournal.com
Дата: 10.08.10 22:52
Оценка:
Здравствуйте, Visor2004, Вы писали:

V>+ В вашем подходе надо задавать ограничение в БД и обрабатывать соответствующие ADOException, что тоже выглядит неудобно.

Можно подробнее, что вы имеете в виду?
Re[6]: NHibernate в многопользовательской среде
От: Visor2004  
Дата: 11.08.10 07:38
Оценка:
Здравствуйте, MozgC, Вы писали:

MC>Здравствуйте, Visor2004, Вы писали:


V>>+ В вашем подходе надо задавать ограничение в БД и обрабатывать соответствующие ADOException, что тоже выглядит неудобно.

MC>Можно подробнее, что вы имеете в виду?

Ничего, просто проглядел where в вашем запросе
Помните!!! ваш говнокод кому-то предстоит разгребать.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.