Fluent Nhibernate Multi-Level reference - c#

I have a three generation class structure that I am attempting to ORM-ify using Fluent Nhibernate. To summarize the classes they look as such...
public class Highest{
public virtual Guid Id{
get;
set;
}
public virtual IList<Medium> Children{
get;
set;
}
}
public class Medium{
public virtual int Index{
get;
set;
}
public virtual Highest Parent{
get;
set;
}
public virtual IList<Lowest> Children{
get;
set;
}
}
public class Lowest{
public virtual MyEnum LowType{
get;
set;
}
public virtual Medium Parent{
get;
set;
}
}
I have attempted a number of tries to create the appropriate Maps using a simple Id for the highest level and composite Id for both the Medium and lowest levels. But all my strategies have failed for creating a compositeid for the lowest to the Medium since (I presume) it in turn uses a CompositeId from itself to Highest.
The Question!
How should the maps be designed such that I am certain that when I load a Highest class all of Medium and Lower relationships will also be created, including the Parent/Children relationships.
Thanks!

the mapping you requested
public class HighestMap : ClassMap<Highest>
{
public HighestMap()
{
Id(x => x.Id).GeneratedBy.GuidComb();
HasMany(x => x.Children)
.KeyColumn("parent_id");
}
}
public class MediumMap : ClassMap<Medium>
{
public MediumMap()
{
CompositeId()
.KeyReference(x => x.Parent, "parent_id")
.KeyProperty(x => x.Index, "indexColumn");
HasMany(x => x.Children)
.KeyColumns.Add("medium_id", "indexColumn")
.Component(c =>
{
c.ParentReference(x => x.Parent);
c.Map(x => x.LowType);
});
}
}
However you said you want to load all in one go then it might be worth sacrificing queryability for performance. The following code will save the Medium.Children collection as a string in the database and is only queryable with equal and unequal session.Query<Medium>().Where(x => x.Children == myCollection);
public class Medium
{
public virtual Highest Parent { get; set; }
public virtual ICollection<MyEnum> Children { get; set; }
}
public class HighestMap : ClassMap<Highest>
{
public HighestMap()
{
Id(x => x.Id).GeneratedBy.GuidComb();
HasMany(x => x.Children)
.KeyColumn("parent_id")
.AsList(i => i.Column("indexColumn"))
.Component(c =>
{
c.ParentReference(x => x.Parent);
// remove Index property and use Highest.Children.IndexOf(medium)
c.Map(x => x.Children).CustomType<EnumCollectionAsStringUserType<MyEnum>>();
});
}
}
[Serializable]
public class EnumCollectionAsStringUserType<TEnum> : IUserType
{
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var value = (string)NHibernateUtil.String.Get(rs, names[0]);
if (string.IsNullOrEmpty(value))
return new List<TEnum>();
return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(Parse).ToList();
}
private static TEnum Parse(string arg)
{
return (TEnum)Enum.Parse(typeof(TEnum), arg);
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var collection = (IEnumerable<TEnum>)value;
NHibernateUtil.String.Set(cmd, string.Join(",", collection), index);
}
public Type ReturnedType { get { return typeof(ICollection<TEnum>); } }
public SqlType[] SqlTypes { get { return new[] { SqlTypeFactory.GetString(255) }; } }
public object DeepCopy(object value)
{
return new List<TEnum>((IEnumerable<TEnum>)value);
}
bool IUserType.Equals(object x, object y)
{
return ((IEnumerable<TEnum>)x).SequenceEqual((IEnumerable<TEnum>)y);
}
int IUserType.GetHashCode(object x)
{
return ((IEnumerable<TEnum>)x).Aggregate(0, (a, v) => a << 8 + v.GetHashCode());
}
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public bool IsMutable { get { return true; } }
public object Replace(object original, object target, object owner)
{
return original;
}
}

Related

How to map an enum as a foreign key in (Fluent) NHibernate?

How do I map an enum property as an integer foreign key in (Fluent) NHibernate?
The enum property serves as a type field which is an integer in the database, but it also needs to be a foreign key to a table which contains all the allowable values of that enum (reporting purposes).
Specifically, I need to have a Communication entity with a Type field of type enum CommunicationType mapped as an integer foreign key to the CommunicationType table. Code below.
NOTE: Answers for NHibernate (without Fluent) are accepted also.
public enum CommunicationType {
General = 1,
ServicesContactEnquiry = 2,
ChangePassword = 3,
OrderDetails = 4,
}
// This is basically the Enum represented in the database for reporting purposes
public class CommunicationTypeRecord {
public virtual int ID { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
}
public class Communication {
public virtual CommunicationType Type { get; set; }
// rest of fields ommitted
}
public class CommunicationMap : ClassMap<Communication> {
public CommunicationMap() {
Map(x => x.Type).CustomType<CommunicationType>.Not.Nullable(); // Needs to be FK -> [CommunicationType].[ID]
// rest of fields ommitted
}
}
public class CommunicationTypeRecordMap : ClassMap<CommunicationTypeRecord> {
public CommunicationTypeRecordMap () {
Table("CommunicationType");
Id(x => x.ID).Column("ID").GeneratedBy.Assigned();
Map(x => x.Name).Column("Name").Not.Nullable().CustomType("AnsiString");
Map(x => x.Description).Column("Description").Nullable();
}
}
Attempt 1
I tried the following which works for generating the DB correctly, but it doesn't work with hydrating/dehydrating data.
References<CommunicationTypeRecord>(x => x.CommunicationType).ForeignKey().Not.Nullable();
Exception:
[ArgumentException: There is a problem with your mappings. You are probably trying to map a System.ValueType to a <class> which NHibernate does not allow or you are incorrectly using the IDictionary that is mapped to a <set>.
A ValueType (CommunicationType) can not be used with IdentityKey. The thread at google has a good description about what happens with boxing and unboxing ValueTypes and why they can not be used as an IdentityKey: http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&oe=UTF-8&threadm=bds2rm%24ruc%241%40charly.heeg.de&rnum=1&prev=/groups%3Fhl%3Den%26lr%3D%26ie%3DUTF-8%26oe%3DUTF-8%26q%3DSystem.Runtime.CompilerServices.RuntimeHelpers.GetHashCode%26sa%3DN%26tab%3Dwg (Parameter 'key')]
Data:
StackTrace:
at NHibernate.Util.IdentityMap.VerifyValidKey(Object obj)
at NHibernate.Util.IdentityMap.set_Item(Object key, Object value)
at NHibernate.Engine.StatefulPersistenceContext.AddChildParent(Object child, Object parent)
at NHibernate.Engine.Cascade.CascadeToOne(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeAssociation(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeProperty(Object parent, Object child, IType type, CascadeStyle style, String propertyName, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeOn(IEntityPersister persister, Object parent, Object anything)
at NHibernate.Event.Default.AbstractSaveEventListener.CascadeBeforeSave(IEventSource source, IEntityPersister persister, Object entity, Object anything)
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.PerformSave(Object entity, Object id, 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.PerformSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.FireSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.SaveOrUpdate(Object obj)
this should work. can't test it right now.
public class CommunicationTypeRecord {
public virtual CommunicationType ID { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
}
public class Communication {
public virtual CommunicationTypeRecord Type { get; set; }
// rest of fields ommitted
}
public class CommunicationMap : ClassMap<Communication> {
public CommunicationMap() {
CompositeId().KeyReference(x => x.Type);
// rest of fields ommitted
}
}
public class CommunicationTypeRecordMap : ClassMap<CommunicationTypeRecord> {
public CommunicationTypeRecordMap () {
Table("CommunicationType");
Id(x => x.ID).Column("ID").CustomType<CommunicationType>().GeneratedBy.Assigned();
Map(x => x.Name).Column("Name").Not.Nullable().CustomType("AnsiString");
Map(x => x.Description).Column("Description").Nullable();
}
}
What i usually do is to not have the type as a record in the database at all. We can still create the table and entries but not use them in code at all.
This is more performant (no joins) easier to maintain translations (no migrations needed for changes or additions of translations) and it is trivial to fill a combobox with all communicationtypes or use CommunicationType as a strategy object or add more metadata like Icon and so on.
public class CommunicationType
{
public static readonly CommunicationType General = new CommunicationType(1, nameof(General));
public static readonly CommunicationType ServicesContactEnquiry = new CommunicationType(2, nameof(ServicesContactEnquiry));
public static readonly CommunicationType ChangePassword = new CommunicationType(3, nameof(ChangePassword));
public static readonly CommunicationType OrderDetails = new CommunicationType(4, nameof(OrderDetails));
private static IReadOnlyList<CommunicationType> _all;
public static IReadOnlyList<CommunicationType> All { get { return _all ?? (_all = typeof(CommunicationType).GetFields(BindingFlags.Static | BindingFlags.Public).Select(f => f.GetValue(null)).Cast<CommunicationType>().ToArray()); } }
private CommunicationType(int iD, string name)
{
ID = iD;
NameKey = "CommunicationType." + name;
DescriptionKey = NameKey + ".Description";
Icon = NameKey + ".Icon";
}
public int ID { get; }
/// <summary>resrouceKey to translate in ui</summary>
public string NameKey { get; }
/// <summary>resrouceKey to translate in ui</summary>
public string DescriptionKey { get; }
public string Icon { get; }
public override int GetHashCode()
{
return ID.GetHashCode();
}
public override bool Equals(object obj)
{
var other = obj as CommunicationType;
return other != null && ID == other.ID;
}
public static bool operator ==(CommunicationType x, CommunicationType y) => Equals(x, y);
public static bool operator !=(CommunicationType x, CommunicationType y) => !Equals(x, y);
}
public class Communication
{
public virtual CommunicationType Type { get; set; }
// rest of fields ommitted
}
public class CommunicationMap : ClassMap<Communication>
{
public CommunicationMap()
{
Id(x => x.Type).CustomType<CommunicationType>();
// rest of fields ommitted
}
}
public class CommunicationTypeUserType : ImmutableUserType // see https://stackoverflow.com/a/7484139/671619 for example implementation
{
public override Type ReturnedType => typeof(CommunicationType);
public override SqlType[] SqlTypes => new[] { NHibernateUtil.Int32.SqlType };
public override object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner)
{
var id = (int)rs[names[0]];
return CommunicationType.All.First(c => c.ID == id);
}
public override void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session)
{
cmd.Parameters[index].Value = ((CopmmunicationType)value).ID;
}
}
class ConnectionTypeDbObject : SimpleAuxiliaryDatabaseObject
{
public static string Create
{
get {
var builder = new StringBuilder("Create Table CommunicationType(...);", 100).AppendLine();
foreach (var insert in CommunicationType.All.Select(t => $"INSERT INTO CommunicationType (...) Values ({t.ID}, ...) "))
builder.AppendLine(insert);
builder.AppendLine("Create ForeignKey ...");
return builder.ToString();
}
}
public const string Drop = "Drop Table CommunicationType";
public ConnectionTypeDbObject() : base(Create, Drop) { }
}

Applying entity configurations in a foreach loop

I was wondering if someone might have an idea how to apply entity configuration dynamically. I don't want to repeat let's say builder.ApplyConfiguration(new PersonConfiguration()); for each entity I have/might have in the future.
I was trying with the following code and for the sake of simplicity I have placed everything into a single file:
namespace WebApplication1.Data
{
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
var entityTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(p => p.GetTypes())
.Where(p => typeof(IEntity).IsAssignableFrom(p) && p != typeof(IEntity));
foreach (var entityType in entityTypes)
{
// this doesn't work
//builder.ApplyConfiguration(new BaseEntityConfiguration<entityType>());
}
// this works, but I don't want to basically repeat it for each entity individually
//builder.ApplyConfiguration(new BaseEntityConfiguration<Person>());
//builder.ApplyConfiguration(new PersonConfiguration());
}
public virtual DbSet<Person> People { get; set; }
}
public interface IEntity
{
int Id { get; set; }
string RowModifyUser { get; set; }
DateTime RowModifyDate { get; set; }
byte[] RowVersion { get; set; }
}
public class Person : IEntity
{
public int Id { get; set; }
public string RowModifyUser { get; set; }
public DateTime RowModifyDate { get; set; }
public byte[] RowVersion { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class BaseEntityConfiguration<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : class, IEntity
{
public void Configure(EntityTypeBuilder<TEntity> builder)
{
builder.HasKey(m => m.Id);
builder.Property(m => m.RowModifyDate).IsRequired();
builder.Property(m => m.RowModifyUser).IsRequired();
builder.Property(m => m.RowVersion).IsRequired().IsConcurrencyToken();
}
}
public class PersonConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
builder.Property(m => m.FirstName).IsRequired();
builder.Property(m => m.LastName).IsRequired();
}
}
}
In addition to creating a generic BaseEntityConfiguration<TEntity> class as suggested in another answer, you also need to call a generic ModelBuilder.ApplyConfiguration<TEntity>(IEntityTypeConfiguration<TEntity> configuration) method via reflection.
Something like this (needs using System.Reflection;):
// Can be moved to a static readonly field of the class
var applyConfigurationMethodDefinition = typeof(ModelBuilder)
.GetTypeInfo()
.DeclaredMethods
.Single(m => m.Name == "ApplyConfiguration" &&
m.IsGenericMethodDefinition &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType.IsGenericType &&
m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>));
foreach (var entityType in entityTypes)
{
var configurationType = typeof(BaseEntityConfiguration<>).MakeGenericType(entityType);
var configuration = Activator.CreateIntance(configurationType);
var applyConfigurationMethod = applyConfigurationMethodDefinition.MakeGenericMethod(entityType);
applyConfigurationMethod.Invoke(builder, new object[] { configuration });
}
Note that in EF Core 2.1 ModelBuilder class has 2 ApplyConfiguration method overloads which differ only by the type of the parameter, that's why finding the method includes all the checks.
Untested, but you could try the following:
foreach (Type entityType in entityTypes)
{
Type openConfigType = typeof(BaseEntityConfiguration<>);
Type genericConfigType = openConfigType.MakeGenericType(entityType);
builder.ApplyConfiguration(Activator.CreateInstance(genericConfigType));
}

How to exclude all virtual properties from classes inheritted from ClassMapper<T> by default?

I have a lot of POCO classes that contain several virtual properties each. Something like this:
public class Policy
{
public int Id { get; set; }
public int EntityId { get; set; }
public int ProgramId { get; set; }
public string PolicyNumber { get; set; }
public DateTime EffectiveDate { get; set; }
public DateTime ExpirationDate { get; set; }
public virtual Entity Entity{ get; set; }
public virtual Program Program { get; set; }
public virtual ICollection<Transaction> Transactions { get; set; }
}
To make Dapper.Extensions work, I need to write a mapping for each of these classes, which is fine. My problem is, if there are any virtual properties inside a class, they need to be explicitly marked as ignored, which I always forget to do.
public sealed class PolicyMapper : BaseMapper<Policy>
{
public PolicyMapper()
{
Map(p => p.Entity).Ignore();
Map(p => p.Program).Ignore();
Map(p => p.Transactions).Ignore();
AutoMap();
}
}
What would be great for me, if the Dapper.Extensions library will automatically exclude virtual properties, if any, when mapped to the POCO class. There is an extension for Automapper that does something similar (link). Is there a way to do that for Dapper.Extensions library? Possibly something like this:
public sealed class PolicyMapper : BaseMapper<Policy>
{
public PolicyMapper()
{
IgnoreAllVirtual();
AutoMap();
}
}
I found my own solution. Since all my mapping classes derive from BaseMapper class, I decided to override AutoMap() method that will exclude virtual properties:
public class BaseMapper<T> : ClassMapper<T> where T : BaseClass
{
public BaseMapper()
{
}
protected override void AutoMap()
{
CustomAutoMap(null);
}
private void CustomAutoMap(Func<Type, PropertyInfo, bool> canMap)
{
Type type = typeof(T);
bool hasDefinedKey = Properties.Any(p => p.KeyType != KeyType.NotAKey);
PropertyMap keyMap = null;
foreach (var propertyInfo in type.GetProperties())
{
// Exclude virtual properties
if (propertyInfo.GetGetMethod().IsVirtual)
{
continue;
}
if (Properties.Any(p => p.Name.Equals(propertyInfo.Name, StringComparison.InvariantCultureIgnoreCase)))
{
continue;
}
if ((canMap != null && !canMap(type, propertyInfo)))
{
continue;
}
PropertyMap map = Map(propertyInfo);
if (!hasDefinedKey)
{
if (string.Equals(map.PropertyInfo.Name, "id", StringComparison.InvariantCultureIgnoreCase))
{
keyMap = map;
}
if (keyMap == null && map.PropertyInfo.Name.EndsWith("id", true, CultureInfo.InvariantCulture))
{
keyMap = map;
}
}
}
if (keyMap != null)
{
keyMap.Key(PropertyTypeKeyTypeMapping.ContainsKey(keyMap.PropertyInfo.PropertyType)
? PropertyTypeKeyTypeMapping[keyMap.PropertyInfo.PropertyType]
: KeyType.Assigned);
}
}
}
}

Invalid index N for this SqlParameterCollection with Count=N" server error

I've read many posts here about exception mentioned in the title. Typically this exception means that somewhere I mapped to fields to one entity. I spent pretty much time looking at my mappings, but still can't find what's wrong. Could you please help me with understanding what've been done wrong here:
public class CustomerSegment : ModelEntity, IEntityDescriptor
{
public const string Standart = "Standard";
public virtual string Name { get; set; }
public virtual NetworkNHMapped Network { get; set; }
public virtual string GetDescriptor()
{
return Name;
}
}
public class CustomerSegmentMap : ClassMap<CustomerSegment>
{
public CustomerSegmentMap()
{
Table("NetworkProperty");
Id(x => x.Id).Column("NetworkPropertyId");
Map(x => x.Name).Column("PropertyName");
References(x => x.Network).Column("NetworkId");
}
}
}
The exception occurs when I'm trying get all CustomerSegment entities from DB.
The code of other entities:
public class NetworkNHMapped : ModelEntity
{
[StringLength(50)]
public virtual string Name { get; set; }
public virtual int NetworkOwnerId { get; set; }
public virtual int NetworkTypeId { get; set; }
public virtual RepairShop.NetworkType NetworkType { get { return (RepairShop.NetworkType)NetworkTypeId; } }
}
public class NetworkNewMap : ClassMap<NetworkNHMapped>
{
public NetworkNewMap()
{
Table("Network");
Id(x => x.Id, "NetworkId");
Map(x => x.Name, "NetworkName");
Map(x => x.NetworkOwnerId, "NetworkOwnerId");
Map(x => x.NetworkTypeId, "NetworkType");
}
}
And base ModelEntity:
public virtual int Id { get; set; }
public override int GetHashCode()
{
if (!IsPersisted())
{
return base.GetHashCode();
}
unchecked
{
int result = GetObjectRealType(this).GetHashCode();
result = 42 * result + Id.GetHashCode();
return result;
}
}
public override bool Equals(object other)
{
if (ReferenceEquals(this, other))
{
return true;
}
if ((other == null) || !(other is ModelEntity))
{
return false;
}
var thisType = GetObjectRealType(this);
var otherType = GetObjectRealType(other);
if (thisType != otherType)
return false;
if (Id.Equals(default(long)) && (other as ModelEntity).Id.Equals(default(long)))
{
return base.Equals(other);
}
return Id == (other as ModelEntity).Id;
}
public static bool operator ==(ModelEntity entity1, ModelEntity entity2)
{
var obj1 = (object)entity1;
if (obj1 == null && ((object)entity2) == null)
return true;
return obj1 != null && entity1.Equals(entity2);
}
public static bool operator !=(ModelEntity entity1, ModelEntity entity2)
{
return !(entity1 == entity2);
}
public virtual bool IsPersisted()
{
return Id > 0;
}
protected static Type GetObjectRealType(object obj)
{
return (obj is INHibernateProxy) ? NHibernateUtil.GetClass(obj) : obj.GetType();
}
}
The first thing I would do is to look at the XML files that are being generated, this shows if you have duplicate mappings to the same property.
If you are using NH and ModelMapper then simply make a call to WriteAllXmlMapping e.g.:-
var mapper = new ModelMapper();
mapper.AddMappings(typeof(CmsMeta).Assembly.GetTypes());
//This will write all the XML into the bin/mappings folder
mapper.CompileMappingForEachExplicitlyAddedEntity().WriteAllXmlMapping();
If you are using Fluent NHibernate then look at this blog post to export your XML.
After you have generated the XML I bet you will find duplicate mappings! Otherwise see this blog post, it is possible if you are exposing a foreign-key as well as using a many-to-one ... to the related entity in the mapping file. If this is the case add insert="false" and update="false" to the foreign key property and run again.
Either way generate the XML and take a look, if you can't see it post the relevant XML files to your question and we will take a look.

Nhibernate duplicating column

I want to mapping following simple entities:
public class AnimalsOwner
{
public AnimalsOwner()
{
Cats = new List<Cat>();
Dogs = new List<Dog>();
}
public virtual int Id { get; set; }
public virtual IList<Cat> Cats { get; set; }
public virtual IList<Dog> Dogs { get; set; }
}
public abstract class Animal
{
public virtual int Id { get; set; }
public virtual AnimalsOwner AnimalsOwner { get; set; }
}
public class Dog : Animal
{
public virtual string DogsProperty { get; set; }
}
public class Cat : Animal
{
public virtual string CatsProperty { get; set; }
}
With the following mappings:
public OwnerMapping()
{
this.Table("Owners");
Id(x => x.Id)
.GeneratedBy.Native();
HasMany(x => x.Cats).AsBag().Cascade.SaveUpdate();
HasMany(x => x.Dogs).AsBag().Cascade.SaveUpdate();
}
}
public class AnimalMapping : ClassMap<Animal>
{
public AnimalMapping()
{
this.Table("Animals");
Id(x => x.Id).GeneratedBy.Native();
References(x => x.AnimalsOwner).Not.Nullable();
}
}
public class DogMap : SubclassMap<Dog>
{
public DogMap()
{
this.Table("Dogs");
this.KeyColumn("AnimalId");
Map(x => x.DogsProperty);
}
}
public class CatMap : SubclassMap<Cat>
{
public CatMap()
{
this.Table("Cats");
this.KeyColumn("AnimalId");
Map(x => x.CatsProperty);
}
}
With such mappings everything works fine, but generated schema looks like following:
And such code:
var owner = new AnimalsOwner();
owner.Cats.Add(new Cat()
{
AnimalsOwner = owner,
CatsProperty = "cat"
});
Makes inserts in all three tables (Animals, Cats, Dogs) of column OwnderId. But isn't it enough to have it only in Animals table? If yes then how to achieve it? If NHibernate acts as expected then why is he doing such a duplication?
I would map this using table-per-class strategy and filter the public collections by type. This database structure is much easier to work with and more efficient to query.
public class AnimalMapping : ClassMap<Animal>
{
public AnimalMapping()
{
Table("Animals");
Id(x => x.Id).GeneratedBy.Native();
References(x => x.AnimalsOwner);
DiscriminateSubClassesOnColumn("AnimalType");
}
}
public class DogMap : SubclassMap<Dog>
{
public DogMap()
{
DiscriminatorValue("DOG");
Map(x => x.DogsProperty);
}
}
public class CatMap : SubclassMap<Cat>
{
public CatMap()
{
DiscriminatorValue("CAT");
Map(x => x.CatsProperty);
}
}
The Dogs and Cats collection can be exposed by mapping the Animals collection as a private member and filtering by type:
public class AnimalsOwner
{
private IList<Animal> _animals;
public AnimalsOwner()
{
_animals = new List<Animal>();
}
public virtual int Id { get; set; }
public virtual IEnumerable<Animal> Animals { get { return _animals; } }
public virtual IEnumerable<Cat> Cats { get { return _animals.OfType<Cat>(); } }
public virtual IEnumerable<Dog> Dogs { get { return _animals.OfType<Dog>(); } }
public virtual void AddAnimal(Animal animal)
{
if (!_animals.Contains(animal))
{
animal.AnimalsOwner = this;
_animals.Add(animal);
}
}
public virtual void RemoveAnimal(Animal animal)
{
if (_animals.Contains(animal))
{
animal.AnimalsOwner = null;
_animals.Remove(animal);
|
}
}
The mapping for this class would be:
public OwnerMapping()
{
this.Table("Owners");
Id(x => x.Id).GeneratedBy.Native();
HasMany(x => x.Animals.AsBag.Cascade.AllDeleteOrphan()
.Inverse().LazyLoad()
.Access.CamelCaseField(Prefix.Underscore);
}
}
And the code for adding a cat would be:
var owner = new AnimalsOwner();
owner.AddAnimal(new Cat()
{
CatsProperty = "cat"
});
You're getting the duplicates becuse you have HasMany Cats and Dogs defined in the owner mapping (and Cat & Dog collections in the Owner class). Perhaps what you really want is a collection of Animals in the owners class (not individually dogs and cats) and just one HasMany Animals in the Owner mapping?

Categories

Resources