Search code examples
nhibernatefluent-nhibernatenhibernate-mapping

NHibernate, adding children via parent vs setting reference to parent on children


I'm currently working on my first NHibernate project.

To test it out I am building an entity graph and that graph I try to persist to the database. When I add children to the parent via the add on the IList the insert is not working in the database because I get null column exceptions on the database (code snippet 1)

  1. When I set a reference on the child to the parent it is working (though this feels not natural for me, see code snippet
  2. Is this normal behavior or am I doing something wrong?

Snippet 1:

var country = new Country();
var countryLocale = new CountryLocale { LangCode = "nl-nl", Name = "Nederland" };
country.CountryLocales.Add(countryLocale);

var city = new City();
var cityLocale = new CityLocale { LangCode = "nl-nl", Name = "Amsterdam" };
city.CityLocales.Add(cityLocale);
country.Cities.Add(city);

Snippet 1 Error:

Cannot insert the value NULL into column 'CountryId', table 'ArtWorld.dbo.City'; column does not allow nulls. INSERT fails.\r\nThe statement has been terminated.

snippet 2:

var country = new Country();

var countryLocale = new CountryLocale { Country = country,LangCode = "nl-nl", 
                                        Name = "Nederland" };
var city = new City{Country = country};

var cityLocale = new CityLocale { City = city, LangCode = "nl-nl", 
                                        Name = "Amsterdam" };

session.SaveOrUpdate(country);

City and CityLocale Map:

public class CityMap : ClassMap<City>
{
    public CityMap()
    {
        Table("City");
        Id(x => x.Id);
        References(x => x.Country).Column("CountryId").Cascade.SaveUpdate();
        HasMany(x => x.CityLocales).KeyColumn("CityId").Cascade.SaveUpdate();
    }
}

public class CityLocaleMap : ClassMap<CityLocale>
{
    public CityLocaleMap()
    {
        Table("City_Locale");
        Id(x => x.Id);
        Map(x => x.LangCode).Not.Nullable();
        Map(x => x.Name).Not.Nullable();
        References(x => x.City).Column("CityId").Cascade.SaveUpdate();
    }
}

this is my Country/Countrylocale map:

public class CountryMap : ClassMap<Country>
{
    public CountryMap()
    {
        Table("Country");
        Id(x => x.Id);
        HasMany(x => x.Cities).KeyColumn("CountryId").Cascade.SaveUpdate();
        HasMany(x => x.CountryLocales).KeyColumn("CountryId").Cascade.SaveUpdate();
    }

}

public class CountryLocaleMap :ClassMap<CountryLocale>
{
    public CountryLocaleMap()
    {
        Table("Country_Locale");
        Id(x => x.Id);
        Map(x => x.LangCode).Not.Nullable();
        Map(x => x.Name).Not.Nullable();
        References(x => x.Country).Column("CountryId").Cascade.SaveUpdate();
    }
}

Solution

  • NHibernate Mapping changes :

    On the Country side you have

      HasMany(x => x.CountryLocales).KeyColumn("CountryId").Cascade.SaveUpdate()
    

    On the CountryLocale side you have

    References(x => x.Country).Column("CountryId").Cascade.SaveUpdate();
    

    One of the relations should be marked as inverse using .Inverse().

    The .Cascase.SaveUpdate() makes sure that NHibernate manages the life cycle of the child entity CountryLocale when a transient child is added to the parent Country's list of CountryLocales. If you dont want to explicitly handle the life cycle of CountryLocale yourself , I would suggest to mark the Many-To-One side from CountryLocale -> Country as Inverse.

    There are two ways you could make your referencing Country in CountryLocale less of a sour eye and more intutive

    If Country manages the Locales

    Add a method in Country which will manage the Locales which are being added. A client (user of Country and CountryLocale need not explicitly play with the references)

    public virtual bool AddCountryLocales(CountryLocale locale)
    {
            if(!this.CountryLocales.Contains(locale))
            {
                  locale.Country =this;
                  this.CountryLocales.Add(locale);
                  return true;  
            }
            return false;
    }
    

    CountryLocale is more domain driven

    By redefining it such that you cannot have a CountryLocale without a Country

    public class CountryLocale
    {
        public CountryLocale(Country country)
        {
             this.Country = country;
        }
    
        //you need a no-agrument constructor for NHibernate
        protected CountryLocale()
        {
        }
    }