tags:

views:

286

answers:

2

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.

+1  A: 

It looks like you are doing something funny with the CreateSql I would guess that you aren't returning entities from it. Please post whatever it is you are doing with the CreateSqlQuery You absolutely should not be forced to use session.Lock

Ayende Rahien
A: 

I figured out the problem, and it wasn't very obvious that what I was doing was wrong. I guess that's what you get when you try to hack with NHibernate.

The problem is with this code:

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

The problem is that I'm doing by custom query before I loaded the TakeOffItem (the parent entity). Normally when you load things with NHibernate, the parents get loaded first and the list of known entities in the session will have the parent before the children. If you don't have the parent in the list before the children, the auto-dirty-check code doesn't work because it goes sequentially through the list of known entities, mark child entities as dirty as it goes. Well, if it tries to process the child before the parent, the child entity gets marked as dirty after NHibernate has checked to see if it needed to save it, so the child doesn't get saved.

I don't know what ISession.Lock() does, but like Ayende said in his answer, I don't need to be doing that anymore. So I took out the Lock() calls.

Here is what the new code looks like:

public override TakeOffItem Get(long id)
{
    // Moved this line up
    var takeOffItem = base.Get(id);

    var materialGroups = _getMaterialGroupsForTakeOffItemQuery.GetResults(id);

    var fabricationNotesByMaterialDetailsId = _getFabricationNotesForTakeOffItemQuery.GetFabricationNotesForMaterialDetails(
        materialGroups.Where(mg => mg.MaterialDetails != null).Select(mg => mg.MaterialDetails.Id));

    _assignMaterialGroupsService.AssignMaterialGroups(id, takeOffItem, materialGroups, fabricationNotesByMaterialDetailsId);
    _session.Lock(takeOffItem, LockMode.None);
    return takeOffItem;
}

The moral of the story is this: if you disable the auto-dirty-check behavior AND you do custom loading of entities, make sure that you load the parent up before the children.

Jon Kruger