Understanding foreign key not-null=true and inverse behavior in a zero-to-one relationship with NHibernate
Asked Answered
D

1

11

I'm trying to get NHibernate to use the many side of a collection to manage a bidirectional association to model a zero-to-one relationship.

Parent Class and Map:

public class Parent
{
    private ICollection<Child> children;
    public Parent()
    {
        this.children = new HashedSet<Child>();
    }
    public virtual Guid Id { get; protected internal set; }
    public virtual Child Child
    {
        get { return children.FirstOrDefault(); }
        set
        {
            {
                this.children.Clear();
                if (value != null)
                {
                    this.children.Add(value);
                }
            }
        }
    }
}

public class ParentMap : ClassMap<Parent>
{
    public ParentMap()
    {
        this.Id(x => x.Id)
            .GeneratedBy.GuidComb();
        this.HasMany<Child>(Reveal.Member<Parent>("children"))
            .Access.Field()
            .Cascade.All()
            .Not.Inverse()
            .AsSet();
    }
}

Child Class and Map:

public class Child
{
    public virtual Guid Id { get; protected internal set; }
    public virtual Parent Parent { get; set; }
}

public class ChildMap : ClassMap<Child>
{
    public ChildMap()
    {
        this.Id(x => x.Id)
            .GeneratedBy.GuidComb();
        this.References(x => x.Parent)
            .Not.Nullable()
            .Cascade.All();
    }
}

The following code produces two inserts and an update:

var parent = new Parent();
var child = new Child();
parent.Child = child;
child.Parent = parent;
session.Save(parent);
session.Flush();

Notice the essentially duplicate SQL for the second insert and the following update:

exec sp_executesql N'INSERT INTO [Parent] (Id) VALUES (@p0)',N'@p0 uniqueidentifier',@p0='AA5A146E-E3F5-4373-B7A8-9EF301171401'
go
exec sp_executesql N'INSERT INTO [Child] (Parent_id, Id) VALUES (@p0, @p1)',N'@p0 uniqueidentifier,@p1 uniqueidentifier',@p0='AA5A146E-E3F5-4373-B7A8-9EF301171401',@p1='B78C4461-A217-47FC-BE02-9EF30117140A'
go
exec sp_executesql N'UPDATE [Child] SET Parent_id = @p0 WHERE Id = @p1',N'@p0 uniqueidentifier,@p1 uniqueidentifier',@p0='AA5A146E-E3F5-4373-B7A8-9EF301171401',@p1='B78C4461-A217-47FC-BE02-9EF30117140A'
go

While this code produces the infamous not-null property references a null or transient value inverse:

var parent = new Parent();
var child = new Child();
parent.Child = child;
//child.Parent = parent;
session.Save(parent);
session.Flush();

I've found numerous posts about this, but have yet to find a definitive guide on how to do zero-to-one, with inverse=false on the one side.

I've tried the one-to-many/one-to-one method mentioned here.

As well, I've found several open issues on NHibernate about (not)nullable Foreign Keys: NH-941, NH-1050, etc..

What am I doing wrong?

Edit 2011-05-30

So, my temporary solution is to go for the standard inverse=true setting on the many side, and do some magic in the setter of the Parent:

public virtual Child Child
{
    get { return children.FirstOrDefault(); }
    set
    {
        {
            this.children.Clear();
            if (value != null)
            {
                value.Parent = this;
                this.children.Add(value);
            }
        }
    }
}

But I'm still baffled by the inverse=false behavior, which should be the equivalent of inverse=true on the many-to-one side (interestingly, FluentNhibernate doesn't allow for the ManyToOnePart to set inverse=true like this article recommends).

Denominative answered 31/5, 2011 at 0:17 Comment(0)
T
36

When to use inverse="true|false"

The inverse attribute is used to help NHibernate know which side of a relationship should be used to persist the relationship. The non-inverse side (please note the double negative) is the side that will be persisted. If neither side is inverse, then the relationship will be persisted twice, like with the INSERT followed immediately by an UPDATE example you provided above. If both sides are inverse, then the relationship won't be persisted at all, so it's important to set inverse correctly.

I like to think about inverse in the following way. I don't know whether this is officially the way it works or not, but it helps my world make sense:

many-to-one

many-to-one relationships are always inverse="false". They are always used to persist the relationship to the database. Since they are always inverse="false", there's no need to specify it, so NHibernate (and hence Fluent NHibernate) doesn't provide an option for it.

(I have run across only one situation where I wish I could specify inverse="true" on a many-to-one. If you have a one-to-many list on one side and a many-to-one on the other side, you should be able to let the list control the relationship so that NHibernate can take care of setting the index values for you. As it currently stands, you have to add a property to the child class and manage the index values yourself.)

one-to-one

one-to-one relationships are always inverse="true". They never exist without either an id or many-to-one on the other side of the relationship which will take care of persisting the relationship. Since the inverse value is always the same, there's no need to specify it, so specifying it is not supported.

Collections

Collections like bag, list, set, etc. may or may not be a part of a bi-directional relationship. If they exist on their own (perhaps a bag of string elements), then they need to be inverse="false" (which is the default) because no one else will be responsible for persisting the relationship. If they exist in conjunction with another relationship, however (like your traditional one-to-many/many-to-one) they should be specified as inverse="true".

With many-to-many collections where you have a collection on either side of the relationship, mark one of them as inverse="true" and leave the other one as the default inverse="false". Again, the point is that one side of the relationship must be non-inverse. Which side should you pick? If we take a many-to-many relationship between Users and Roles for example, you probably have lots of Users and a few Roles. In my opinion, you should map Role.Users as inverse="true" and let User.Roles control the relationship since it's a smaller set of data to work with and it's probably the collection you care more about anyway.

(In fact, I would be hesitant to include Role.Users in the model at all. Suppose a "Customer" role has 100,000 users. Then customerRole.Users is an unusable lazy-loading bomb waiting to explode.)

...back to your question...

Since it doesn't really matter which side of the relationship is inverse, just so long as one side is non-inverse, then you should make the one-to-one side the inverse side since that's the way NHibernate wants to do it. Don't fight the tool over stuff that doesn't matter. In the mappings you provided, essentially both sides of the relationship have been marked non-inverse, which caused the relationship to be persisted twice. The following mappings should work better for you:

public class Parent
{
    public virtual Guid Id { get; set; }
    public virtual Child Child { get; set; }
}

public class ParentClassMap : ClassMap<Parent>
{
    public ParentClassMap()
    {
        Id(x => x.Id);
        HasOne(x => x.Child)
            .PropertyRef(x => x.Parent)
            .Cascade.All();
    }
}

public class Child
{
    public virtual Guid Id { get; set; }
    public virtual Parent Parent { get; set; }
}

public class ChildClassMap : ClassMap<Child>
{
    public ChildClassMap()
    {
        Id(x => x.Id);
        References(x => x.Parent)
            .Not.Nullable()
            .Unique()
            .Cascade.SaveUpdate();
    }
}

... which results in the following SQL from your test insertion code:

exec sp_executesql N'INSERT INTO [Parent] (Id) VALUES (@p0)',N'@p0 uniqueidentifier',@p0='925237BE-558B-4985-BDA2-9F36000797F5'
exec sp_executesql N'INSERT INTO [Child] (Parent_id, Id) VALUES (@p0, @p1)',N'@p0 uniqueidentifier,@p1 uniqueidentifier',@p0='925237BE-558B-4985-BDA2-9F36000797F5',@p1='BE6D931A-8A05-4662-B5CD-9F36000797FF'

No update query!

Tunisia answered 5/8, 2011 at 4:44 Comment(3)
related answer (by me :-) #1061679Erechtheum
What about many-to-many when you have a middle table which holds the relation between 2 entities?Incisure
@Incisure many-to-many is addressed above in the 2nd paragraph of the "Collections" section.Tunisia

© 2022 - 2024 — McMap. All rights reserved.