如何处理更新实体.NHibernate+ASP.NET MVC

本文关键字:实体 NHibernate+ASP NET MVC 更新 何处理 处理 | 更新日期: 2023-09-27 18:27:50

我无法更新以前创建的实体。我收到一个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;
            }
        }
    }
}

这给了我同样的效果。。在Merge之后的transaction.Commit();给出了相同的例外。

此外,我想知道是否应该使用隐藏输入在Edit视图上公开实体ID

编辑

所以实体真的分离了。当它传递到控制器时,ID等于Guid.Empty。我该如何处理Merge还是Reattach

如何处理更新实体.NHibernate+ASP.NET MVC

根据您的代码模式,您可以遇到两种场景。

  1. 您可以使用ISession.Get()从数据库中检索对象,然后对检索到的对象进行更改/更新。为了使此更改生效,您所需要做的就是刷新会话或提交事务,因为Nhibernate将自动为您跟踪所有更改。

  2. 您有一个瞬态实例,一个和上下文中的ISession不关联的对象,您要从中更新。在这种情况下,根据我的经验,最佳实践是ISession.Get()对象,并对刚刚检索到的对象进行相应的更改。(通常您的视图模型也与域模型不同,不要将两者混合使用)此模式如下所示。它一直有效。确保您也使用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(工作单元)的上下文中工作。

您的逻辑不好,因为您使用了像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,并且您无法删除视图中不存在的某些属性。

我相信您的Employee对象在Edit操作方法的GET和POST之间已经变成了NHibernate所说的"分离"对象。有关更多详细信息和一些解决方案,请参阅有关此主题的NHibernate文档。事实上,该链接描述了您似乎正在使用的确切GET-POST场景。

您可能需要重新附加Employee对象和/或指定Firo建议的"未保存的值",以便NHibernate知道ID为Guid的Employee.Empty尚未持久化到数据库中。否则,正如Firo所建议的那样,NHibernate认为Guid.Empty是一个有效的ID,并认为该对象已经保存到数据库中,但检索该对象的会话已被丢弃(因此,该对象变得"分离")。

希望这能有所帮助。

你问,

此外,我想知道是否应该使用隐藏输入在Edit视图中公开实体ID?

是的,你应该。您还应该在隐藏输入中公开版本,因为它的业务是帮助防止对同一实体进行并发编辑。StaleObjectException提示您已启用版本控制,在这种情况下,只有当您发送回的版本值(Int32)与数据库中的版本值相同时,更新才会起作用。

您总是可以通过重新加载实体并映射它来绕过它,确保Version值可能匹配,但这似乎破坏了它的目的。

IMHO,我会把实体ID和版本放在一个隐藏的输入中,在回发时,重新加载实体并映射数据。这样,就像Ivan Korytin上面建议的那样,你就不必随身携带你认为不需要的房产。您还可以在控制器级别处理过时问题,并添加验证错误,而不是让NHibernate告诉您您的对象过时了。

Ivan Korytin概述了处理实体的简单编辑的标准流程。他的答案唯一的问题是它没有处理Version属性。IMHO,不应该对数据库进行版本控制,或者Version属性应该很重要。

缺少"未保存的值"。因此NH认为Guid.Empty是一个有效的id

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

如果要更新某些实体的字段,则不需要使用会话。Update(),使用会话。关闭事务前Flush()。

session.Update()->使用给定瞬态实例的标识符更新持久实例。

如果您是我们中的一员,这里没有任何答案,请尝试在您的实体中查找要发送的"ID"。

我也有同样的问题,但最终,我看到我将ID更改为另一个数字(在NHibernate中,ID将自行生成,如果你这样设置的话!)。

所以,底线是,检查您正在发送的数据的结构和值是否与您期望发送的内容相匹配。

希望我能帮助任何人!:)