I would like to handle the case where a user is editing an object from a database in a Windows Forms application, makes an edit that would violate a database constraint (i.e. column unique value), saves the entity back to the database, and NHibernate throws an exception thus trashing the session.
I'm using the guidance published in the MSDN article Building a Desktop To-Do Application with NHibernate and using a session-per-presenter/form approach. At creation of the presenter, I keep a reference to the SessionFactory, create a new session, retrieve the object from the database via the session and store a reference to it [the object]. When the form is displayed, its fields are populated from the object.
When changes are made to the form and the user wishes to save the data, the object is updated from the field values, and the object is saved back to the database.
I handle stale-state exceptions where the object is altered between the time it was retrieved from the database and when it is saved back: A new session is created, the original object is loaded again, the user is informed that a conflict has occurred and is presented with his changes and what is currently in the database. He can elect to save his changes or cancel (and accept what is now stored in the database).
In the event of a constraint violation, it would seem that one of two things can happen:
However, I don't think I can actually detect which case occurred since a stale-state exception is never thrown because the constraint exception occurred (I've tested this).
Handling case 1 is trivial since I can simply display an error message that says "FIELD-X has a value that is already in the DB" and pretend that nothing really bad happened. The user can change FIELD-X to a unique value and save again without having to reenter his changes.
Handling case 2 would be like handling a regular stale-state exception.
I know I can "brute force" this by keeping a copy of the original (or its values) and then comparing the two field-by-field. However, I suspect that there is a better way to handle this by leveraging NHibernate. How would you handle this?
In case it is helpful:
EDIT 23 Feb - I added some example code to better illustrate what I am currently doing.
/**
* Save handler called when user initiates save in UI.
*
* Returns true when save was successful (essentially, tells the presenter
* that the UI can be closed.
*/
private bool SaveData()
{
try
{
if (this.creatingNewUserAccount)
{
// Do whatever is necessary to instantiate a new object.
this.userAccount = new UserAccount();
// and copy values from the UI into the new object.
this.userAccount.Name = this.form.Name;
// etc.
}
else
{
// Just copy values from the UI into the existing object
// from the database.
this.userAccount.Name = this.form.Name;
// etc.
}
using (ITransaction tx = this.session.BeginTransaction())
{
this.accountRepository.store(this.userAccount);
tx.Commit();
}
return true;
}
catch (StaleObjectStateException)
{
HandleStaleStateException();
return false;
}
catch (ArgumentException e)
{
this.m_View.ShowOtherDialog(e.Message);
return false;
}
catch (GenericADOException e)
{
HandleConstraintViolationException();
return false;
}
}
private void HandleStaleStateException()
{
// The session was trashed when the exception was thrown,
// so close it and create a new one.
this.session.Dispose();
this.session = this.sessionFactory.OpenSession();
CurrentSessionContext.Bind(this.session);
// Reload the object from the database.
this.userAccount = LoadData();
// Do a bunch of things that deal with informing the user
// of the stale-state and displaying a form to merge changes.
HandleEditConflict();
}
private void HandleConstraintViolationException()
{
// The session was trashed when the exception was thrown,
// so close it and create a new one.
this.session.Dispose();
this.session = this.sessionFactory.OpenSession();
CurrentSessionContext.Bind(this.session);
// Determine if trying to save a new entity or editing an existing one.
if (this.creatingNewUserAccount)
{
// If saving a new entity, we don't care about the old object
// we created and tried to save.
this.userAccount = null;
}
else
{
// ????
}
}
The ISession.Refresh(Object obj)
method was what ended up working for me. The code from my question remained the same except for the final method:
private void HandleConstraintViolationException()
{
// The session was trashed when the exception was thrown,
// so close it and create a new one.
this.session.Dispose();
this.session = this.sessionFactory.OpenSession();
CurrentSessionContext.Bind(this.session);
// Determine if trying to save a new entity or editing an existing one.
if (this.creatingNewUserAccount)
{
// If saving a new entity, we don't care about the old object
// we created and tried to save.
this.userAccount = null;
}
else
{
this.session.Refresh(this.userAccount);
}
this.form.ShowDialog("... Describe the constraint violation ...");
}