Как обрабатывать обновления объектов. NHibernate + ASP.NET MVC

Я не могу обновить созданный ранее объект. Я получаю исключение StaleObjectException с сообщением:

Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [Project.DomainLayer.Entities.Employee#00000000-0000-0000-0000-000000000000]

Я не передаю процесс обновления никому. В чем проблема?

Доступ к данным /DI

public class DataAccessModule : Ninject.Modules.NinjectModule
{
    public override void Load()
    {
        this.Bind<ISessionFactory>()
            .ToMethod(c => new Configuration().Configure().BuildSessionFactory())
            .InSingletonScope();

        this.Bind<ISession>()
            .ToMethod(ctx => ctx.Kernel.TryGet<ISessionFactory>().OpenSession())
            .InRequestScope();

        this.Bind(typeof(IRepository<>)).To(typeof(Repository<>))
            .InRequestScope();
    }
}

Доступ к данным/сопоставления

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Project.DomainLayer"   namespace="Project.DomainLayer.Entities">
<class name="Employee" optimistic-lock="version">
    <id name="ID" column="EmployeeID" unsaved-value="00000000-0000-0000-0000-000000000000">
        <generator class="guid.comb" />
    </id>
    <version name="Version" type="Int32" column="Version" />
    <!-- properties -->
    <property name="EmployeeNumber" />
    <!-- ... -->
    <property name="PassportRegistredOn" not-null="true" />
    <!-- sets -->
    <set name="AttachedInformation" cascade="all">
        <key column="EmployeeID" />
        <element column="Attachment" />
    </set>
    <set name="TravelVouchers" cascade="all">
        <key column="EmployeeID" />
        <one-to-many class="TravelVoucher" />
    </set>
  </class>
</hibernate-mapping>

Доступ к данным/репозиторий

public class Repository<T> : IRepository<T> where T : AbstractEntity<T>, IAggregateRoot
{
    private ISession session;

    public Repository(ISession session)
    {
        this.session = session;
    }

    // other methods are omitted

    public void Update(T entity)
    {            
        using(var transaction = this.session.BeginTransaction())
        {
            this.session.Update(entity);
            transaction.Commit();
        }
    }
    public void Update(Guid id)
    {            
        using(var transaction = this.session.BeginTransaction())
        {
            this.session.Update(this.session.Load<T>(id));
            transaction.Commit();
        }
    }
} 

Внутри контроллера

public class EmployeeController : Controller
{
    private IRepository<Employee> repository;

    public EmployeeController(IRepository<Employee> repository)
    {
        this.repository = repository;
    }        
    public ActionResult Edit(Guid id)
    {
        var e = repository.Load(id);
        return View(e);
    }
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(Employee employee)
    {
        if(ModelState.IsValid)
        {
            repository.Update(employee);
            return RedirectToAction("Deatils", "Employee", new { id = employee.ID });
        }
        else
        {
            return View(employee);
        }
    }
}

Как обновить объекты? Спасибо!

ИЗМЕНИТЬ

Итак, я добавил unsaved-value="{Guid.Empty goes here}" в свою разметку. Более того, я попытался сделать следующее:

public void Update(T entity)
{
    using(var transaction = this.session.BeginTransaction())
    {
        try
        {
            this.session.Update(entity);
            transaction.Commit();
        }
        catch(StaleObjectStateException ex)
        {
            try
            {
                session.Merge(entity);
                transaction.Commit();
            }
            catch
            {
                transaction.Rollback();
                throw;
            }
        }

    }
}

И это дает мне тот же эффект. Я имею в виду transaction.Commit(); после того, как Merge дает то же исключение.

Также мне интересно, следует ли открывать, используя скрытый ввод, объект ID в представлении Edit?

ИЗМЕНИТЬ

Таким образом, сущность действительно отделяется. Когда он переходит к контроллеру, ID равен Guid.Empty. Как мне обрабатывать, Merge или Reattach?

Ответ 1

Есть два сценария, с которыми вы можете столкнуться, учитывая ваш шаблон кода.

1) Вы можете получить объект из db, используя ISession.Get, за которым может последовать изменение/обновление получаемого объекта. Чтобы это изменение было эффективным, все, что вам нужно сделать, это очистить сеанс или зафиксировать транзакцию, поскольку Nhibernate автоматически отслеживает все изменения.

2) У вас есть временный экземпляр, объект, который не связан с ISession в контексте, из которого вы хотите обновить. В этом случае, по моему опыту, наилучшей практикой является ISession. Получите объект и сделайте соответствующие изменения для объекта, который вы только что извлекли (обычно ваша модель просмотра отличается от вашей модели домена, не смешивайте оба ) Этот шаблон показан ниже. Он работает все время. Убедитесь, что вы также используете ISession.SaveOrUpdate.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Employee employee)
{
    if(ModelState.IsValid)
    {
        var persistentEmployee = repository.Get(employee.Id);
        if(  persistentEmployee == null){
            throw new Exception(String.Format("Employee with Id: {0} does not exist.", employee.Id));
        }
        persistentEmployee.Name = employee.Name;
        persistentEmployee.PhoneNumber = employee.PhoneNumber;
        //and so on
        repository.Update(persistentEmployee);
        return RedirectToAction("Deatils", "Employee", new { id = employee.ID });
    }
    else
    {
        return View(employee);
    }
}

Также обратите внимание, что ваш контроллер, вероятно, создается на основе запроса, следовательно, срок службы вашего ISession не охватывает несколько вызовов для разных методов, которые вы используете в своем контроллере. Другими словами, каждый метод почти всегда работает в контексте нового ISession (единицы работы).

Ответ 2

Ваша логика не очень хорошая, потому что вы используете модель домена, например Employee, как ViewModel. Лучшей практикой является использование CreateEmploeeViewModel и EditEmployeeViewModel и отдельная логика логики и просмотра модели. Например:

public class Employee 
 {
        public virtual int Id { get; set; }

        public virtual string FirstName { get; set; }

        public virtual string LastName { get; set; }

        public virtual string MiddleName { get; set; }
 }

public class CreateEmployeeViewModel 
 {
        public virtual string FirstName { get; set; }

        public virtual string LastName { get; set; }

        public virtual string MiddleName { get; set; }
 }

public class EditEmployeeViewModel : CreateEmployeeViewModel  
 {
        public virtual int Id { get; set; }
 }

Чтобы преобразовать из Employee в ViewModel, я предпочитаю использовать Automapper.

Итак, действия контроллера выглядят следующим образом:

[HttpGet]
    public virtual ActionResult Edit(int id)
    {
        Employee entity = GetEntityById(id);
        EmployeeEditViewModel model = new EmployeeEditViewModel();

        Mapper.Map(source, destination);            

        return View("Edit", model);
    }

    [HttpPost]
    public virtual ActionResult Edit(EmployeeEditViewModel model)
    { 
        if (ModelState.IsValid)
        {
            Employee entity = GetEntityById(model.Id);

            entity = Mapper.Map(model, entity);               
            EntitiesRepository.Save(entity);

            return GetIndexViewActionFromEdit(model);
        }           

        return View("Edit", model);
    }

В этом случае NHibernate знает, что вы обновляете Employee, и вы не можете удалить некоторые свойства, которые не существуют в вашем представлении.

Ответ 3

Я считаю, что ваш объект Employee стал тем, что NHibernate называет "отключенным" между GET и POST ваших методов действий "Редактировать". Подробнее см. NHibernate на эту тему для получения более подробной информации и некоторых решений. Фактически, ссылка описывает точный сценарий GET-POST, который вы, кажется, используете.

Вам может понадобиться повторно привязать ваш объект Employee и/или указать "несохраненное значение", как предлагалось Firo, чтобы NHibernate знал сотрудника с идентификатором Guid.Empty еще не сохранялся в базе данных. В противном случае, как предположил Фиро, NHibernate видит Guid.Empty в качестве действительного идентификатора и считает, что объект уже сохранен в базе данных, но сеанс, в котором он был извлечен, был отброшен (следовательно, объект становится "отсоединенным" ).

Надеюсь, что это поможет.

Ответ 4

Вы спрашиваете,

Также мне интересно, следует ли открывать, используя скрытый ввод, идентификатор объекта в представлении "Изменить"?

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

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

ИМХО, я поместил идентификатор и версию сущности в скрытый ввод, а после обратной передачи перезагрузите объект и сопоставьте данные. Таким образом, как сказал Иван Корытин, вам не нужно было носить с собой свойства, которые вам не нужны. Вы также можете справиться с неторопливостью на уровне контроллера и добавить ошибку проверки, а не NHibernate сказать, что ваш объект устарел.

Иван Корытин описывает стандартный процесс обработки простого редактирования объекта. Единственный вопрос с его ответом в том, что он не касается свойства Version. IMHO, база данных не должна быть версией, или должно иметь значение свойство Version.

Ответ 5

"несохраненное значение" отсутствует. поэтому NH считает, что Guid.Empty является действительным id

<id name="ID" column="EmployeeID" unsaved-value="0000000-0000-0000-0000-000000000000">

Ответ 6

Если вы хотите обновить некоторые поля объектов, вам не нужно использовать session.Update(), используйте session.Flush() перед закрытой транзакцией.

session.Update() → Обновить постоянный экземпляр с идентификатором данного временного экземпляра.

Ответ 7

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

У меня та же проблема, но в конце я увидел, что я меняю идентификатор на другой номер (в NHibernate идентификатор будет создан сам, , если вы настроите его таким образом!)..

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

Надеюсь, я могу помочь кому угодно!:)

Ответ 8

В конце концов, это помогает, но я считаю это ужасным:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Guid id, Employee employee)
{
    if(ModelState.IsValid)
    {
        var e = repository.Get(id);

        if(Guid.Empty != e.ID)
        {
            e.Department = employee.Department;
            repository.Update(employee.ID);
            return RedirectToAction("Details", "Employee", new { id = e.ID });
        }
        /*...*/
    }
}

Даже если я поместил поля HiddenFor в представление Edit для IDVersion), передаваемый идентификатор является обычным a Guid.Empty, в котором указано, что employee является переходным.

Я очень благодарен за вашу помощь, ребята!

Вопросы

I know what viewmodels are, but quite not understood how does it help with detaching.

Why if I put TextBoxFor(e => e.ID) on Edit view it binds employee like a transient entity without saving the ID value?