Fluent nHibernate: How to set up id convention so that override works on a table with a different id - fluent-nhibernate

Weird case with a legacy database:
Most tables have the same named field for the primary key, but I have one table that has a different primary key but also has that named field. When I override the automap for this table it uses the convention not the override unless I explicitly override the mapping for the name field.
public class IdConvention : IIdConvention, IIdConventionAcceptance,
{
public void Accept(IAcceptanceCriteria<IIdentityInspector> criteria)
{
criteria.Expect(x => x.Type == typeof(string));
}
public void Apply(IIdentityInstance instance)
{
if (instance.Name == "StandardId")
instance.Column("FooBarThingyId");
}
}
public class WierdCase
{
virtual int CustomId {get;set;}
virtual int StandardId {get;set;}
}
public class WierdCaseOverride : IAutoMappingOverride<WierdCase>
{
public void Override(AutoMapping<WierdCase> mapping)
{
mapping.Id(x => x.CustomId).GeneratedBy.Identity();
mapping.Map(x => x.StandardId , "FooBarThingyId");
}
}
public class CustomConfiguration : DefaultAutomappingConfiguration
{
public override bool IsId(Member member)
{
if (member.DeclaringType.GetProperty("StandardId") != null)
return member.Name == "StandardId";
return base.IsId(member);
}
}
Without the mapping.Map() line, this uses StandardId as the key, not CustomId. I would expect calling mapping.Id() to override the convention, but it does not seem to.
What am I missing here? As I'd rather fix the convention than have to explicitly override it for the exceptions.

Related

One-to-one relationship in NHibernate using composite key

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.

How to map a view without an identity column in NHibernate?

I have a view that I am going to only read from (no writes). This view does not have any unique key (not event composite).
So how can I map this view in NHibernate without touching the view? I do not want to add a new column to the view to generate a unique identity for me. Is there a way to map this view and generate the identity column on the NHibernate side?
I can generate a GUID in my entity class like:
public class MyViewClass
{
private Guid _id = new Guid();
public virtual Guid Id { get { return _id; } set { _id = value; } }
}
But how can I make the mapping work? The following code does not work:
public class MyViewClass: ClassMapping<MyViewClass>
{
public MyViewClass()
{
Mutable(false);
Id(x => x.Id, m => m.Generator(Generators.Guid));
}
}
It expects to have the Id column in view and throws:
System.Data.SqlClient.SqlException: Invalid column name 'Id'.
BTW, I am using NHibernate 3.2 and mapping by code.
Update: to use it in LINQ map all columns as CompositeId and Mutable(false) then override Equals and GetHashCode with default implementation.
public class MyViewClass
{
public override bool Equals(object obj)
{
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
original answer:
i wouldnt map it at all if you dont want to insert/update it
public class MyViewClass
{
public virtual string Prop1 { get; set; }
public virtual int Prop2 { get; set; }
}
var viewObjects = session.CreateSQLQuery("SELECT col1 as Prop1, col2 as Prop2 FROM MyView")
.SetResultTransformer(Transformers.AliasToBean<MyViewClass>())
.List<MyViewClass>();

My custom ForeignKeyConvention is resulting in two foreign keys being created instead of one

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));
}
}

FluentNHibernate: Automapping OneToMany relation using attribute and convention

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

how to tell flunet-nhibernate to use the ID from a base class (which is abstract and ignored in the mapping)

i have an abstract class
public abstract class Document
{
public int DocumentID {get; set;}
}
and derived class
public class DoctorDocument : Document{
public string DoctorName {get;set;}
}
and I'm using Fluent Auto Mapping,
i need not to make a table for Document, but i need each derived class to get the DocumentID as Primary Key.
mappings.IgnoreBase<Document>();
mappings.AddEntityAssembly(typeof(DoctorDocument).Assembly);
mappings.Setup(c=>c.FindIdentity = type.Name == type.DeclaringType.Name + "ID";);
but it still can't find the ID and tells me that DoctorDocument doesn't have an ID.
but when i made the following override it worked:
public class DoctorDocumentMap: IAutoMappingOverride<DoctorDocument>
{
public void Override(AutoMapping<DoctorDocument> mapping)
{
mapping.Id(x => x.Id, "DocumentID").GeneratedBy.Identity();
}
}
how can i tell the automapping to do that for all entities?? especially the GeneratedBy.Identity();
Overriding DefaultAutomappingConfiguration might help.
Something like this might work :
public class MyAppAutoConfiguration : DefaultAutomappingConfiguration
{
public override bool IsId(Member member)
{
return "DocumentID" == member.Name;
}
}
The configuration can be like this:
var cfg = new MyAppAutoConfiguration();
var autoPersistenceModel = AutoMap.AssemblyOf<Person>(cfg).IgnoreBase<Document>();
ISessionFactory sessionFactory = Fluently.Configure()
.Database(OracleClientConfiguration.
Oracle10.ConnectionString(
ConfigurationManager.ConnectionStrings["OracleConn"].ConnectionString))
.Mappings(m =>
m.AutoMappings
.Add(autoPersistenceModel))
.BuildSessionFactory();