How to handle updating entities. NHibernate + ASP.NET MVC

20,715

Solution 1

There are two scenarios that you can run into, given your code pattern.

  1. You could retrieve the object from the db using ISession.Get() which can be followed by a change/update to the retrieved object. For this change to be effective, all you need to do is flush the session or commit the transaction as Nhibernate will track all the changes for you automatically.

  2. You have a transient instance, an object that is not associated with the ISession in context, from which you want to update. In this case, from my experience, the best practice is to ISession.Get() the object and make the corresponding changes to the object you just retrieve. (usually your view model is different from your domain model as well, don't mix both) This pattern is shown below. It works all the time. Make sure you also use 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);
    }
}

Also, notice that your controller is probably instantiated on a per-request basis, hence, the lifetime of your ISession does not span multiple calls to the different methods you have in your controller. In other words, every method is almost always working within the context of a new ISession (unit of work).

Solution 2

Your logic is not good, becouse you use domain model like Employee as ViewModel. Best practice is use CreateEmploeeViewModel and EditEmployeeViewModel and separate Domain Logic and View Model logic. For Example:

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; }
 }

To convert from Employee to ViewModel I prefer yo use Automapper.

So controller Actions become to looks like:

[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);
    }

In this case NHibernate knows that you update Employee, and you can`t remove some properties which not exist in your View.

Solution 3

I believe your Employee object has become what NHibernate calls "detached" between the GET and POST of your Edit action methods. See the NHibernate documentation on this topic for more details and some solutions. In fact, the link describes the exact GET-POST scenario you seem to be using.

You may need to reattach your Employee object and/or specify the "unsaved value" as Firo suggested so that NHibernate knows an Employee with an ID of Guid.Empty has not been persisted to the database yet. Otherwise, as Firo suggested, NHibernate sees Guid.Empty as a valid ID, and thinks the object has already been saved to the database but the session in which it was retrieved has been discarded (hence, the object becoming "detached").

Hope this helps.

Solution 4

You ask,

Also I'm wondering should I expose, using hidden input, the entity ID on the Edit view?

Yes, you should. You should also expose the Version in a hidden input as its business is to help prevent concurrent edits to the same entity. The StaleObjectException hints that you've got versioning turned on, and in that case, the update will only work if the version value (Int32) that you send back is identical to the one in the database.

You can always get around it by reloading the entity and mapping it, ensuring that the Version value is likely to match, but that seems to subvert its purpose.

IMHO, I'd put the entity ID and Version in a hidden input, and on postback, reload the entity and map the data. That way, like Ivan Korytin suggests above, you would not have to carry around properties that aren't needed in your view. You can also handle the staleness at the controller level and add a validation error rather than have NHibernate tell you your object is stale.

Ivan Korytin outlines the standard process for handling a simple edit of an entity. The only issue with his answer is that it does not address the Version property. IMHO, the database should not be versioned, or the Version property should matter.

Solution 5

"unsaved value" is missing. hence NH thinks that Guid.Empty is a valid id

<id name="ID" column="EmployeeID" unsaved-value="0000000-0000-0000-0000-000000000000">
Share:
20,715
lexeme
Author by

lexeme

The worst programming community

Updated on July 09, 2022

Comments

  • lexeme
    lexeme almost 2 years

    I cannot update created previously entity. I'm getting a StaleObjectException exception with message:

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

    I don't share the update process with anyone. What's the problem?

    Data Access / 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();
        }
    }
    

    Data Access / Mappings

    <?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>
    

    Data Access / Repository

    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();
            }
        }
    } 
    

    Inside a Controller

    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);
            }
        }
    }
    

    How do I update my entities? Thanks!

    EDIT

    So I added unsaved-value="{Guid.Empty goes here}" to my markup. Moreover I've tried to do the next thing:

    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;
                }
            }
    
        }
    }
    

    And this gives me the same effect.. I mean transaction.Commit(); after Merge gives the same exception.

    Also I'm wondering should I expose, using hidden input, the entity ID on the Edit view?

    EDIT

    So entity really detaches. When it passes to controller the ID equals Guid.Empty. How do I handle it, Merge or Reattach?

  • lexeme
    lexeme about 12 years
    Ok. So wouldn't using Automapper with corresponding viewmodels result in entities detachment while edit/update? Using another 3d party sowtware makes application more complex. What I want to know is how to make edit/update with nhibernate without detaching entities.
  • brightgarden
    brightgarden about 12 years
    The process that causes detachment ultimately is that you are in a web environment, not a desktop environment. Let's say you're using ASP.Net MVC, and your user makes a change in an edit form and clicks the submit button. The model binder is going to bind to a "model" (in this case, your domain object) that has a null constructor and do its best to assign values to that "model object" based on what has been posted back.
  • brightgarden
    brightgarden about 12 years
    If you were absolutely insisting on finding a solution in which the resulting model object is a fully attached entity, I'm sure there is a way, but it would be interesting only in an academic sense. In terms of building software and delivering software efficiently and maintaining a code base that is easy for other developers who come along later, it is much wiser to follow standard approaches. There is no need for a 3rd party automapper: you can map the properties manually, and there is nothing invalid about that approach.
  • lexeme
    lexeme about 12 years
    It seems that I will always retrieve employee with id == Guid.Empty?
  • sleepydrmike
    sleepydrmike about 12 years
    When you update the object, the ID shouldn't be Guid.Empty. Only when you are in the process of creating/saving the new object it will. The ID can either be generated by your app using or the database. See nhibernate documentation
  • lexeme
    lexeme about 12 years
    Look at my answer below. That's the way it works. Advices are appreciated.
  • Samuel Goldenbaum
    Samuel Goldenbaum almost 12 years
    You have missed the point of the op' question. Your answer does not take concurrency into account.
  • Kat Lim Ruiz
    Kat Lim Ruiz over 10 years
    although you are right, domainmodel vs viewmodel is good. But it doesnt have any relation with the question.
  • Ivan Korytin
    Ivan Korytin over 10 years
    If you look at anwser you can see what is the solution. Option 2.
  • CSharper
    CSharper over 9 years
    Great answer, according to shevchyk's answer, I think you mean "You have a detached instance" stackoverflow.com/questions/161224/…
  • AnorZaken
    AnorZaken over 2 years
    Documentation link is dead.