It's a long shot that anyone will have dealt with this, but here goes.
First, I have NHibernate's auto-dirty-check behavior disabled. I did this because I don't want NHibernate to save every changed object that it knows about when I commit a transaction (FlushMode = Commit) because it get a little over-zealous sometimes and tries to save lots of huge object graphs just because I changed something in an object. I followed the instructions found here: http://fabiomaulo.blogspot.com/2009/03/ensuring-updates-on-flush.html
Second, I have a situation where I need to load entities using custom SQL, so I have some dynamic SQL that loads this one object graph using ISession.CreateSQLQuery(). After I do this, I call ISession.Lock() on the entities that I loaded. I don't really know what this is doing, but if I don't do it, changes to the custom loaded objects will never get saved.
The problem now is that if I add or remove objects from collections inside the objects that I loaded with my custom SQL query, NHibernate doesn't save them. With auto-dirty-check on (the default), they save because it just saves the whole object graph since all of the relationships are marked as cascade="save-update". But since I have auto-dirty-check off, it's not making its way through all of the relationships.
It seems that since I called ISession.Lock(), NHibernate knows about all of my objects, but something isn't the same with the collections in those objects.
If anyone has any ideas, they would be appreciated.
EDIT: More details on what I'm doing. I have this method where does a query. As you can see from the complexity of the query, HQL is not an option (I always prefer HQL or ISession.CreateCriteria() over CreateSQLQuery()).
public IList<MaterialGroup> GetResults(IList<long> takeOffItemIds)
{
if (takeOffItemIds == null || takeOffItemIds.Count == 0)
return new List<MaterialGroup>();
var query =
@"with MaterialGroupsByTakeOffItem (MaterialGroupId, TakeOffItemId) as
(
select MaterialGroupId, ParentTakeOffItemId
from MaterialGroups mg
inner join TakeOffItems toi on toi.TakeOffItemId = mg.ParentTakeOffItemId
where ParentTakeOffItemId is not null
union all
select grp.MaterialGroupId, parent.TakeOffItemId
from MaterialGroups grp
inner join MaterialGroupsByTakeOffItem parent on parent.MaterialGroupId = grp.ParentMaterialGroupId
)
select {mg.*}, {md.*}, {mi.*}, {mc.*} from MaterialGroupsByTakeOffItem mgbtoi
inner join MaterialGroups {mg} on {mg}.MaterialGroupId = mgbtoi.MaterialGroupId
left outer join MaterialDetails {md} on {md}.MaterialDetailsId = {mg}.MaterialGroupId
left outer join MaterialItems {mi} on {mi}.MaterialItemId = {md}.PartId
left outer join MaterialCodes {mc} on {mc}.MaterialCodeId = {mi}.CodeId
";
if (takeOffItemIds.Count == 1)
query += "where mgbtoi.TakeOffItemId = " + takeOffItemIds[0];
else
query += "where mgbtoi.TakeOffItemId in (" + takeOffItemIds.ToCommaDelimitedString() + ")";
return _session.CreateSQLQuery(query)
.AddEntity("mg", typeof(MaterialGroup))
.AddJoin("md", "mg.MaterialDetails")
.AddJoin("mi", "md.Part")
.AddJoin("mc", "mi.Code")
.List<MaterialGroup>();
}
This method returns a list of MaterialGroup objects that will belong to another entity called TakeOffItem. I take the list of MaterialGroup objects and put them into TakeOffItem.MaterialGroups.
public override TakeOffItem Get(long id)
{
var materialGroups = _getMaterialGroupsForTakeOffItemQuery.GetResults(id);
var fabricationNotesByMaterialDetailsId = _getFabricationNotesForTakeOffItemQuery.GetFabricationNotesForMaterialDetails(
materialGroups.Where(mg => mg.MaterialDetails != null).Select(mg => mg.MaterialDetails.Id));
var takeOffItem = base.Get(id);
_assignMaterialGroupsService.AssignMaterialGroups(id, takeOffItem, materialGroups, fabricationNotesByMaterialDetailsId);
_session.Lock(takeOffItem, LockMode.None);
return takeOffItem;
}
public void AssignMaterialGroups(long takeOffItemId, IHasMaterialGroups parent, IList<MaterialGroup> allMaterialGroups,
Dictionary<long, IList<FabricationNote>> fabricationNotesByMaterialDetailsId)
{
if (parent is TakeOffItem)
parent.MaterialGroups = allMaterialGroups.Where(mg => mg.ParentTakeOffItem != null && mg.ParentTakeOffItem.Id == takeOffItemId).ToList();
else if (parent is MaterialGroup)
{
var group = (MaterialGroup) parent;
parent.MaterialGroups = allMaterialGroups.Where(mg => mg.ParentMaterialGroup != null && mg.ParentMaterialGroup.Id == parent.Id).ToList();
if (group.MaterialDetails != null)
{
if (fabricationNotesByMaterialDetailsId.ContainsKey(group.MaterialDetails.Id))
group.MaterialDetails.FabricationNotes = fabricationNotesByMaterialDetailsId[group.MaterialDetails.Id];
else
group.MaterialDetails.FabricationNotes = new List<FabricationNote>();
}
}
else
throw new NotSupportedException();
foreach (var group in parent.MaterialGroups)
{
_session.Lock(group, LockMode.None);
if (group.MaterialDetails != null)
_session.Lock(group.MaterialDetails, LockMode.None);
AssignMaterialGroups(takeOffItemId, group, allMaterialGroups, fabricationNotesByMaterialDetailsId);
}
}
There are a couple problems with this. First, because I'm manually populating a collection on the TakeOffItem entity, NHibernate thinks that the TakeOffItem is dirty now. I have cascade="save-update" on TakeOffItem.MaterialGroups (and there are more cascading relationships under that), and since the TakeOffItem is considered to be dirty, it will save the entire TakeOffItem object graph when it saves it. That's OK if I really wanted to save the TakeOffItem, but if I don't want to save the TakeOffItem, it ends up doing a lot of queries that are basically unnecessary.
In order to get around some of these issues, I implmented Fabio's code that will disable the auto-dirty-check behavior in NHibernate. Now it will only save things that I call SaveOrUpdate() on (along with the cascading relationships), so it doesn't matter anymore that NHibernate thinks that all of these other objects are dirty because it won't save them when I flush the session. But now something else is broken because if I change collections in a MaterialGroup object (for example, TakeOffItem.MaterialGroups[0].MaterialGroups.Add(something)), NHibernate doesn't realize that it needs to save those objects. If I remove all of my custom loading code, Fabio's code works fine. But I need the custom loading code for optimization reasons.
I think part of the problem also stems from the fact that I can't tell NHibernate that an entity is NOT dirty (if there is a way, I'd love to know!). I would really like to be able to do my custom loading and then tell NHibernate, "Hey, this entire object graph is not dirty, pretend like you just loaded it."
Thanks again for any help.