Fluent NHibernate automapping table-per-abstract-hierarchy / table-per-concrete-subclass - fluent-nhibernate

I have classes
public abstract class Content : IContent
{
public virtual Guid Id { get; protected set; }
public virtual IPage Parent { get; set; }
public virtual DateTime Created { get; set; }
/* ... */
}
public abstract class Page : Content, IPage
{
public virtual string Slug { get; set; }
public virtual string Path { get; set; }
public virtual string Title { get; set; }
/* ... */
}
public class Foo : Page, ITaggable
{
// this is unique property
// map to joined table
public virtual string Bar { get; set; }
// this is a unique collection
public virtual ISet<Page> Related { get; set; }
// this is "shared" property (from ITaggable)
// map to shared table
public virtual ISet<Tag> Tags { get; set; }
}
And as a result I'd like to have the following tables. I've tried implementing tons of different IConventions, but even the hierarchy mappings (table-per-abstract-hierarchy / table-per-concrete-subclass) seem to fail.
Content
Id
Type (discriminator)
ParentId
Created
Slug
Path
Title
Content_Tags (Tags from ITaggable)
ContentId
TagId
Content$Foo
Bar
Content$Foo_Related
ParentFooId
ChildPageId
I already have ugly, working fluent mappings, but I would like to get rid of some ugliness
public class ContentMapping : ClassMap<Content>
{
public ContentMapping()
{
Table("Content");
Id(x => x.Id).GeneratedBy.GuidComb();
References<Page>(x => x.Parent, "ParentId");
Map(x => x.Created);
DiscriminateSubClassesOnColumn("Type");
}
}
public class PageMapping : SubclassMap<Page>
{
public PageMapping()
{
Map(x => x.Slug);
Map(x => x.Path);
Map(x => x.Title);
}
}
public class ConcreteContentMapping<T> : SubclassMap<T> where T : Content, new()
{
public ConcreteContentMapping() : this(true) { }
protected ConcreteContentMapping(bool mapJoinTable)
{
DiscriminatorValue(typeof(T).FullName);
MapCommonProperties();
if(mapJoinTable)
{
MapJoinTableWithProperties(CreateDefaultJoinTableName(), GetPropertiesNotFrom(GetContentTypesAndInterfaces().ToArray()));
}
}
private void MapCommonProperties()
{
if (typeof(ITagContext).IsAssignableFrom(typeof(T)))
{
Map(x => ((ITagContext)x).TagDirectory);
}
if (typeof(ITaggable).IsAssignableFrom(typeof(T)))
{
HasManyToMany(x => ((ITaggable)x).Tags).Table("Content_Tags").ParentKeyColumn("ContentId").ChildKeyColumn("TagId").Cascade.SaveUpdate();
}
}
/* ... */
// something I would like to get rid of with automappings...
protected void MapCollectionProperty(JoinPart<T> table, PropertyInfo p)
{
var tableName = ((IJoinMappingProvider)table).GetJoinMapping().TableName + "_" + p.Name;
var elementType = p.PropertyType.GetGenericArguments()[0];
var method = table.GetType().GetMethods().Where(m => m.Name == "HasManyToMany")
.Select(m => new { M = m, P = m.GetParameters() })
.Where(x => x.P[0].ParameterType.GetGenericArguments()[0].GetGenericArguments()[1] == typeof(object))
.FirstOrDefault().M.MakeGenericMethod(elementType);
dynamic m2m = method.Invoke(table, new object[] { MakePropertyAccessExpression(p)});
m2m.Table(tableName).ParentKeyColumn("Parent" + typeof(T).Name + "Id").ChildKeyColumn("Child" + elementType.Name + "Id");
}
protected Expression<Func<T, object>> MakePropertyAccessExpression(PropertyInfo property)
{
var param = Expression.Parameter(property.DeclaringType, "x");
var ma = Expression.MakeMemberAccess(param, property);
return Expression.Lambda<Func<T, object>>(ma, param);
}
}
How do I get the same result with automappings?

Related

AutoMapper error: class maps to a datatype (byte[])

I have two classes, each having a domain and a repo version.
DOMAIN:
public class MusicInfo
{
public string Id { get; set; }
public MusicImage Image { get; set; }
public MusicInfo(byte[] image)
{
this.Image = new MusicImage(this, image);
}
}
public class MusicImage
{
public byte[] Blob { get; set; }
public MusicInfo MusicInfo { get; set; }
public string Id { get; set; }
public MusicImage(MusicInfo musicInfo, byte[] blob)
{
if (musicInfo == null)
throw new ArgumentNullException("musicInfo");
if (blob == null)
throw new ArgumentNullException("blob");
this.MusicInfo = musiscInfo;
this.Blob = blob;
}
}
REPO:
public class MusicInfoRepo
{
public virtual long Id { get; set; }
public virtual MusicImageRepo Image { get; set; }
}
public class MusicImageRepo
{
public virtual byte[] Blob { get; set; }
public virtual MusicInfoRepo MusicInfo { get; set; }
public virtual long Id { get; set; }
}
And here are their mappings:
public class MusicInfoRepoMap : HighLowClassMapping<MusicInfoRepo>
{
public MusicInfoRepoMap()
{
Table("MusicInfo");
Id(f => f.Id, m => m.Generator(Generators.HighLow, HighLowMapper));
OneToOne(f => f.Image, m => m.Cascade(Cascade.All));
}
}
public class MusicImageRepoMap : ClassMapping<MusicImageRepo>
{
public MusicImageRepoMap()
{
Table("MusicImage");
Id(f => f.Id, m => m.Generator(Generators.Foreign<MusicImageRepo>(f => f.MusicInfo)));
Property(f => f.Blob, m =>
{
m.NotNullable(true);
m.Column(c => c.SqlType("VARBINARY(MAX)"));
m.Length(Int32.MaxValue);
m.Update(false);
});
OneToOne(f => f.MusicInfo,
m =>
{
m.Cascade(Cascade.None);
m.Constrained(true);
m.Lazy(LazyRelation.NoLazy);
});
}
}
When I am trying to query for a ClassA that has a one to one relationship with ClassB which also has a one to one relationship with MusicInfo, an error occurs that says:
Missing type map configuration or unsupported mapping.
Mapping types:
MusicImageRepo -> Byte[]
Blah.MusicImageRepo -> System.Byte[]
Destination path:
List`1[0]
Source value:
Blah.MusicImageRepo
but here is how i map them:
Mapper.CreateMap<MusicInfo, MusicInfoRepo>();
Mapper.CreateMap<MusicInfoRepo, MusicInfo>();
Mapper.CreateMap<MusicImage, MusicImageRepo>();
Mapper.CreateMap<MusicImageRepo, MusicImage>();
There are no problems when saving these classes.
I really dont get why the error happens.
Will really appreciate your help.
OneToOne says that the other entity/table has the Reference(column). It should not work when both sides have a onetoone mapping since they bounce the responsiblity back and forth.
Also the classes look a bit overcomplicated when all you need is to store the bytes somewhere else.
public class MusicInfoRepo
{
public virtual long Id { get; private set; }
public virtual MusicImageRepo Image { get; private set; }
public MusicInfo(byte[] image)
{
this.Image = new MusicImageRepo(this, image);
}
}
public class MusicImageRepo
{
public virtual MusicInfoRepo MusicInfo { get; private set; }
public virtual byte[] Blob { get; set; }
}
public class MusicInfoRepoMap : HighLowClassMapping<MusicInfoRepo>
{
public MusicInfoRepoMap()
{
Table("MusicInfo");
Id(f => f.Id, m => m.Generator(Generators.HighLow, HighLowMapper));
OneToOne(f => f.Image, m => m.Cascade(Cascade.All));
}
}
public class MusicImageRepoMap : ClassMapping<MusicImageRepo>
{
public MusicImageRepoMap()
{
Table("MusicImage");
ComposedId(m => m.ManyToOne(x => x.MusicInfo));
Property(f => f.Blob, m =>
{
m.NotNullable(true);
m.Column(c => c.SqlType("VARBINARY(MAX)"));
m.Length(Int32.MaxValue);
m.Update(false);
});
}
}
Note: think about it if the seperation between DomainModel and MappedModel really makes sense. NHibernate goes to great length supporting mapping of DomainModels.

Fluent NHibernate - HasOne mapped to a ReferencesAny

I have the following POCO classes:
public class Container
{
public virtual Int64 ContainerId { get; protected set; }
public virtual string Name { get; set; }
public virtual Location Location { get; set; }
}
public abstract class Location
{
public virtual Int64 LocationId { get; protected set; }
public virtual string Name { get; set; }
}
public class UniqueLocation : Location
{
public virtual Container Container { get; set; }
}
public class SharedLocation : Location
{
public SharedLocation()
{
this.Containers = new List<Container>();
}
public virtual IList<Container> Containers { get; set; }
}
and the following Fluent mapping:
public class ContainerMap: ClassMap<Container>
{
public ContainerMap()
{
Table("Containers");
Id(x => x.ContainerId);
Map(x => x.Name);
ReferencesAny(x => x.Location).IdentityType<Int64>().EntityTypeColumn("LocationType").EntityIdentifierColumn("LocationId")
.AddMetaValue<UniqueLocation>("U")
.AddMetaValue<SharedLocation>("S");
}
}
public class LocationMap : ClassMap<Location>
{
public LocationMap()
{
Table("Locations");
Id(x => x.LocationId);
Map(x => x.Name);
}
}
public class UniqueLocationMap : SubclassMap<UniqueLocation>
{
public UniqueLocationMap()
{
HasOne(x => x.Container).PropertyRef(x => x.Location).ForeignKey("LocationId").Cascade.All().Constrained();
}
}
public class SharedLocationMap : SubclassMap<SharedLocation>
{
public SharedLocationMap()
{
HasMany(x => x.Containers).KeyColumn("LocationId");
}
}
The problem is HasOne() mapping generates the following exception: "broken column mapping for: Container.Location of: UniqueLocation, type Object expects 2 columns, but 1 were mapped".
How do I tell HasOne() to use/map both LocationType and LocationId?
AFAIK Where conditions are not possible on Entity references except using Formulas. The design seems a strange because it would be nasty to change a unique Location to a shared location.
what you want can be done using:
Reference(x => x.Container).Formula("(SELECT c.Id FROM Container c WHERE c.LocationId = Id AND c.LocationType = 'U')");
But i would prefere
class Location
{
...
public virtual bool IsUnique { get { return Container.Count == 1; } }
}

How to get records in first table(projects) not present in second table(finances) with foreign key reference using nhibernate

I'm trying to query on a simple data structure in nhibernate and MSSQL
dbo.Projects : Id(int, not null)
dbo.Finances : Id(int, not null), ProjectId(int,not null), foreign key references to dbo.projects
I want to get all the records in projects table that are not present in finances table where the finances table has a foreign key reference ProjectId.
I am migrating to (Fluent) Nhibernate 3 from EntityFramework?
//So far I have got here:
public IQueryable<ProjectModel> GetProjectsNotPresentInFinance()
{
var factory = Fluently.Configure()
.Database(MsSqlConfiguration
.MsSql2008
.ConnectionString(m_connectionString))
.Mappings(m => m.FluentMappings
.AddFromAssemblyOf<ProjectMap>()
).BuildSessionFactory();
using (var session = factory.OpenSession())
{
var allprojects = session.QueryOver<ProjectModel>();
var projectsToReturn = allprojects.List<ProjectModel>().AsQueryable();
//--- Something like : all the records not in finances table ---------
// .Where( proj => !db.Finances.Where(fin => fin.ProjectId == proj.Id).Any())
// .Select(project => new ProjectModel
// {
// Id=project.Id,
// ProjectName = project.ProjectName,
// });
return projectsToReturn;
}
}
public class FinanceModel
{
public virtual int Id { get; set; }
public virtual int ProjectId { get; set; }
}
public class ProjectModel
{
public virtual int Id { get; set; }
public virtual string ProjectName { get; set; }
}
public class ProjectMap:ClassMap<ProjectModel>
{
public ProjectMap() {
Table("Projects");
Id(x => x.Id);
Map(x => x.ProjectName);
}
}
public class FinanceMap : ClassMap<FinanceModel>
{
public FinanceMap()
{
Table("Finances");
Id(x => x.Id);
References(x => x.ProjectModel);
}
}
//-------------------------------------------------------
//This is an Equivalent working code Using EntityFramework :
public IQueryable<ProjectModel> GetProjectsNotPresentInFinance() {
IQueryable<ProjectModel> projectList = db.Projects
.Where( proj => !db.Finances.Where(fin => fin.ProjectId == proj.Id).Any())
.Select(project => new ProjectModel
{
Id=project.Id,
ProjectName = project.ProjectName,
});
return projectList;
}
//-------------------------------------------------------
On second thought, you may try this without changing anything to your mapping, using a subquery :
var notOrphanProjectIdsSubquery = QueryOver.Of<FinanceModel>()
.Select(x => x.ProjectId);
var orphanProjects = session.QueryOver<ProjectModel>()
.WithSubquery
.WhereProperty(x=>x.Id)
.NotIn(notOrphanProjectIdsSubquery)
.List();
----------------------- Initial answer
Assuming you have a mapped Finances Property in your Project class, and according to https://stackoverflow.com/a/14980450/1236044, it should be something like :
var orphanProjects = session.QueryOver<ProjectModel>()
.WhereRestrictionOn(x => x.Finances).IsEmpty()
.List();
I must confess I am not proficient with FluentNH. I guess the classes and mappings should be something like this, hoping I'm not setting you on the wrong track...
public class FinanceModel
{
public virtual int Id { get; set; }
public virtual int ProjectId { get; set; }
public virtual ProjectModel Project{get;set;}
}
public class ProjectModel
{
public virtual int Id { get; set; }
public virtual string ProjectName { get; set; }
public virtual IList<FinanceModel> Finances { get; set; }
}
public class ProjectMap:ClassMap<ProjectModel>
{
public ProjectMap() {
Table("Projects");
Id(x => x.Id);
Map(x => x.ProjectName);
HasMany(x => x.Finances);
}
}
public class FinanceMap : ClassMap<FinanceModel>
{
public FinanceMap()
{
Table("Finances");
Id(x => x.Id);
References(x => x.Project);
}
}

FluentNHibernate subclass and hierarchical data

I have an Account table that stores Master Accounts and Sub Accounts. Master Accounts are basically the same as Sub Accounts except that they can have an associated Company. Account is an abstract class and both MasterAccount and SubAccount derive from it.
A MasterAccount is any account entry with a null ParentAccountId. If an Account record has a ParentAccountId then it is a SubAccount and the ParentAccountId references the AccountId field for the MasterAccount.
I am trying get FluentNhibernate mappings for them.
The classes look like the following
public class Account : EntityBase
{
public Account() { }
public virtual string AccountNumber { get; set; }
public virtual string AccountName { get; set; }
public virtual string ContactRole { get; set; }
public virtual bool EmailBillDataFile { get; set; }
public virtual bool EmailBill { get; set; }
public virtual bool PostBill { get; set; }
public virtual BillingMethod BillingMethod { get; set; }
public virtual BillingAddressType BillingAddressType { get; set; }
public virtual Contact Contact { get; set; }
public virtual bool IsInvoiceRoot { get; set; }
public virtual string Password { get; set; }
public virtual bool HasRequestedInvoicing { get; set; }
public virtual bool IsInternational { get; set; }
public virtual decimal AmountPaid { get; set; }
public virtual decimal PreviousBill { get; set; }
public virtual void MakePayment(decimal amount)
{
MakePayment(amount, null);
}
public virtual void MakePayment(decimal amount, string invoiceNumber)
{
AmountPaid += amount;
if (string.IsNullOrEmpty(invoiceNumber))
LogActivity(string.Format("Made payment of {0:c}", amount));
else {
LogActivity(string.Format("Made payment of {0:c} on Invoice '{1}'", amount, invoiceNumber));
}
}
public virtual Invoice CreateInvoice()
{
Invoice invoice;
invoice = IsInternational ? new NoGstInvoice() : new Invoice();
// Can update invoice properties that rely on account data here.
return invoice;
}
#region Business Rules
public override IEnumerable<RuleViolation> GetRuleViolations()
{
if (string.IsNullOrEmpty(AccountName))
yield return new RuleViolation("Account Name required", "AccountName");
if (string.IsNullOrEmpty(AccountNumber))
yield return new RuleViolation("Acocunt Number required", "AccountNumber");
if (string.IsNullOrEmpty(Password))
yield return new RuleViolation("Password required", "Password");
yield break;
}
#endregion
}
public class MasterAccount : Account
{
private Company _company;
private IList<SubAccount> _subAccounts;
public MasterAccount() : this(null) { }
public MasterAccount(Company company)
{
_company = company;
_subAccounts = new List<SubAccount>();
}
public virtual Company Company
{
get { return _company; }
}
public virtual IEnumerable<SubAccount> SubAccounts
{
get { return _subAccounts; }
}
public virtual SubAccount CreateSubAccount(string accountNumber, string accountName)
{
var subAccount = new SubAccount(this)
{
AccountName = accountName,
AccountNumber = accountNumber,
Contact = this.Contact,
ContactRole = this.ContactRole,
PreviousBill = 0,
AmountPaid = 0,
BillingAddressType = this.BillingAddressType,
BillingMethod = this.BillingMethod,
IsInternational = this.IsInternational,
IsInvoiceRoot = false,
EmailBill = this.EmailBill,
EmailBillDataFile = this.EmailBillDataFile,
Password = this.Password,
PostBill = this.PostBill
};
return subAccount;
}
}
public class SubAccount : Account
{
private MasterAccount _masterAccount;
public SubAccount() { }
public SubAccount(MasterAccount master)
{
_masterAccount = master;
}
public virtual MasterAccount MasterAccount
{
get { return _masterAccount; }
}
}
The mappings I have are:
public class AccountMap : ClassMap<Account>
{
public AccountMap()
{
Table("Account");
Id(x => x.Id).Column("AccountId").GeneratedBy.Identity();
Map(x => x.AccountName).Length(50).Not.Nullable();
Map(x => x.AccountNumber).Length(10).Not.Nullable();
Map(x => x.ContactRole).Length(50);
Map(x => x.BillingMethod).Not.Nullable();
Map(x => x.EmailBill).Not.Nullable();
Map(x => x.PostBill).Not.Nullable();
Map(x => x.EmailBillDataFile).Not.Nullable();
Map(x => x.BillingAddressType).Not.Nullable();
Map(x => x.IsInvoiceRoot).Not.Nullable();
Map(x => x.HasRequestedInvoicing).Not.Nullable();
Map(x => x.IsInternational).Not.Nullable();
Map(x => x.PreviousBill).Not.Nullable();
Map(x => x.AmountPaid).Not.Nullable();
Map(x => x.Password).Length(20).Not.Nullable();
References(x => x.Contact).Column("ContactId").Not.Nullable();
DiscriminateSubClassesOnColumn("ParentAccountId");
}
}
public class MasterAccountMap : SubclassMap<MasterAccount>
{
public MasterAccountMap()
{
References(x => x.Company).Column("CompanyId");
HasMany(x => x.SubAccounts).KeyColumn("ParentAccountId").Inverse().Cascade.All();
}
}
public class SubAccountMap : SubclassMap<SubAccount>
{
public SubAccountMap()
{
References(x => x.MasterAccount).Column("ParentAccountId").Not.Nullable();
}
}
However, when I execute the following test:
[Test]
public void Can_add_subAccount_to_database()
{
var master = Session.Get<MasterAccount>(1);
var subAccount = master.CreateSubAccount("TST123", "Test Account");
Session.Save(subAccount);
Session.Flush();
Session.Clear();
var fromDb = Session.Get<SubAccount>(subAccount.Id);
Assert.AreNotSame(subAccount, fromDb);
}
I get an exception on the Session.Save(subAccount); line.
System.ArgumentOutOfRangeException : Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
I do not get the exception if I comment out the References mapping in SubAccountMap.
Any help on correctly mapping this relationship is appreciated.
Seems I needed to use the Formula method off DiscriminateSubClassesOnColumn
DiscriminateSubClassesOnColumn("").Formula("case when parentaccountid is null then '0' else '1' end");
and then use the following in each of the subclasses
DiscriminatorValue("0"); // In MasterAccountMap
DiscriminatorValue("1"); // in SubAccountMap
see http://wiki.fluentnhibernate.org/Fluent_mapping

NHibernate - Delete Not Peristing in the Database

i'm trying to remove an item from a one to many list and have it persist in the database. Here are the entities i have defined:
public class SpecialOffer
{
public virtual int SpecialOfferID { get; set; }
public virtual string Title { get; set; }
public virtual IList<SpecialOfferType> Types { get; private set; }
public SpecialOffer()
{
Types = new List<SpecialOfferType>();
}
}
public class SpecialOfferType
{
public virtual SpecialOffer SpecialOffer { get; set; }
public virtual Type Type { get; set; }
public virtual int MinDaysRemaining { get; set; }
#region composite id requirements
public override bool Equals(object obj)
{
if (obj == null || !(obj is SpecialOfferType))
return false;
var t = (SpecialOfferType)obj;
return SpecialOffer.SpecialOfferID == t.SpecialOffer.SpecialOfferID && Type.TypeID == t.Type.TypeID;
}
public override int GetHashCode()
{
return (SpecialOffer.SpecialOfferID + "|" + Type.TypeID).GetHashCode();
}
#endregion
}
public class Type
{
public virtual int TypeID { get; set; }
public virtual string Title { get; set; }
public virtual decimal Price { get; set; }
}
With the following fluent mappings:
public class SpecialOfferMap : ClassMap<SpecialOffer>
{
public SpecialOfferMap()
{
Table("SpecialOffers");
Id(x => x.SpecialOfferID);
Map(x => x.Title);
HasMany(x => x.Types)
.KeyColumn("SpecialOfferID")
.Inverse()
.Cascade.All();
}
}
public class SpecialOfferTypeMap : ClassMap<SpecialOfferType>
{
public SpecialOfferTypeMap()
{
Table("SpecialOfferTypes");
CompositeId()
.KeyReference(x => x.SpecialOffer, "SpecialOfferID")
.KeyReference(x => x.Type, "TypeID");
Map(x => x.MinDaysRemaining);
}
}
public class TypeMap : ClassMap<Type>
{
public TypeMap()
{
Table("Types");
Id(x => x.TypeID);
Map(x => x.Title);
Map(x => x.Price);
}
}
The problem i have is that if i remove an item from the SpecialOffer.Types collection it successfully removes it from the list but when i try to save the session the change is not persisted in the database. I'm assuming this is something to do with the composite id on the join table since i have been able to do this successfully in the past with a standard id.
I'd appreciate it if someone could show me what i'm doing wrong. Thanks
I think you have to 1) Change the cascade setting on SpecialOffer.Types to Cascade.AllDeleteOrphan() and 2) set SpecialOfferType.SpecialOffer = null when you remove it from the collection. Since the collection is the inverse side of the relationship, the many-to-one reference to SpecialOffer on SpecialOfferType has to be set to null to make it an orphan, then Cascade.AllDeleteOrphan will cause it to be deleted.