wysinawyg
wysinawyg

Reputation: 280

Fluent NHibernate Mapping / Parent/Child delete fail using composite key

I'm struggling to understand why when I remove a child Settings object from MyUser.Settings and SAVE MyUser I get SQL errors like below:

Cannot insert the value NULL into column 'MyUserId', table '###.Settings'; column does not allow nulls. UPDATE fails.
The statement has been terminated.

What I would expect to happen is that removing the item from the collection, then saving MyUser causes NHibernate to issue a DELETE command for the given child. However, what it does is UPDATE the relevant row for the Settings object, setting MyUserId to NULL - which isn't allowed as I'm using a Composite Key.

I've tried so many combinations of Inverse() and the various Cascade options but nothing seems to work. I should point out that Adding to the collection works perfectly when I save MyUser.

I'm totally baffled!

Below is pseudo code to try and explain my entities and mappings.

public class SettingType
{
    public virtual int SettingTypeId { get; set; }
    public virtual string Name { get; set; }
    public virtual bool Active { get; set; }
}

public class Setting
{
    public virtual MyUser MyUser { get; set; }
    public virtual SettingType SettingType { get; set; }
    public virtual DateTime Created { get; set; }
}

public class MyUser
{
    public virtual int MyUserId { get; set; }
    public virtual IList<Setting> Settings { get; set; }
    public virtual string Email { get; set; }

    public void AddSetting(SettingType settingType, DateTime now)
    {
        var existing = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);

        if (existing != null)
        {
            existing.Updated = now;
        }
        else
        {
            var setting = new Setting
            {
                MyUser = this,
                SettingType = settingType,
                Created = now,
            };

            _settings.Add(setting);
        }
    }

    public void RemoveSetting(SettingType settingType)
    {
        var existingPref = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);

        if (existingPref != null)
        {
            _settings.Remove(existingPref);
        }
    }

    private readonly IList<Setting> _settings = new List<Setting>();
}

And my mappings:

public class SettingTypeMap : IAutoMappingOverride<SettingType>
{
    public void Override(AutoMapping<SettingType> mapping)
    {
        mapping.Table("SettingTypes");
        mapping.Id(m => m.SettingTypeId).GeneratedBy.Identity();
        mapping.Map(m => m.Name).Not.Nullable().Length(100);
        mapping.Map(m => m.Active).Not.Nullable().Default("0");
    }
}

public class SettingMap : IAutoMappingOverride<Setting>
{
    public void Override(AutoMapping<Setting> mapping)
    {
        mapping.Table("Settings");
        mapping.CompositeId()
            .KeyReference(m => m.MyUser)
            .KeyReference(m => m.SettingType);
        mapping.Map(m => m.Created).Not.Nullable().Default("CURRENT_TIMESTAMP");
        mapping.Map(m => m.Updated).Nullable();
    }
}

public class MyUserMappingOverride : IAutoMappingOverride<MyUser>
{
    public void Override(AutoMapping<MyUser> mapping)
    {
        mapping.Table("MyUsers");
        mapping.Id(m => m.MyUserId).GeneratedBy.Identity();
        mapping.Map(m => m.Email).Not.Nullable().Length(200);
        mapping.HasMany(m => m.Settings).KeyColumn("MyUserId").Cascade.DeleteOrphan()
            .Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
    }
}

All using:

FluentNHibernate v1.3.0.733

NHibernate v3.3.1.4000

UPDATE: After a few suggestions I've tried to change the mapping for MyUser entity.

First to this:

 mapping.HasMany(m => m.Settings)
            .KeyColumn("MyUserId")
            .Inverse()
            .Cascade.DeleteOrphan()
            .Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);

This gives the error: Given key was not present in the dictionary

So tried to add second key column:

 mapping.HasMany(m => m.Settings)
            .KeyColumn("MyUserId")
            .KeyColumn("SettingTypeId")
            .Inverse()
            .Cascade.DeleteOrphan()
            .Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);

But this then causes odd behaviour when loading the Settings collection from the DB for a given MyUserId. Looking at the nh profiler I see a second SELECT ... FROM Settings but setting the SettingTypeId same as value for MyUserId.

Still totally baffled. Has cost me too much time so going to revert to adding a primary key id field to the Settings entity. Maybe you just can't do what I'm trying using NHibernate. In pure SQL this is simple.

Upvotes: 1

Views: 2111

Answers (1)

Radim K&#246;hler
Radim K&#246;hler

Reputation: 123861

You should use the Inverse mapping

mapping.HasMany(m => m.Settings)
  .KeyColumn("MyUserId")
  .Inverse()
  .Cascade.DeleteOrphan()
  .Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);

This will allow NHibernate to ask the setting itself to be deleted. Otherwise, NHibernate firstly tries to delete the relation, and would try to delete the entity.

See: 6.4. One-To-Many Associations

Very Important Note: If the column of a association is declared NOT NULL, NHibernate may cause constraint violations when it creates or updates the association. To prevent this problem, you must use a bidirectional association with the many valued end (the set or bag) marked as inverse="true". See the discussion of bidirectional associations later in this chapter.

Upvotes: 2

Related Questions