NHibernate Table Per Subclass results in nonsensical INSERT statement - What am I doing wrong?

407 views Asked by At

I have the following entities:

public abstract class User : IIdentity
{
    private readonly UserType _userType;

    public virtual int EntitySK { get; set; }
    public virtual int TenantSK { get; set; }
    public abstract string Name { get; set; }
    public virtual PublicKey PublicKey { get; set; }
    public abstract string AuthenticationType { get; set; }
    public virtual bool IsAuthenticated { get; protected internal set; }
    public virtual bool LoginEnabled { get; set; }
    public virtual bool LockedOut { get; set; }
    public virtual int NumberOfFailedLoginAttempts { get; set; }

    // Hibernate requires this constructor
    protected User()
    {
        this._userType = this is PersonUser ? Models.UserType.Person : Models.UserType.Client;
        this.LoginEnabled = true;
    }

    protected User(UserType userType)
    {
        this._userType = userType;
        this.LoginEnabled = true;
    }

    public virtual UserType UserType
    {
        get { return this._userType; }
        set
        {
            if(value != this._userType)
                throw new InvalidOperationException("Attempted to load " + value + " into " + this._userType + "User.");
        }
    }
}

public class PersonUser : User
{
    public virtual string Domain { get; set; }
    public override string Name { get; set; }
    public virtual byte[] Password { get; set; }
    public virtual byte[] Pepper { get; set; }
    public virtual string EmailAddress { get; set; }
    public virtual int PersonSK { get; set; }
    public override string AuthenticationType { get; set; }

    public PersonUser() : base(UserType.Person) { }
}

public class ClientUser : User
{
    public override string Name { get; set; }
    public virtual string SharedSecret { get; set; }
    public virtual ISet<string> Scopes { get; set; }
    public virtual ISet<GrantType> AuthorizedGrantTypes { get; set; }
    public virtual ISet<Uri> RegisteredRedirectUris { get; set; }
    public virtual int AuthorizationCodeValiditySeconds { get; set; }
    public virtual int AccessTokenValiditySeconds { get; set; }

    public ClientUser() : base(UserType.Client) { }
}

I map these entities using the following Hibernate Conformist mapping:

public class UserMapping : ClassMapping<User>
{
    public UserMapping()
    {
        LogManager.GetLogger().Info("Initialized User mapping.");

        this.Table("Authentication_Users");
        this.Id(u => u.EntitySK,
            m => {
                m.Column("UserSK");
                m.Generator(Generators.Identity);
                m.UnsavedValue(0);
            });
        this.Property(u => u.TenantSK,
            m => {
                m.Column("TenantSK");
                m.NotNullable(true);
            });
        this.Property(u => u.PublicKey,
            m => {
                m.Column("PublicKey");
                m.Type<PublicKeyCustomType>();
                m.NotNullable(false);
                m.Lazy(true);
            });
        this.Property(u => u.UserType,
            m => {
                m.Column("UserType");
                m.NotNullable(true);
                m.Type<EnumCustomType<UserType>>();
            });
        this.Property(u => u.LoginEnabled,
            m => {
                m.Column("LoginEnabled");
                m.NotNullable(true);
            });
        this.Property(u => u.LockedOut,
            m => {
                m.Column("LockedOut");
                m.NotNullable(true);
            });
        this.Property(u => u.NumberOfFailedLoginAttempts,
            m => {
                m.Column("NumberOfFailedLoginAttempts");
                m.NotNullable(true);
            });
        this.Discriminator(d => d.Column("UserType"));
    }
}

public class PersonUserMapping : SubclassMapping<PersonUser>
{
    public PersonUserMapping()
    {
        LogManager.GetLogger().Info("Initialized PersonUser mapping.");

        this.DiscriminatorValue((int)UserType.Person);
        this.Join(
            "PersonUser",
            j =>
            {
                j.Table("Authentication_Users_PersonUsers");
                j.Key(m => {
                        m.Column("UserSK");
                        m.NotNullable(true);
                        m.OnDelete(OnDeleteAction.Cascade);
                        m.Unique(true);
                        m.Update(false);
                    });
                j.Property(u => u.Domain,
                    m => {
                        m.Column("DomainName");
                        m.NotNullable(false);
                    });
                j.Property(u => u.Name,
                    m => {
                        m.Column("Username");
                        m.NotNullable(true);
                    });
                j.Property(u => u.Password,
                    m => {
                        m.Column("Password");
                        m.NotNullable(false);
                        m.Lazy(true);
                    });
                j.Property(u => u.Pepper,
                    m => {
                        m.Column("Pepper");
                        m.NotNullable(false);
                        m.Lazy(true);
                    });
                j.Property(u => u.EmailAddress,
                    m => {
                        m.Column("EmailAddress");
                        m.NotNullable(false);
                    });
                j.Property(u => u.PersonSK,
                    m => {
                        m.Column("PersonSK");
                        m.NotNullable(false);
                    });
                j.Property(u => u.AuthenticationType,
                    m => {
                        m.Column("AuthenticationType");
                        m.NotNullable(true);
                    });
            }
        );
    }
}

public class ClientUserMapping : SubclassMapping<ClientUser>
{
    public ClientUserMapping()
    {
        LogManager.GetLogger().Info("Initialized ClientUser mapping.");

        this.DiscriminatorValue((int)UserType.Client);
        this.Join(
            "ClientUser",
            j =>
            {
                j.Table("Authentication_Users_ClientUsers");
                j.Key(m => {
                        m.Column("UserSK");
                        m.NotNullable(true);
                        m.OnDelete(OnDeleteAction.Cascade);
                        m.Unique(true);
                        m.Update(false);
                    });
                j.Property(u => u.Name,
                    m => {
                        m.Column("DisplayName");
                        m.NotNullable(true);
                    });
                j.Property(u => u.SharedSecret,
                    m => {
                        m.Column("SharedSecret");
                        m.NotNullable(true);
                    });
                j.Property(u => u.AuthorizationCodeValiditySeconds,
                    m => {
                        m.Column("AuthorizationCodeValiditySeconds");
                        m.NotNullable(true);
                    });
                j.Property(u => u.AccessTokenValiditySeconds,
                    m => {
                        m.Column("AccessTokenValiditySeconds");
                        m.NotNullable(true);
                    });

                j.Set(u => u.Scopes,
                    s => {
                        s.Fetch(CollectionFetchMode.Join);
                        s.Lazy(CollectionLazy.Lazy);
                        s.Table("Authentication_Users_ClientUsers_Scopes");
                        s.Key(m => {
                            m.Column("UserSK");
                            m.NotNullable(true);
                        });
                    },
                    r => r.Element(m => {
                            m.Column("Scope");
                            m.NotNullable(true);
                            m.Unique(true);
                    }));

                j.Set(u => u.AuthorizedGrantTypes,
                    s => {
                        s.Fetch(CollectionFetchMode.Join);
                        s.Lazy(CollectionLazy.Lazy);
                        s.Table("Authentication_Users_ClientUsers_AuthorizedGrantTypes");
                        s.Key(m => {
                            m.Column("UserSK");
                            m.NotNullable(true);
                        });
                    },
                    r => r.Element(m => {
                            m.Column("GrantType");
                            m.NotNullable(true);
                            m.Unique(true);
                            m.Type<EnumCustomType<GrantType>>();
                    }));

                j.Set(u => u.RegisteredRedirectUris,
                    s => {
                        s.Fetch(CollectionFetchMode.Join);
                        s.Lazy(CollectionLazy.Lazy);
                        s.Table("Authentication_Users_ClientUsers_RegisteredRedirectUris");
                        s.Key(m => {
                            m.Column("UserSK");
                            m.NotNullable(true);
                        });
                    },
                    r => r.Element(m => {
                            m.Column("Uri");
                            m.NotNullable(true);
                            m.Unique(true);
                            m.Type<UriCustomType>();
                    }));
            }
        );
    }
}

The EnumCustomType is an IUserType that maps C# enums to integer columns.

I formulated this design and mapping after much research and consultation with the NHibernate reference documentation and this blog (specific page) (summary). I am sure this is the entity design I want, but of course it's possible (likely?) I got the mapping wrong.

When I start up and configure NHibernate, it loads the mappings and does not complain. The log output has no warnings about the mappings. However, when I create a PersonUser, assign values to all of its properties, and Add it to the ISession, the most peculiar thing happens:

2014-01-16 00:58:34,465 DEBUG NHibernate.AdoNet.AbstractBatcher.Generate() - Building an IDbCommand object for the SqlString: INSERT INTO Authentication_Users (TenantSK, DisplayName, PublicKey, LoginEnabled, LockedOut, NumberOfFailedLoginAttempts, Username, AuthenticationType, UserType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '1'); select SCOPE_IDENTITY()
2014-01-16 00:58:34,472 DEBUG NHibernate.Persister.Entity.AbstractEntityPersister.Dehydrate() - Dehydrating entity: [Models.PersonUser#<null>]
2014-01-16 00:58:34,475 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding '0' to parameter: 0
2014-01-16 00:58:34,478 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'nick.williams' to parameter: 1
2014-01-16 00:58:34,482 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'PublicKey' to parameter: 3
2014-01-16 00:58:34,485 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'True' to parameter: 4
2014-01-16 00:58:34,486 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'False' to parameter: 5
2014-01-16 00:58:34,487 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding '0' to parameter: 6
2014-01-16 00:58:34,488 DEBUG NHibernate.Type.NullableType.NullSafeSet() - binding 'nick.williams' to parameter: 8
NHibernate.PropertyValueException : Error dehydrating property value for Models.PersonUser.Name
  ----> System.IndexOutOfRangeException : Invalid index 8 for this SqlParameterCollection with Count=8.
   at NHibernate.Persister.Entity.AbstractEntityPersister.Dehydrate(Object id, Object[] fields, Object rowId, Boolean[] includeProperty, Boolean[][] includeColumns, Int32 table, IDbCommand statement, ISessionImplementor session, Int32 index)
   at NHibernate.Persister.Entity.AbstractEntityPersister.GeneratedIdentifierBinder.BindValues(IDbCommand ps)
   at NHibernate.Id.Insert.AbstractReturningDelegate.PerformInsert(SqlCommandInfo insertSQL, ISessionImplementor session, IBinder binder)
   at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object[] fields, Object obj, ISessionImplementor session)
   at NHibernate.Action.EntityIdentityInsertAction.Execute()
   at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
   at NHibernate.Event.Default.AbstractSaveEventListener.PerformSaveOrReplicate(Object entity, EntityKey key, IEntityPersister persister, Boolean useIdentityColumn, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
   at NHibernate.Event.Default.AbstractSaveEventListener.SaveWithGeneratedId(Object entity, String entityName, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
   at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.SaveWithGeneratedOrRequestedId(SaveOrUpdateEvent event)
   at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.EntityIsTransient(SaveOrUpdateEvent event)
   at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(SaveOrUpdateEvent event)
   at NHibernate.Impl.SessionImpl.FireSave(SaveOrUpdateEvent event)
   at NHibernate.Impl.SessionImpl.Save(Object obj)

Importantly, take a look at that SQL generated:

INSERT INTO Authentication_Users (TenantSK, DisplayName, PublicKey, LoginEnabled, LockedOut, NumberOfFailedLoginAttempts, Username, AuthenticationType, UserType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '1')

That makes no sense. It in no way corresponds to the mapping at all. It's got columns from three different tables in an insert statement for one table. It has columns from PersonUser AND from ClientUser (which shouldn't be possible), it's binding parameters with nonexistent parameter indexes, and it's not even including all the properties I set!

I've been playing around with this for several hours now without any progress. I'm at a complete loss here. It just doesn't make any sense. Anyone seen this before? Any idea what's going on?

EDIT I forgot to mention: I'm using the discriminator here because I want to be able to get a generic User by its ID and it return the proper PersonUser or ClientUser depending on which type it is.

2

There are 2 answers

6
MichaC On BEST ANSWER

There are some issues with the mapping

a) You are mapping the UserType as property but you are using it as discriminator, too. But the user type value is generated by nhibernate, because of the discriminator usage... Therefore you can define the property to be generated.

this.Property(u => u.UserType,
    m =>
    {
        m.Column("UserType");
        m.NotNullable(true);
        m.Generated(PropertyGeneration.Always); // this should fix it for user type
        m.Type<EnumCustomType<UserType>>();
    });

You should also not throw an exception on the setter... to prevent UserType being set from somewhere, simply mark the auto property setter protected internal

    public virtual UserType UserType
    {
        get;
        protected internal set;
    }

b) You are mapping the Name attribute of your base class in your sub classes only and you are trying to map that property to different columns within your sub classes' tables. Don't know if this is even possible, usually you have to map all properties of your base class to the base class table, or move the property into the subclass...

To fix this, simply remove the mappings to Name from your subclass mappings and move it to your base class mapping.

0
Nick Williams On

With MichaC's help, this was the code that finally worked.

Entities:

public abstract class User : IIdentity
{
    // necessary because the property is read-only
    private readonly UserType _userType;
    // necessary because the property needs to default to true
    private bool _loginEnabled = true;

    //-------- These properties are mapped to database columns --------//
    public virtual int ObjectId { get; set; }
    public virtual int? TenantId { get; set; }
    public virtual PublicKey PublicKey { get; set; }
    public virtual bool LoginEnabled { get { return this._loginEnabled; } set { this._loginEnabled = value; } }
    public virtual bool LockedOut { get; set; }
    public virtual int NumberOfFailedLoginAttempts { get; set; }

    //-------- These properties are NOT mapped to database columns --------//
    public abstract string Name { get; set; }
    public abstract string AuthenticationType { get; set; }
    public virtual bool IsAuthenticated { get; protected internal set; }
    public virtual UserType UserType
    {
        get { return this._userType; }
        set { throw new InvalidOperationException("Property UserType is read-only."); }
    }

    ...
}

public class PersonUser : User
{
    //-------- These properties are mapped to database columns --------//
    public virtual string Domain { get; set; }
    protected internal virtual string Username { get { return this.Name; } set { this.Name = value; } }
    public virtual byte[] Password { get; set; }
    public virtual byte[] Pepper { get; set; }
    public virtual string EmailAddress { get; set; }
    public virtual int PersonSK { get; set; }
    protected internal virtual string AuthenticationStrategy
    {
        get { return this.AuthenticationType; }
        set { this.AuthenticationType = value; }
    }

    //-------- These properties are NOT mapped to database columns --------//
    public override string Name { get; set; }
    public override string AuthenticationType { get; set; }
}

public class ClientUser : User
{
    //-------- These properties are mapped to database columns --------//
    protected internal virtual string DisplayName { get { return this.Name; } set { this.Name = value; } }
    public virtual string SharedSecret { get; set; }
    public virtual ISet<string> Scopes { get; set; }
    public virtual ISet<GrantType> AuthorizedGrantTypes { get; set; }
    public virtual ISet<Uri> RegisteredRedirectUris { get; set; }
    public virtual int AuthorizationCodeValiditySeconds { get; set; }
    public virtual int AccessTokenValiditySeconds { get; set; }

    //-------- These properties are NOT mapped to database columns --------//
    public override string Name { get; set; }
    public override string AuthenticationType
    {
        get { return AuthorizationHeaderProtocol.SignatureClientCredentials; }
        set { throw new InvalidOperationException("Cannot change the authentication type for a ClientUser."); }
    }
}

Mappings:

public class UserMapping : ClassMapping<User>
{
    public UserMapping()
    {
        LogManager.GetLogger().Info("Initialized User mapping.");

        this.Table("Authentication_Users");
        this.Id(u => u.ObjectId,
            m => {
                m.Column("UserId");
                m.Generator(Generators.Identity);
                m.UnsavedValue(0);
            });
        this.Property(u => u.TenantId,
            m => {
                m.Column("TenantId");
                m.NotNullable(false);
            });
        this.Property(u => u.PublicKey,
            m => {
                m.Column("PublicKey");
                m.Type<PublicKeyCustomType>();
                m.NotNullable(false);
                m.Lazy(true);
            });
        this.Property(u => u.LoginEnabled,
            m => {
                m.Column("LoginEnabled");
                m.NotNullable(true);
            });
        this.Property(u => u.LockedOut,
            m => {
                m.Column("LockedOut");
                m.NotNullable(true);
            });
        this.Property(u => u.NumberOfFailedLoginAttempts,
            m => {
                m.Column("NumberOfFailedLoginAttempts");
                m.NotNullable(true);
            });
        this.Discriminator(d => d.Column("UserType"));
    }
}

public class PersonUserMapping : SubclassMapping<PersonUser>
{
    public PersonUserMapping()
    {
        LogManager.GetLogger().Info("Initialized PersonUser mapping.");

        this.DiscriminatorValue((int)UserType.Person);
        this.Join(
            "PersonUser",
            j =>
            {
                j.Table("Authentication_Users_PersonUsers");
                j.Key(m => {
                        m.Column("UserId");
                        m.NotNullable(true);
                        m.OnDelete(OnDeleteAction.Cascade);
                        m.Unique(true);
                        m.Update(false);
                    });
                j.Property(u => u.Domain,
                    m => {
                        m.Column("DomainName");
                        m.NotNullable(false);
                    });
                j.Property("Username", // protected internal, see NH-3485
                    m => {
                        m.Column("Username");
                        m.NotNullable(true);
                    });
                j.Property(u => u.Password,
                    m => {
                        m.Column("Password");
                        m.NotNullable(false);
                        m.Lazy(true);
                    });
                j.Property(u => u.Pepper,
                    m => {
                        m.Column("Pepper");
                        m.NotNullable(false);
                        m.Lazy(true);
                    });
                j.Property(u => u.EmailAddress,
                    m => {
                        m.Column("EmailAddress");
                        m.NotNullable(false);
                    });
                j.Property(u => u.PersonSK,
                    m => {
                        m.Column("PersonSK");
                        m.NotNullable(false);
                    });
                j.Property("AuthenticationStrategy", // protected internal, see NH-3485
                    m => {
                        m.Column("AuthenticationType");
                        m.NotNullable(true);
                    });
            }
        );
    }
}

public class ClientUserMapping : SubclassMapping<ClientUser>
{
    public ClientUserMapping()
    {
        LogManager.GetLogger().Info("Initialized ClientUser mapping.");

        this.DiscriminatorValue((int)UserType.Client);
        this.Join(
            "ClientUser",
            j =>
            {
                j.Table("Authentication_Users_ClientUsers");
                j.Key(m => {
                        m.Column("UserId");
                        m.NotNullable(true);
                        m.OnDelete(OnDeleteAction.Cascade);
                        m.Unique(true);
                        m.Update(false);
                    });
                j.Property("DisplayName", // protected internal, see NH-3485
                    m => {
                        m.Column("DisplayName");
                        m.NotNullable(true);
                    });
                j.Property(u => u.SharedSecret,
                    m => {
                        m.Column("SharedSecret");
                        m.NotNullable(true);
                    });
                j.Property(u => u.AuthorizationCodeValiditySeconds,
                    m => {
                        m.Column("AuthorizationCodeValiditySeconds");
                        m.NotNullable(true);
                    });
                j.Property(u => u.AccessTokenValiditySeconds,
                    m => {
                        m.Column("AccessTokenValiditySeconds");
                        m.NotNullable(true);
                    });

                j.Set(u => u.Scopes,
                    s => {
                        s.Fetch(CollectionFetchMode.Join);
                        s.Lazy(CollectionLazy.Lazy);
                        s.Table("Authentication_Users_ClientUsers_Scopes");
                        s.Key(m => {
                            m.Column("UserId");
                            m.NotNullable(true);
                        });
                    },
                    r => r.Element(m => {
                            m.Column("Scope");
                            m.NotNullable(true);
                            m.Unique(true);
                    }));

                j.Set(u => u.AuthorizedGrantTypes,
                    s => {
                        s.Fetch(CollectionFetchMode.Join);
                        s.Lazy(CollectionLazy.Lazy);
                        s.Table("Authentication_Users_ClientUsers_AuthorizedGrantTypes");
                        s.Key(m => {
                            m.Column("UserId");
                            m.NotNullable(true);
                        });
                    },
                    r => r.Element(m => {
                            m.Column("GrantType");
                            m.NotNullable(true);
                            m.Unique(true);
                            m.Type<EnumCustomType<GrantType>>();
                    }));

                j.Set(u => u.RegisteredRedirectUris,
                    s => {
                        s.Fetch(CollectionFetchMode.Join);
                        s.Lazy(CollectionLazy.Lazy);
                        s.Table("Authentication_Users_ClientUsers_RegisteredRedirectUris");
                        s.Key(m => {
                            m.Column("UserId");
                            m.NotNullable(true);
                        });
                    },
                    r => r.Element(m => {
                            m.Column("Uri");
                            m.NotNullable(true);
                            m.Unique(true);
                            m.Type<UriCustomType>();
                    }));
            }
        );
    }
}