Как вы выполняете управление версиями в Nhibernate?

Я не могу поверить, что так сложно заставить кого-нибудь показать мне простой рабочий пример. Это заставляет меня думать, что каждый может говорить только так, как будто они знают, как это делать, но на самом деле они этого не делают.

Я сокращаю пост до только того, что я хочу, чтобы этот пример делал. Может быть, этот пост добирался до долгих и испуганных людей.

Чтобы получить эту награду, я ищу РАБОЧИЙ ПРИМЕР, который я могу скопировать в VS 2010 и запустить.

Что должен сделать пример.

  • Покажите, какой тип данных должен находиться в моем домене для версии как отметка времени в mssql 2008
  • Показать nhibernate автоматически бросает "StaleObjectException"
  • Покажите мне рабочие примеры этих 3 сценариев.

Сценарий 1

Пользователь A приходит на сайт и редактирует строку Row1. Пользователь B приходит (обратите внимание, что он может видеть Row1), и нажимает для редактирования Row1, пользователю не разрешается изменять строку до тех пор, пока не будет закончен пользователь A.

Сценарий 2

Пользователь A приходит на сайт и редактирует строку Row1. Пользователь B наступает через 30 минут и нажимает для редактирования строки Row1. Пользователь B должен иметь возможность редактировать эту строку и сохранять ее. Это связано с тем, что Пользователь А занимал слишком много времени, чтобы редактировать строку и потерял право редактировать.

Сценарий 3

Пользователь А возвращается из дома. Он нажимает кнопку строки обновления, и его следует приветствовать с помощью StaleObjectException.

Я использую asp.net mvc и свободно nhibernate. Ищете пример, который нужно сделать в них.


Что я пробовал

Я попытался создать свой собственный, но я не могу заставить его бросать StaleObjectException, и я не могу получить номер версии для увеличения. Я устал открывать 2 отдельных браузера и загружал индексную страницу. Оба браузера показали тот же номер версии.

public class Default1Controller : Controller
{
    //
    // GET: /Default1/

    public ActionResult Index()
    {
        var sessionFactory = CreateSessionFactory();

        using (var session = sessionFactory.OpenSession())
        {
            using (var transaction = session.BeginTransaction())
            {
                var firstRecord = session.Query<TableA>().FirstOrDefault();
                transaction.Commit();
                return View(firstRecord);
            }

        }

    }

    public ActionResult Save()
    {
        var sessionFactory = CreateSessionFactory();
        using (var session = sessionFactory.OpenSession())
        {
            using (var transaction = session.BeginTransaction())
            {
                var firstRecord = session.Query<TableA>().FirstOrDefault();
                firstRecord.Name = "test2";
                transaction.Commit();
                return View();
            }
        }
    }

    private static ISessionFactory CreateSessionFactory()
    {
        return Fluently.Configure()
            .Database(MsSqlConfiguration.MsSql2008
                .ConnectionString(c => c.FromConnectionStringWithKey("Test")))
            .Mappings(m => m.FluentMappings.AddFromAssemblyOf<TableA>())
                             //  .ExposeConfiguration(BuidSchema)
            .BuildSessionFactory(); 
    }


    private static void BuidSchema(NHibernate.Cfg.Configuration config)
    {
        new NHibernate.Tool.hbm2ddl.SchemaExport(config).Create(false, true);
    }

}


public class TableA
{
    public virtual Guid Id { get; set; }
    public virtual string Name { get; set; }

    // Not sure what data type this should be for timestamp.
    // To eliminate changing to much started with int version
    // but want in the end timestamp.
    public virtual int Version { get; set; } 
}

public class TableAMapping : ClassMap<TableA>
{
    public TableAMapping()
    {
        Id(x => x.Id);
        Map(x => x.Name);
        Version(x => x.Version);
    }
}

Ответ 1

Будет ли запрет останавливать вывод строки?

Нет. Замки размещаются только в объеме транзакции, которая в веб-приложении заканчивается, когда запрос заканчивается. Кроме того, режим изоляции транзакций по умолчанию - Чтение завершено, что означает, что блокировки чтения освобождаются, как только оператор select заканчивается. Если вы читаете и вносите изменения в один и тот же запрос и транзакцию, вы можете поместить блокировку чтения и записи в строку под рукой, что помешало бы другим транзакциям писать или читать из этой строки. Однако этот тип управления concurrency не работает хорошо в веб-приложении.

Или пользователь B мог бы все еще видеть строку, но если бы он попытался ее сохранить, это сработало бы?

Это произойдет, если [оптимистичный concurrency] используется. В NHibernate оптимистичный concurrency работает, добавив поле версии. Команды сохранения/обновления выдаются с версией, на которой было основано обновление. Если это отличается от версии в таблице базы данных, строки не обновляются и NHibernate будет бросать.

Что произойдет, если User A say отменяет и не редактирует. Должен ли я отпустить блокировку самостоятельно или установить тайм-аут для освобождения замок?

Нет, блокировка освобождается в конце запроса.

В целом, лучше всего выбрать оптимистичный concurrency с полями версии, управляемыми NHibernate.

Ответ 2

Как это выглядит в коде? Я устанавливаю в своем свободном nhibernate сгенерировать временную метку (не уверен, если бы я использовал временные данные).

Я бы предложил использовать столбец версии. Если вы используете FluentNhibernate с автоматическими сопоставлениями, то, если вы создадите столбец с именем Version of type int/long, он будет использовать его для версии по умолчанию, альтернативно вы можете использовать метод Version() в сопоставлении для этого (аналогично для отметки времени).

Итак, теперь я как-то сгенерировал временную метку, и пользователь редактирует строка (через gui). Должен ли я хранить временную метку в памяти или что нибудь? Затем, когда пользователь отправляет вызов из памяти, отметка времени и id строки и проверить?

Когда пользователь начинает редактирование строки, вы извлекаете ее и сохраняете текущую версию (значение свойства версии). Я бы порекомендовал поставить текущую версию в скрытое поле в форме. Когда пользователь сохраняет свои изменения, вы можете либо выполнить ручную проверку с версией в базе данных (убедитесь, что она такая же, как версия в скрытом поле), либо вы можете установить для свойства версии значение из скрытого поля ( если вы используете привязку данных, вы можете сделать это автоматически). Если вы установите свойство версии, то при попытке сохранить объект NHibernate проверит, что версия, которую вы сохраняете, соответствует версии в базе данных и выдает исключение, если это не так.

NHibernate выдаст запрос на обновление примерно так:

UPDATE xyz  ЗАДАВАТЬ,      Версия = 16  WHERE Id = 1234 AND Version = 15

(при условии, что ваша версия была 15) - при этом она также увеличит поле версии

Если это означает, что бизнес-логика отслеживает "строку блокировки", но теоретически кто-то еще мог просто пойти Где (x = > x.Id == id) и захватить эту строку и обновить по своему усмотрению.

Если кто-то еще обновляет строку через NHibernate, она автоматически увеличит версию, поэтому, когда ваш пользователь попытается сохранить ее с неправильной версией, вы получите исключение, которое вам нужно решить, как обращаться (т.е. попробовать показать некоторые объединить экран или попросить пользователя повторить попытку с новыми данными)

Что происходит, когда строка обновляется? Вы устанавливаете значение null в метку времени?

Он обновляет версию или временную метку (временная метка будет обновляться до текущего времени) автоматически

Что произойдет, если пользователь никогда не закончит обновление и не уйдет. Как строка все снова разблокируются?

Строка не блокируется сама по себе, вместо нее используется оптимистичный concurrency, где вы предполагаете, что никто не будет изменять одну и ту же строку одновременно, а если кто-то это сделает, вам нужно повторить обновление.

Есть ли еще состояние гонки, что происходит, или это рядом с невозможно? Я просто заинтересован 2 ppl попытаться получить одна и та же строка, и оба они видят это в своем gui для редактирования, но на самом деле будет отказано в конце, потому что они потеряли гонку состояние.

Если 2 человека пытаются отредактировать одну и ту же строку одновременно, один из них проиграет, если вы используете оптимистичный concurrency. Преимущество в том, что они будут ЗНАЮТ, что конфликт был, в отличие от того, чтобы потерять свои изменения и подумать, что он обновлен, или переписать кого-то другого, не зная об этом.

Итак, я сделал что-то вроде этого

var test = session.Query.Where(x = > x.Id == ID).FirstOrDefault();//отправьте пользователю для редактирования. Имеет версию на Это. редактирование пользователя и отправка данных спустя 30 минут.

Коды

test.Id = vm.Id; test.ColumnA = vm.ColumnA; test.Version = vm.Version;

Session.update(тест); session.Commit(); Таким образом, вышесказанное будет работать правильно?

Вышеупомянутое выдает исключение, если кто-то еще зашел и изменил строку. В этом суть, поэтому вы знаете, что возникла проблема concurrency. Обычно вы показываете пользователю сообщение "Кто-то другой изменил эту строку" с новой строкой и, возможно, их изменения также, чтобы пользователь мог выбрать, какие изменения выиграют.

но если я сделаю это

test.Id = vm.Id;     test.ColumnA = vm.ColumnA;

session.Update(test);
session.Commit(); it would not commit right?

Исправить, если вы не перезагрузили тест (т.е. вы выполнили test = new Xyz(), а не test = session.Load()), потому что метка времени в строке не будет соответствовать

Если кто-то еще обновляет строку через NHibernate, она увеличит версии, поэтому, когда ваш пользователь пытается сохранить его с помощью неправильная версия, вы получите исключение, которое вам нужно решить, как (например, попробуйте показать некоторый экран слияния или попросите пользователя попробовать снова с новыми данными)

Могу ли я это сделать, когда запись будет проверена. я хочу сначала попробуйте сначала изменить только один человек. другой парень даже не сможет получить доступ к записи для редактирования, пока что-то редактирует его.

Это не оптимистично concurrency. В качестве простого ответа вы можете добавить свойство CheckOutDate, которое вы установите, когда кто-то начнет его редактировать, и установите его значение null, когда они закончат. Затем, когда они начинают редактировать или когда вы показываете им строки для редактирования, вы можете исключить все строки, в которых этот CheckOutDate является более новым, чем последние 10 минут (тогда вам не понадобится запланированная задача для reset его периодически)

Строка не блокируется сама по себе, она использует оптимистичный concurrency, где вы предполагаете, что никто не изменит ту же строку в в то же время, и если кто-то это сделает, вам нужно повторить обновление.

Я не уверен, что ваше высказывание означает, что я могу сделать

session.query.Where(x = > x.id == id).FirstOrDefault(); весь день долгое время, и он будет держать меня в записи (думал, что он будет держать увеличивая версию).

Запрос НЕ увеличит версию, только обновление к нему приведет к увеличению версии.

Ответ 3

Да, можно заблокировать строку с nhibernate, но если я хорошо понимаю, ваш сценарий находится в веб-контексте, а затем это не лучшая практика.

Лучше всего использовать оптимистичную блокировку с автоматическим управлением версиями, как вы упомянули. Блокировка строки при открытии страницы и ее освобождении при разгрузке страницы быстро приведет к блокировке строки (проблема с javascript, страница не будет правильно уничтожена...). Оптимистическая блокировка заставит NHibernate вызывать исключение при очистке транзакции, содержащей объекты, измененные другим сеансом. Если вы хотите иметь истинную согласованную модификацию одной и той же информации, вы можете попытаться подумать о системе, которая объединит многих пользователей, вводимых внутри одного документа, но это сама по себе система, не управляемая ORM.

Вам нужно будет выбрать способ работы с сеансом в веб-среде. http://nhibernate.info/doc/nh/en/index.html#transactions-optimistic

Единственный подход, который согласуется с высоким concurrency и высоким масштабируемость оптимистична concurrency контроль с управлением версиями. NHibernate предусматривает три возможных подхода к написанию код приложения, который использует оптимистичный concurrency.

Ответ 4

Я не так много знаю о самом nHibernate, но если вы готовы создать некоторые хранимые procs в базе данных, он может > сортировать < сделайте.

Для хранения информации по каждой строке вам понадобится один дополнительный столбец данных и два поля в объектной модели:

  • "хэш" всех значений полей (с использованием SQL Server CHECKSUM 2008 и более поздних версий или HASHBYTES для более ранних выпусков), кроме самого поля хэша и поля EditTimestamp. Это может быть сохранено в таблице с помощью триггеров INSERT/UPDATE, если это необходимо.
  • "время редактирования" типа datetime.

Измените свои процедуры, чтобы сделать следующее:

  • Процедура 'select' должна включать предложение where, подобное 'edit-timestamp < (Сейчас - 30 минут) "и должен обновить" время редактирования "до текущего времени. Запустите select с соответствующей блокировкой ПЕРЕД обновлением строки. Я думаю о хранимой процедуре с фиксацией фиксации, такой как этот здесь Используйте постоянную дату/время а не что-то вроде GETDATE().

Пример (с использованием фиксированных значений):

BEGIN TRAN

DECLARE @now DATETIME 
SET @now = '2012-09-28 14:00:00'

SELECT *, @now AS NewEditTimestamp, CHECKSUM(ID, [Description]) AS RowChecksum
FROM TestLocks
WITH (HOLDLOCK, ROWLOCK)
WHERE ID = 3 AND EditTimestamp < DATEADD(mi, -30, @now)

/* Do all your stuff here while the record is locked */
UPDATE TestLocks
SET EditTimestamp = @now
WHERE ID = 3 AND EditTimestamp < DATEADD(mi, -30, @now)

COMMIT TRAN

Если вы вернете строку из этой процедуры, тогда у вас есть "блокировка", иначе строки не будут возвращены и ничего не изменится.

  • Процедура "update" должна добавить предложение where, подобное "hash = ранее возвращенный хеш"

Пример (с использованием фиксированных значений):

BEGIN TRAN

    DECLARE @RowChecksum INT
    SET @RowChecksum = -845335138

    UPDATE TestLocks
    SET [Description] = 'New Description'
    WHERE ID = 3 AND CHECKSUM(ID, [Description]) = @RowChecksum

    SELECT @@ROWCOUNT AS RowsUpdated

COMMIT TRAN

Итак, в ваших сценариях:

  • Пользователь A редактирует строку. Когда вы возвращаете эту запись из базы данных, "edit-timestamp" обновляется до текущего времени, и у вас есть строка, чтобы вы знали, что можете редактировать. Пользователь B не получит строку, потому что временная метка все еще слишком недавняя.

  • Пользователь B редактирует строку через 30 минут. Они получают строку назад, потому что отметка времени прошла более 30 минут назад. Хэш полей будет таким же, как для пользователя A 30 минут назад, поскольку никаких обновлений не было написано.

  • Теперь пользовательские обновления B. Ранее полученный хэш по-прежнему соответствует хешу полей в строке, поэтому оператор обновления успешно завершает работу, и мы возвращаем число строк, чтобы показать, что строка была обновлена. Однако пользователь A пытается обновить следующее. Поскольку значение поля описания изменилось, значение hash изменилось, и поэтому ничего не обновляется оператором UPDATE. Мы получаем результат "обновленных нулевых строк", поэтому мы знаем, что либо строка была изменена, либо строка была удалена.

Вероятно, есть некоторые проблемы в отношении масштабируемости, когда все эти блокировки продолжаются, и приведенный выше код может быть оптимизирован (может возникнуть проблема с синхронизацией, идущей вперед/назад, например, с использованием UTC), но я написал эти примеры, чтобы объяснить, как это сделать может работать.

Вне этого я не вижу, как вы можете это сделать, не используя блокировку строк уровня базы данных в транзакции select. Возможно, вы можете запросить эти блокировки с помощью nHibernate, но я не знаю, что я могу сказать о nHibernate.

Ответ 5

Вы посмотрели интерфейс ISaveOrUpdateEventListener?

public class SaveListener : NHibernate.Event.ISaveOrUpdateEventListener
{

    public void OnSaveOrUpdate(NHibernate.Event.SaveOrUpdateEvent e)
    {
        NHibernate.Persister.Entity.IEntityPersister p = e.Session.GetEntityPersister(null, e.Entity);
        if (p.IsVersioned)
        {
            //TODO: check types etc...
            MyEntity m = (MyEntity) e.Entity;
            DateTime oldversion = (DateTime) p.GetVersion(m, e.Session.EntityMode);
            DateTime currversion = (DateTime) p.GetCurrentVersion(m.ID, e.Session);

            if (oldversion < currversion.AddMinutes(-30))
                throw new StaleObjectStateException("MyEntity", m.ID);
        }
    }

}

Затем в вашей конфигурации зарегистрируйте его.

    private static void Configure(NHibernate.Cfg.Configuration cfg)
    {
        cfg.EventListeners.SaveOrUpdateEventListeners = new NHibernate.Event.ISaveOrUpdateEventListener[] {new SaveListener()};

    }



    public static ISessionFactory CreateSessionFactory()
    {
        return Fluently.Configure().Database(...).
                    .Mappings(...)
                    .ExposeConfiguration(Configure)                       
                    .BuildSessionFactory();
    }

И версия Свойства, которые вы хотите использовать в своем классе сопоставления.

public class MyEntityMap: ClassMap<MyENtity>
{
    public MyEntityMap()
    {
        Table("MyTable");

        Id(x => x.ID);
        Version(x => x.Timestamp);
        Map(x => x.PropA);
        Map(x => x.PropB);

    }
}

Ответ 6

Короткий ответ на ваш вопрос заключается в том, что вы не можете/не должны делать это в простом веб-приложении с оптимизацией nhibernates (версия) и пессимистичной блокировкой строк. Тот факт, что ваши транзакции только до тех пор, пока ваш запрос является вашим ограничивающим фактором.

Что вы можете сделать, это создать другой класс таблицы и сущности и сопоставления, управляющие этими "блокировками". На самом низком уровне вам нужен идентификатор редактируемого объекта и идентификатор пользователя, выполняющего редактирование, и дату и время, когда была получена блокировка. Я бы сделал ИД объекта редактируемым основным ключом, так как вы хотите, чтобы он был эксклюзивным...

Когда пользователь нажимает на редактируемую строку, вы можете попытаться получить блокировку (создать новую запись в этой таблице с идентификаторами и текущим временем жизни). Если блокировка уже существует для другого пользователя, она будет терпеть неудачу, потому что вы пытаетесь нарушить ограничение первичного ключа.

Если блокировка получена, когда пользователь нажимает кнопку "Сохранить", вам нужно проверить, что они все еще имеют действительную "блокировку" перед выполнением фактического сохранения. Затем выполните фактическое сохранение и удалите запись блокировки.

Я бы также рекомендовал фоновый сервис/процесс, который периодически меняет эти блокировки и удаляет те, которые истекли или старше вашего лимита времени.

Это мой предписанный способ работы с "замками" в веб-среде. Удачи!