I'm trying to figure out the proper way to model a one-to-one (or one-to-zero) relationship in NHibernate, or indeed to learn categorically whether such a thing can be done.
Currently I have two models, Document and ScriptDocument whereby there should be a bidirectional relationship between the two, defined by a composite primary key made up of two properties that are shared/duplicated across both tables. A Document may have zero or one associated ScriptDocument and every ScriptDocument will have an associated Document. They both have a shared primary key made up of two properties: a string ("key") and int ("userref").
Currently I've set up my models and mappings as follows:
public class Document
{
public virtual string Key { get; set; }
public virtual int UserRef { get; set; }
public virtual ScriptDocument ScriptDocument { get; set; }
// ... other properties ...
public override bool Equals(object obj)
{
return obj is Document document &&
Key == document.Key &&
UserRef == document.UserRef;
}
public override int GetHashCode()
{
return HashCode.Combine(Key, UserRef);
}
}
public class DocumentMap : ClassMapping<Document>
{
public DocumentMap()
{
Schema("Documents");
Table("Documents");
ComposedId(m =>
{
m.Property(x => x.Key);
m.Property(x => x.UserRef, m => m.Column("User_Ref"));
// the PK fields are named slightly differently across the two tables. Same data types though and same names in the models.
});
OneToOne(x => x.ScriptDocument, m => {
m.Cascade(Cascade.All);
m.Constrained(false);
});
// ... other property mappings ...
}
}
public class ScriptDocument
{
public virtual string Key { get; set; }
public virtual int UserRef { get; set; }
public virtual Document Document { get; set; }
// ... other properties ...
public override bool Equals(object obj)
{
return obj is ScriptDocument sd &&
Key == sd.Key &&
UserRef == sd.UserRef;
}
public override int GetHashCode()
{
return HashCode.Combine(Key, UserRef);
}
}
public class ScriptDocumentMap : ClassMapping<ScriptDocument>
{
public ScriptDocumentMap()
{
Table("Script_Document");
ComposedId(m =>
{
m.Property(x => x.Key, m => m.Column("DocKey"));
m.Property(x => x.UserRef);
});
OneToOne(x => x.Document, m => m.Constrained(true));
// ... other property mappings ...
}
}
At this point, NHibernate seems happy with these models and mapping definitions, but the problem is that the relationships seem to be effectively ignored. When loading one or more Document entities, they all have a null ScriptDocument property and the same is true of the Document property on any ScriptDocument entities.
As far as I can tell, NHibernate isn't even attempting to fill those properties in any cases. I therefore assume one of two things is happening:
I've done something wrong (probably in the mappings). I'm sort of hoping there's just one or two little things I've missed, but I can't for the life of me work out what that might be.
This can't actually be done. My understanding is that this approach should be just fine if we had a single shared primary key but I'm not sure whether the shared composite key is something we can do. I can't find any comparable examples.
Note about this approach: you definitely don't need to tell me how unorthodox this is 😅 I'm painfully aware. But I'm working within the constraints of pre-existing systems. Unless this absolutely, categorically, isn't possible, this is the approach that I'd like to continue with at this point.
So the key to solving this seemed to be to use a component composite ID.
I added the following class to define the composite primary key for both tables:
[Serializable]
public class DocumentIdentifyingKey
{
public virtual string Key { get; set; }
public virtual int UserRef { get; set; }
public override bool Equals(object obj)
{
return obj is DocumentIdentifyingKey key &&
Key == key.Key &&
UserRef == key.UserRef;
}
public override int GetHashCode()
{
return HashCode.Combine(Key, UserRef);
}
public override string ToString()
{
return $"{UserRef}/{Key}";
}
}
And was then able to update the entity model classes and associated mappings as follows, using ComponentAsId defining the actual database fields for the identities for each of the two classes/tables:
public class Document
{
public virtual DocumentIdentifyingKey Identity { get; set; }
public virtual ScriptDocument ScriptDocument { get; set; }
// ... other properties ...
public override bool Equals(object obj)
{
return obj is Document document &&
Identity == document.Identity;
}
public override int GetHashCode()
{
return Identity.GetHashCode();
}
}
public class DocumentMap : ClassMapping<Document>
{
public DocumentMap()
{
Schema("Documents");
Table("Documents");
ComponentAsId(x => x.Identity, m => {
m.Property(i => i.Key);
m.Property(i => i.UserRef, m => m.Column("User_Ref"));
});
OneToOne(x => x.ScriptMetadata, m => {
m.Cascade(Cascade.All);
m.Constrained(false);
m.Fetch(FetchKind.Join);
m.Lazy(LazyRelation.NoLazy);
});
// ... other property mappings ...
}
}
public class ScriptMetadata
{
public virtual DocumentIdentifyingKey Identity { get; set; }
public virtual Document Document { get; set; }
// ... other properties ...
public override bool Equals(object obj)
{
return obj is ScriptMetadata sd &&
Identity == sd.Identity;
}
public override int GetHashCode()
{
return Identity.GetHashCode();
}
}
public class ScriptDocumentMap : ClassMapping<ScriptMetadata>
{
public ScriptDocumentMap()
{
Table("Script_Document");
ComponentAsId(x => x.Identity, m =>
{
m.Property(i => i.Key, m => m.Column("DocKey"));
m.Property(i => i.UserRef);
});
OneToOne(x => x.Document, m => {
m.Constrained(true);
m.Fetch(FetchKind.Join);
m.Lazy(LazyRelation.NoLazy);
});
// ... other property mappings ...
}
}
I'm not entirely sure why this worked but having the identity of the document expressed as an instance of an object rather than just the combination of the two fields on each class seemed to be the key incantation which allowed NHibernate to understand what I was getting at.
Note: in this solution, I've added Fetch and Lazy calls to the two OneToOne relationships. These are not specifically part of this solution but instead were added to better instruct NHibernate what load behaviour would be preferred.
Related
I am trying to create my own foreign key convention that will name the FK in "FK_SourceTable_TargetTable" format.
However, when I run it I end up with two foreign keys instead of one.
My custom foreign key convention looks like this:
public class OurForeignKeyConvention : ForeignKeyConvention
{
protected override string GetKeyName(Member property, Type type)
{
if (property == null)
return string.Format("FK_{0}Id", type.Name); // many-to-many, one-to-many, join
if (property.Name == type.Name)
return string.Format("FK_{0}_{1}", property.DeclaringType.Name, type.Name);
return string.Format("FK_{0}_{1}_{2}", property.DeclaringType.Name, property.Name, type.Name);
}
}
My code to exercise it:
[TestMethod]
public void ShouldBeAbleToBuildSchemaWithOurConventions()
{
var configuration = new Configuration();
configuration.Configure();
Fluently
.Configure(configuration)
.Mappings(m => m.FluentMappings
.AddFromAssemblyOf<Widget>()
.Conventions.Add<OurForeignKeyConvention>()
)
.BuildSessionFactory();
new SchemaExport(configuration).Create(false, true);
}
My classes and mappings:
public class Widget
{
public virtual int Id { get; set; }
public virtual string Description { get; set; }
public virtual WidgetType Type { get; set; }
public virtual ISet<WidgetFeature> Features { get; set; }
}
public class WidgetFeature
{
public virtual int Id { get; set; }
public virtual Widget Widget { get; set; }
public virtual string FeatureDescription { get; set; }
}
public class WidgetMap : ClassMap<Widget>
{
public WidgetMap()
{
Id(w => w.Id);
Map(w => w.Description);
HasMany(w => w.Features).Cascade.AllDeleteOrphan().Inverse();
}
}
public class WidgetFeatureMap : ClassMap<WidgetFeature>
{
public WidgetFeatureMap()
{
Id(w => w.Id);
Map(w => w.FeatureDescription);
References(w => w.Widget);
}
}
The end result is two foreign keys, one called what I want - FK_WidgetFeature_Widget - and another one called FK_WidgetId.
If I change OurForeignKeyConvention to always return the same name regardless of whether the "property" parameter is null then I correctly get a single FK - but I then cannot get the "SourceTable" part of my FK name.
Can anyone explain what I am doing wrong here? Why is GetKeyName called twice? And why does one of the calls not provide a value for the "property" parameter?
Doh. ForeignKeyConvention provides the name for the FK column. What I should have been using is the IHasManyConvention, which can be used to name the FK constraint itself.
public class OurForeignKeyConstraintNamingConvention : IHasManyConvention
{
public void Apply(IOneToManyCollectionInstance instance)
{
instance.Key.ForeignKey(string.Format("FK_{0}_{1}", instance.Relationship.Class.Name, instance.EntityType.Name));
}
}
I have a class that is many-to-one with its parent. I'd like to expose the parent's properties through the child without exposing the parent directly. I'd also like to query on and order by those properties.
Classes
public class Organization
{
public virtual string Name { get; set; }
public virtual bool IsNonProfit { get; set; }
}
public class Contact
{
private Organization _organization;
public virtual string OrganizationName
{ get { return _organization.Name; } }
public virtual bool OrganizationIsNonProfit
{ get { return _organization.IsNonProfit; } }
}
Mapping
public class OrganizationMap : ClassMap<Organization>
{
public OrganizationMap()
{
Map(x => x.Name);
Map(x => x.IsNonProfit);
}
}
public class ContactMap : ClassMap<Contact>
{
public ContactMap()
{
References<Organization>(Reveal.Member<Contact>("_organization"))
.Access.CamelCaseField();
}
}
Query
public class Example
{
private ISessionFactory _sessionFactory;
public Example(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
public IEnumerable<Contact> DoQuery(int forPage, int rowsPerPage)
{
using (var session = _sessionFactory.OpenSession())
{
return session.Query<Contact>().OrderBy(x => x.OrganizationName)
.Skip((forPage - 1) * rowsPerPage).Take(rowsPerPage);
}
}
}
The problem is that this results in a "Could not resolve property: OrganizationName" error. It looks like I could map those fields with a formula, but then I'd end up with a sub-select for each field on a table that's already joined into my query. Alternatively, I could wrap the Contact's organization with a public getter and change my query to OrderBy(x => x.Organization.Name). That leaves me with a Law of Demeter violation though.
Am I off track? How should I handle this?
edited to show paging
You can't use non-mapped properties in queries. How should NHibernate know how to create a SQL condition for it? In your case it might be easy, but what if you'd have a call to a method or some complicated logic in that property?
So yes, you need at least a public getter property.
Alternatively, do the sorting in memory (after NHibernate has executed the query).
This is very similar to my previous question: FluentNHibernate: How to translate HasMany(x => x.Addresses).KeyColumn("PersonId") into automapping
Say I have these models:
public class Person
{
public virtual int Id { get; private set; }
public virtual ICollection<Address> Addresses { get; private set; }
}
public class Address
{
public virtual int Id { get; private set; }
public virtual Person Owner { get; set; }
}
I want FluentNHibernate to create the following tables:
Person
PersonId
Address
AddressId
OwnerId
This can be easily achieved by using fluent mapping:
public class PersonMapping : ClassMap<Person>
{
public PersonMapping()
{
Id(x => x.Id).Column("PersonId");
HasMany(x => x.Addresses).KeyColumn("OwnerId");
}
}
public class AddressMapping : ClassMap<Address>
{
public AddressMapping()
{
Id(x => x.Id).Column("AddressId");
References(x => x.Person).Column("OwnerId");
}
}
I want to get the same result by using auto mapping. I tried the following conventions:
class PrimaryKeyNameConvention : IIdConvention
{
public void Apply(IIdentityInstance instance)
{
instance.Column(instance.EntityType.Name + "Id");
}
}
class ReferenceNameConvention : IReferenceConvention
{
public void Apply(IManyToOneInstance instance)
{
instance.Column(string.Format("{0}Id", instance.Name));
}
}
// Copied from #Fourth: https://stackoverflow.com/questions/6091290/fluentnhibernate-how-to-translate-hasmanyx-x-addresses-keycolumnpersonid/6091307#6091307
public class SimpleForeignKeyConvention : ForeignKeyConvention
{
protected override string GetKeyName(Member property, Type type)
{
if(property == null)
return type.Name + "Id";
return property.Name + "Id";
}
}
But it created the following tables:
Person
PersonId
Address
AddressId
OwnerId
PersonId // this column should not exist
So I added a AutoMappingOverride:
public class PersonMappingOverride : IAutoMappingOverride<Person>
{
public void Override(AutoMapping<Person> mapping)
{
mapping.HasMany(x => x.Addresses).KeyColumn("OwnerId");
}
}
This correctly solved the problem. But I want to get the same result using attribute & convention. I tried:
public class Person
{
public virtual int Id { get; private set; }
[KeyColumn("OwnerId")]
public virtual ICollection<Address> Addresses { get; private set; }
}
class KeyColumnAttribute : Attribute
{
public readonly string Name;
public KeyColumnAttribute(string name)
{
Name = name;
}
}
class KeyColumnConvention: IHasManyConvention
{
public void Apply(IOneToManyCollectionInstance instance)
{
var keyColumnAttribute = (KeyColumnAttribute)Attribute.GetCustomAttribute(instance.Member, typeof(KeyColumnAttribute));
if (keyColumnAttribute != null)
{
instance.Key.Column(keyColumnAttribute.Name);
}
}
}
But it created these tables:
Person
PersonId
Address
AddressId
OwnerId
PersonId // this column should not exist
Below is the rest of my code:
ISessionFactory sessionFactory = Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(connectionString))
.Mappings(m =>
m.AutoMappings.Add(AutoMap.Assemblies(typeof(Person).Assembly)
.Conventions.Add(typeof(PrimaryKeyNameConvention))
.Conventions.Add(typeof(PrimaryKeyNameConvention))
.Conventions.Add(typeof(ReferenceNameConvention))
.Conventions.Add(typeof(SimpleForeignKeyConvention))
.Conventions.Add(typeof(KeyColumnConvention)))
//m.FluentMappings
// .Add(typeof (PersonMapping))
// .Add(typeof (AddressMapping))
)
.ExposeConfiguration(BuildSchema)
.BuildConfiguration()
.BuildSessionFactory();
Any ideas? Thanks.
Update:
The test project can be downloaded from here.
Sigh... Learning NHibernate is really a hair pulling experience.
Anyway I think I finally figured out how to solve this problem: Just remove the SimpleForeignKeyConvention and everything will work fine.
It seems the SimpleForeignKeyConvention conflicts with both ReferenceKeyConvention & KeyColumnConvention. It has higher priority than KeyColumnConvention but lower priority than ReferenceKeyConvention.
public class SimpleForeignKeyConvention : ForeignKeyConvention
{
protected override string GetKeyName(Member property, Type type)
{
if(property == null)
// This line will disable `KeyColumnConvention`
return type.Name + "Id";
// This line has no effect when `ReferenceKeyConvention` is enabled.
return property.Name + "Id";
}
}
I've tested your classes with FHN's auto-mapping feature and it does not create that second PersonId on Address table.
I'm using FHN v1.2.0.721 from here
I am having an issue with using Fluent NHibernate automapping with Inheritance. Below is my entity setup (abbreviated for simplicity). I have configured Fluent NHibernate to create 1 class for the hierarchy with a discriminator column. The automapping appears to be working correctly as when I generate a database, one table is created named "AddressBase" with a discriminator column that signals what type of address each row is.
The problem lies in the face that when I call the method "GetPrimaryBillingAddress()" on the UserAccount class, instead of just querying Billing Addresses, NHibernate is creating a query that looks at both Billing and Shipping Addresses. It doesn't take into account the discriminator at all. I am assuming there is some sort of configuration I can set but have not been able to find anything.
public abstract class AddressBase : ActiveRecord<AddressBase>
{
public virtual long Id { get; set; }
public virtual string Address1 { get; set; }
}
public class AddressBilling : AddressBase
{
public class TypedQuery : ActiveRecordQuery<AddressBilling> { }
public virtual bool IsPrimary { get; set; }
}
public class AddressShipping : AddressBase
{
public class TypedQuery : ActiveRecordQuery<AddressShipping> { }
[Display(Name = "Is Primary")]
public virtual bool IsPrimary { get; set; }
}
public class UserAccount : ActiveRecord<UserAccount>
{
public virtual long Id { get; set; }
public virtual IList<AddressBilling> BillingAddresses { get; set; }
public virtual IList<AddressShipping> ShippingAddresses { get; set; }
public UserAccount()
{
BillingAddresses = new List<AddressBilling>();
ShippingAddresses = new List<AddressShipping>();
}
public virtual AddressBilling GetPrimaryBillingAddress()
{
if (BillingAddresses.Any(x => x.IsPrimary))
{
return BillingAddresses.Single(x => x.IsPrimary);
}
return BillingAddresses.FirstOrDefault();
}
public virtual AddressShipping GetPrimaryShippingAddress()
{
if (ShippingAddresses.Any(x => x.IsPrimary)) {
return ShippingAddresses.Single(x => x.IsPrimary);
}
return ShippingAddresses.FirstOrDefault();
}
}
UPDATE:
Here is the Mapping override functions used in the automapping:
private static FluentConfiguration GetFluentConfiguration(string connectionStringName = "CS")
{
var autoMapping = AutoMap
.AssemblyOf<Product>(new Mapping.AutoMappingConfiguration())
.Conventions.Setup(c =>
{
c.Add<Mapping.ForeignKeyConvention>();
c.Add<Mapping.DiscriminatorConvention>();
})
.IgnoreBase<AddressBilling.TypedQuery>()
.IgnoreBase<AddressShipping.TypedQuery>()
.IncludeBase<AddressBase>();
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2005.ConnectionString(c => c.FromConnectionStringWithKey(connectionStringName)))
.Mappings(m => m.AutoMappings.Add(autoMapping));
}
public class AutoMappingConfiguration : DefaultAutomappingConfiguration
{
public override bool ShouldMap(Type type)
{
var isStatic = type.IsAbstract && type.IsSealed;
return type.Namespace == typeof(Entities.Product).Namespace && !isStatic;
}
public override bool IsDiscriminated(Type type)
{
if (type == (typeof(Entities.AddressBase))) {
return true;
}
return false;
}
public override string GetDiscriminatorColumn(Type type)
{
return "Type";
}
public class DiscriminatorConvention : ISubclassConvention
{
public void Apply(ISubclassInstance instance)
{
//Address
if (instance.Name == typeof(AddressBilling).AssemblyQualifiedName)
{
instance.DiscriminatorValue(Enums.AddressType.BillingAddress);
}
else if (instance.Name == typeof(AddressShipping).AssemblyQualifiedName)
{
instance.DiscriminatorValue(Enums.AddressType.ShippingAddress);
}
}
}
Thanks!
Please, try to change your class UserAccount like this:
public class UserAccount : ActiveRecord<UserAccount>
{
public virtual IList<AddressBase> Addresses { get; set; }
public virtual IList<AddressBilling> BillingAddresses { get {return this.Addresses.OfType<AddressBilling>();} }
public virtual IList<AddressShipping> ShippingAddresses { get {return this.Addresses.OfType<AddressShipping>();} }
// ...
}
Of course, only Addresses property should be mapped here.
I have a class called Worker
public class Worker : BaseEntity
{
public virtual int WorkerID { get; set; }
public virtual string Name { get; set; }
public virtual IList<Indemnification> Indemnifications { get; set; }
}
public class Indemnification : BaseEntity
{
public virtual int IndemnificationID { get; set; }
public virtual string IndemnificationNumber { get; set; }
//another properties
}
i am using automapping with some conventions
var mappings = new AutoPersistenceModel();
mappings.AddEntityAssembly(typeof(Worker).Assembly).Where(GetAutoMappingFilter);
mappings.Conventions.Setup(GetConventions());
mappings.Setup(GetSetup());
private Action<IConventionFinder> GetConventions()
{
return c =>
{
c.Add<PrimaryKeyConvention>();
c.Add<HasManyConvention>();
c.Add<TableNameConvention>();
c.Add<CustomForeignKeyConvention>();
c.Add<SubClassConvention>();
};
}
public class PrimaryKeyConvention : IIdConvention
{
public void Apply(FluentNHibernate.Conventions.Instances.IIdentityInstance instance)
{
instance.Column(instance.EntityType.Name + "ID");
instance.UnsavedValue("0");
}
}
public class HasManyConvention : IHasManyConvention
{
public void Apply(FluentNHibernate.Conventions.Instances.IOneToManyCollectionInstance instance)
{
instance.Key.Column(instance.EntityType.Name + "ID");
instance.Cascade.AllDeleteOrphan();
}
}
public class TableNameConvention : IClassConvention
{
public void Apply(FluentNHibernate.Conventions.Instances.IClassInstance instance)
{
instance.Table(instance.EntityType.Name);
}
}
public class CustomForeignKeyConvention : ForeignKeyConvention
{
protected override string GetKeyName(Member property, Type type)
{
return type.Name + "ID";
}
}
public class SubClassConvention : IJoinedSubclassConvention
{
public void Apply(FluentNHibernate.Conventions.Instances.IJoinedSubclassInstance instance)
{
instance.Table(instance.EntityType.Name);
instance.Key.Column(instance.EntityType.BaseType.Name + "ID");
}
}
the problem is when i save Worker with a list of Indemnifications:
the worker is saved, and so the Indemnifications but the foreign key (WorkerID) in
the Indemnification table is null????
I figured out the problem:
when you need to save an entity which has (one to many) relationship, you need to open a transaction and commit it :).
Session.BeginTransaction();
Session.Save(entity);
Session.CommitTransaction();
Didn´t you wonder why the automapping allowed foreign keys that are created for a one-to-many relation ship to be null in the first place?
So in your example why does the column "workerId" in the table "Indemnification" not have the not null constraint added to it?
I just came across the the problem and I think even though it can be handled in code, it should not be possible at all to insert a null value, right? Any solution for that?