-1

Starting to use Nhibernate for persistency being seduced by the promise that it respects your domain model, I tried to implement a relation manager for my domain objects. Basically, to DRY my code with respect to managing bidirectional one to many and many to many relations, I decided to have those relations managed by a separate class. When a one to many or many to one property is set an entry for the two objects is made in an dictionary, the key is either a one side with a collection value to hold the many sides, or a many side with a value of the one side.

A one to many relation for a specific combination of types looks as follows:

public class OneToManyRelation<TOnePart, TManyPart> : IRelation<IRelationPart, IRelationPart>
    where TOnePart : class, IRelationPart
    where TManyPart : class, IRelationPart
{
    private readonly IDictionary<TOnePart, Iesi.Collections.Generic.ISet<TManyPart>> _oneToMany;
    private readonly IDictionary<TManyPart, TOnePart> _manyToOne;

    public OneToManyRelation()
    {
        _manyToOne = new ConcurrentDictionary<TManyPart, TOnePart>();
        _oneToMany = new ConcurrentDictionary<TOnePart, Iesi.Collections.Generic.ISet<TManyPart>>();
    }

    public void Set(TOnePart onePart, TManyPart manyPart)
    {
        if (onePart == null || manyPart == null) return;
        if (!_manyToOne.ContainsKey(manyPart)) _manyToOne.Add(manyPart, onePart);
        else _manyToOne[manyPart] = onePart;
    }

    public void Add(TOnePart onePart, TManyPart manyPart)
    {
        if (onePart == null || manyPart == null) return;

        if (!_manyToOne.ContainsKey(manyPart)) _manyToOne.Add(manyPart, onePart);
        else _manyToOne[manyPart] = onePart;
        if (!_oneToMany.ContainsKey(onePart)) _oneToMany.Add(onePart, new HashedSet<TManyPart>());
        _oneToMany[onePart].Add(manyPart);
    }

    public Iesi.Collections.Generic.ISet<TManyPart> GetManyPart(TOnePart onePart)
    {
        if (!_oneToMany.ContainsKey(onePart)) _oneToMany[onePart] = new HashedSet<TManyPart>();
        return _oneToMany[onePart];
    }

    public TOnePart GetOnePart(TManyPart manyPart)
    {
        if(!_manyToOne.ContainsKey(manyPart)) _manyToOne[manyPart] = default(TOnePart);
        return _manyToOne[manyPart];
    }

    public void Remove(TOnePart onePart, TManyPart manyPart)
    {
        _manyToOne.Remove(manyPart);
        _oneToMany[onePart].Remove(manyPart);
    }

    public void Set(TOnePart onePart, Iesi.Collections.Generic.ISet<TManyPart> manyPart)
    {
        if (onePart == null) return;
        if (!_oneToMany.ContainsKey(onePart)) _oneToMany.Add(onePart, manyPart);
        else _oneToMany[onePart] = manyPart;
    }

    public void Clear(TOnePart onePart)
    {
        var list = new HashedSet<TManyPart>(_oneToMany[onePart]);
        foreach (var manyPart in list)
        {
            _manyToOne.Remove(manyPart);
        }
        _oneToMany.Remove(onePart);
    }

    public void Clear(TManyPart manyPart)
    {
        if (!_manyToOne.ContainsKey(manyPart)) return;
        if (_manyToOne[manyPart] == null) return;

        _oneToMany[_manyToOne[manyPart]].Remove(manyPart);
        _manyToOne.Remove(manyPart);
    }
}

On the many side a code snippet looks like:

    public virtual SubstanceGroup SubstanceGroup
    {
        get { return RelationProvider.SubstanceGroupSubstance.GetOnePart(this); } 
        protected set { RelationProvider.SubstanceGroupSubstance.Set(value, this); }
    }

On the one side, so, in this case the SubstanceGroup, the snippet looks like:

    public virtual ISet<Substance> Substances
    {
        get { return RelationProvider.SubstanceGroupSubstance.GetManyPart(this); }
        protected set { RelationProvider.SubstanceGroupSubstance.Set(this, value); }
    }

Just using my domain objects, this works excellent. In the domain object I just have to reference an abstract factory that retrieves the appropriate relation and I can set the relation from one side, wich automatically becomes thus bidirectional.

However, when NH kicks in the problem is that I get duplicate keys in my dictionaries. Somehow NH sets a relation property with a null value(!) with a new copy(?) of a domain object. So when the domain object gets saved, I have two entries of that domain object in, for example the many side of the relation, i.e. _manyToOne dictionary.

This problem makes me lose my hair, I do not get it what is happening??

halcwb
  • 1,480
  • 1
  • 13
  • 26
  • 1
    I don't really understand what you are doing. Is this relation class a type you are using as field in your entities? How is it mapped? How are you creating, reading, changing the data? – Stefan Steinegger Aug 24 '11 at 12:04
  • I was incomplete in my question, I have edited and added a code snippet how I use this relation class. The relation class by the way represents the relation between two objects, thats it. In the case of a one to many relation, there is the one part and the many part. – halcwb Aug 24 '11 at 15:33
  • 1
    Seems like a convoluted way to manage bidirectional relationships. Seems like this solution is open to issues. I think it's easier just to manually manage these relationships within the Entities themselves with add/remove methods. – Cole W Aug 24 '11 at 18:01
  • I have followed your advice and reverted back to internal private fields to store the references to parent and child objects. Indeed the problem seems to be solved by this. But, I am still puzzled why the above solution did not work, convoluted or not (did save me about 500 lines of code though). – halcwb Aug 25 '11 at 09:00

2 Answers2

3

To answer your first, very general question: "Does NHibernate really deliver transparent persistency", I just can say: nothing is perfect. NH tries its best to be as transparent as possible, by also trying to keep its complexity as low as possible.

There are some assumptions, particularly regarding collections: Collections and their implementations are not considered to be part of your domain model. NH provides its own collection implementations. You are not only expected to use the interfaces like ISet and IList. You should also take the instances given by NH when the object is read from the database and never replace it with your own. (I don't know what your relation class is actually used for, so I don't know if this is the problem here.)

Domain objects are unique within the same instance of the session. If you get new instances of domain objects each time, you probably implemented the "session-per-call" anti-pattern, which creates a new session for each database interaction.

I don't have a clue what you actually are doing. How is this OneToManyRelation actually used for? What are you doing when NH doesn't behave as expected? This is a very specific problem to your specific implementation.

Stefan Steinegger
  • 63,782
  • 15
  • 129
  • 193
  • Thanks for your effort trying to understand my code. With regard to your comment that collections are not considered part of a domain model, I strongly disagree, but I already knew how NH is handling this. So, I respectfully, accept all references to collections from NH and I store them in my Relation class as you can see the many part is a Iesi.Collections.Generic.ISet. Also I do not use the session per call anti-pattern but I regard a session as a unit of work. Once the work is done, the session is gone, my domain model is gone and the client is informed of the fruits of this. – halcwb Aug 24 '11 at 15:41
  • Finally, I do not think that managing one to many relations and many to many relations in domain models are very specific problem. I just noticed that to handle this with local fields in domain classes with special set and get methods, this leads to repetitive duplicate code. If a parent has a child and the child should know it's parent, there should be some code on both sides. And I have a lot fo parent child relations in my code. As well as many to many relations. – halcwb Aug 24 '11 at 15:47
  • IMO, this repetitive code is trivial enough. Sometimes live is easier if the code is trivial, even if some patterns are repetitive - compared to creating magic classes which implement these patterns. – Stefan Steinegger Aug 25 '11 at 10:10
  • On my remark of your point that collections and implementations are not considered part of the domain model: I was wrong! Did not get that but now I think I understand:http://stackoverflow.com/questions/7230158/does-this-solve-nhibernate-identity-problem-and-gethashcode-issues – halcwb Sep 01 '11 at 07:00
0

Besides the comments on 'convoluted code' and 'what the heck are you doing'. The problem was that I was replacing the persistence collections of NH like in the below code snippet:

public void Add(TOnePart onePart, TManyPart manyPart)
{
    if (onePart == null || manyPart == null) return;

    if (!_manyToOne.ContainsKey(manyPart)) _manyToOne.Add(manyPart, onePart);
    else _manyToOne[manyPart] = onePart;
    if (!_oneToMany.ContainsKey(onePart)) _oneToMany.Add(onePart, new HashedSet<TManyPart>());
    _oneToMany[onePart].Add(manyPart);
}

I create a new Hashed set for the many part. And that was the problem. If just has set the many part with the collection coming in (in case of the persistence collection implementation of NH) than it would have worked.

As a NH newbie, this replacing of collections with a special implementation from NH has been an important source of errors. Just as a warning to other NH newbies.

halcwb
  • 1,480
  • 1
  • 13
  • 26
  • By the way, read an interesting post by Stefan Steinegger about evil Dry. Not sure whether my relation manager is that evil, but still an interesting point to consider. – halcwb Aug 26 '11 at 10:03