NHibernate Table Per Subclass приводит к бессмысленному утверждению INSERT - Что я делаю неправильно?
У меня есть следующие лица:
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) { }
}
Я сопоставляю эти объекты, используя следующее отображение Hibernate Conformist:
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>();
}));
}
);
}
}
EnumCustomType
является IUserType
который отображает перечисления C# в целочисленные столбцы.
Я сформулировал этот дизайн и карту после долгих исследований и консультаций со справочной документацией NHibernate и этим блогом ( конкретная страница) ( резюме). Я уверен, что это дизайн сущностей, который я хочу, но, конечно, возможно (вероятно?), Я неправильно понял отображение.
Когда я запускаю и настраиваю NHibernate, он загружает сопоставления и не жалуется. Вывод журнала не содержит предупреждений о сопоставлениях. Тем не менее, когда я создаю PersonUser
присваивать значения всем его свойствам и Add
это к ISession
происходит самое необычное:
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)
Важно отметить, что SQL генерируется:
INSERT INTO Authentication_Users (TenantSK, DisplayName, PublicKey, LoginEnabled, LockedOut, NumberOfFailedLoginAttempts, Username, AuthenticationType, UserType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '1')
Это бессмысленно. Это никоим образом не соответствует картированию. Он содержит столбцы из трех разных таблиц в операторе вставки для одной таблицы. Имеет столбцы из PersonUser
И из ClientUser
(что не должно быть возможно), это связывание параметров с несуществующими индексами параметров, и это даже не включает все свойства, которые я установил!
Я играл с этим в течение нескольких часов без какого-либо прогресса. Я в полной растерянности здесь. Это просто не имеет никакого смысла. Кто-нибудь видел это раньше? Есть идеи, что происходит?
РЕДАКТИРОВАТЬ Я забыл упомянуть: я использую дискриминатор здесь, потому что я хочу иметь возможность получить общий User
по его идентификатору, и он возвращает правильное PersonUser
или же ClientUser
в зависимости от того, какой это тип.
2 ответа
Есть некоторые проблемы с отображением
a) Вы отображаете UserType как свойство, но также используете его как дискриминатор. Но значение пользовательского типа генерируется nhibernate из-за использования дискриминатора... Поэтому вы можете определить свойство, которое будет сгенерировано.
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>>();
});
Вы также не должны бросать исключение на сеттер... чтобы предотвратить UserType
будучи установленным откуда-то, просто отметьте автоматический установщик свойств как защищенный
public virtual UserType UserType
{
get;
protected internal set;
}
б) Вы картируете Name
атрибут вашего базового класса только в ваших подклассах, и вы пытаетесь сопоставить это свойство с другими столбцами в таблицах ваших подклассов. Не знаю, возможно ли это вообще, обычно вам нужно сопоставить все свойства вашего базового класса с таблицей базового класса или переместить свойство в подкласс...
Чтобы это исправить, просто удалите сопоставления Name
из ваших отображений подкласса и переместите его в ваше отображение базового класса.
С помощью MichaC этот код наконец заработал.
Объекты:
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."); }
}
}
Отображения:
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>();
}));
}
);
}
}