I'm new to nHibernate, and trying to get my head around the proper way to update detached objects from a web application form POST. (We're using ASP.NET MVC)
The object I'm trying to update contains (among other things) an IList of child objects, mapped something like this:
<bag name="PlannedSlices" inverse="true" cascade="all-delete-orphan">
<key column="JobNumber" />
<one-to-many class="SliceClass" />
</bag>
We have arranged our MVC edit view form so that when it's posted back, our action method is passed am object (incluing the List<> of child items. We round-trip all the entity ID's correctly via the form.
Our naive attempt at the post action method does a session.SaveOrUpdate(parentObject), with the parentObject which has been scraped from view form by the default modelbinder.
This seems to work fine for any of the following scenarios:
The scenario which fails is: - Deleting child objects - i.e if they're not in the IList, they don't get deleted from the database. There's no exception or anything, they just don't get deleted.
My understanding is that this is because the magic which nHibernate performs to create a list of children which require deletion doesn't work with detached instances.
I have not been able to find a simple example of what this sort of action method should look like with nHibernate (i.e. using a model-binder object as a detached nHibernate instance) - examples based on MS EF (e.g. http://stephenwalther.com/blog/archive/2009/02/27/chapter-5-understanding-models.aspx) seem to use a method 'ApplyPropertyChanges' to copy changed properties from the model-bound object to a re-loaded entity instance.
So, after all that, the question is pretty simple - if I have the model binder give me a new object which contains collections of child objects, how should I update that via nHibernate, (where 'update' includes possibly deletion of children)?
Here's an example that does what I think you're trying to do. Let me know if I've misunderstood what you're trying to do.
Given the following "domain" classes:
public class Person
{
private IList<Pet> pets;
protected Person()
{ }
public Person(string name)
{
Name = name;
pets = new List<Pet>();
}
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
public virtual IEnumerable<Pet> Pets
{
get { return pets; }
}
public virtual void AddPet(Pet pet)
{
pets.Add(pet);
}
public virtual void RemovePet(Pet pet)
{
pets.Remove(pet);
}
}
public class Pet
{
protected Pet()
{ }
public Pet(string name)
{
Name = name;
}
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
}
With the following mapping:
public class PersonMap : ClassMap<Person>
{
public PersonMap()
{
LazyLoad();
Id(x => x.Id).GeneratedBy.GuidComb();
Map(x => x.Name);
HasMany(x => x.Pets)
.Cascade.AllDeleteOrphan()
.Access.AsLowerCaseField()
.SetAttribute("lazy", "false");
}
}
public class PetMap : ClassMap<Pet>
{
public PetMap()
{
Id(x => x.Id).GeneratedBy.GuidComb();
Map(x => x.Name);
}
}
This test:
[Test]
public void CanDeleteChildren()
{
Person person = new Person("joe");
Pet dog = new Pet("dog");
Pet cat = new Pet("cat");
person.AddPet(dog);
person.AddPet(cat);
Repository.Save(person);
UnitOfWork.Commit();
CreateSession();
UnitOfWork.BeginTransaction();
Person retrievedPerson = Repository.Get<Person>(person.Id);
Repository.Evict(retrievedPerson);
retrievedPerson.Name = "Evicted";
Assert.AreEqual(2, retrievedPerson.Pets.Count());
retrievedPerson.RemovePet(retrievedPerson.Pets.First());
Assert.AreEqual(1, retrievedPerson.Pets.Count());
Repository.Save(retrievedPerson);
UnitOfWork.Commit();
CreateSession();
UnitOfWork.BeginTransaction();
retrievedPerson = Repository.Get<Person>(person.Id);
Assert.AreEqual(1, retrievedPerson.Pets.Count());
}
runs and generates the following sql:
DeletingChildrenOfEvictedObject.CanDeleteChildren : Passed NHibernate: INSERT INTO [Person] (Name, Id) VALUES (@p0, @p1); @p0 = 'joe', @p1 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2'
NHibernate: INSERT INTO [Pet] (Name, Id) VALUES (@p0, @p1); @p0 = 'dog', @p1 = '464e59c7-74d0-4317-9c22-9bf801013abb'
NHibernate: INSERT INTO [Pet] (Name, Id) VALUES (@p0, @p1); @p0 = 'cat', @p1 = '010c2fd9-59c4-4e66-94fb-9bf801013abb'
NHibernate: UPDATE [Pet] SET Person_id = @p0 WHERE Id = @p1; @p0 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2', @p1 = '464e59c7-74d0-4317-9c22-9bf801013abb'
NHibernate: UPDATE [Pet] SET Person_id = @p0 WHERE Id = @p1; @p0 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2', @p1 = '010c2fd9-59c4-4e66-94fb-9bf801013abb'
NHibernate: SELECT person0_.Id as Id5_0_, person0_.Name as Name5_0_ FROM [Person] person0_ WHERE person0_.Id=@p0; @p0 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2'
NHibernate: SELECT pets0_.Person_id as Person3_1_, pets0_.Id as Id1_, pets0_.Id as Id6_0_, pets0_.Name as Name6_0_ FROM [Pet] pets0_ WHERE pets0_.Person_id=@p0; @p0 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2'
NHibernate: UPDATE [Person] SET Name = @p0 WHERE Id = @p1; @p0 = 'Evicted', @p1 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2'
NHibernate: UPDATE [Pet] SET Name = @p0 WHERE Id = @p1; @p0 = 'dog', @p1 = '464e59c7-74d0-4317-9c22-9bf801013abb' NHibernate: UPDATE [Pet] SET Person_id = null WHERE Person_id = @p0 AND Id = @p1; @p0 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2', @p1 = '010c2fd9-59c4-4e66-94fb-9bf801013abb'
NHibernate: DELETE FROM [Pet] WHERE Id = @p0; @p0 = '010c2fd9-59c4-4e66-94fb-9bf801013abb'
NHibernate: SELECT person0_.Id as Id5_0_, person0_.Name as Name5_0_ FROM [Person] person0_ WHERE person0_.Id=@p0; @p0 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2'
NHibernate: SELECT pets0_.Person_id as Person3_1_, pets0_.Id as Id1_, pets0_.Id as Id6_0_, pets0_.Name as Name6_0_ FROM [Pet] pets0_ WHERE pets0_.Person_id=@p0; @p0 = 'cd123fc8-6163-42a5-aeeb-9bf801013ab2'
Note the DELETE FROM [Pet]...
so, what you need to be able to do is hand nhibernate a Person object (in this example) with the modified collections and it should be able to determmine what to delete. Make sure you have the Cascade.AllDeleteOrphan() attribute set.
Rob's answer convinced me to look more closely at the 'load the existing item into the new session and then merge' approach, and of course there's ISession.Merge, which appears to do exactly what I wanted, which is to take a fresh object and merge it with it's predecessor who's just been reloaded into the second session.
So I think the answer to the question I tried to ask is "reload the existing entity and then call 'ISession.Merge' with the new entity."
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With