I've been using Fluent NHibernate and I've noticed what I think is inconsistent behaviour when using reference when overloading the default Foreign Key convention.
Following the conventions of NHibernate, I would have a foreign key name with a _ within it and a single record set in each one with a foreign key relationship between the records. For example:
Person
Id - Integer - PK
Name - String/NVarchar
Address
Id - Integer - PK
LineOne - String/NVarchar
Person_Id - Integer - FK
With the following code, this all works as expected
void Main()
{
using var factory = Fluently.Configure()
.Database(MsSqliteConfiguration.Standard.UsingFile("C:\\Temp\\Test.db"))
.Mappings(mappings =>
{
mappings
.FluentMappings
.AddFromAssemblyOf<AddressClassMap>();
})
.BuildSessionFactory();
using var session = factory.OpenSession();
session.Query<Address>().ToList().Dump();
}
public class PersonClassMap : ClassMap<Person>
{
public PersonClassMap()
{
Table("Person");
Id(x => x.Id);
}
}
public class AddressClassMap : ClassMap<Address>
{
public AddressClassMap()
{
Table("Address");
Id(x => x.Id);
Map(x => x.PersonId).Column("Person_Id");
References(x => x.Person).Not.LazyLoad();
}
}
public class Person
{
public virtual int Id { get; set; }
}
public class Address
{
public virtual int Id { get; set; }
public virtual int PersonId { get; set; }
public virtual Person Person { get; set; }
}
Will return the expected data:
However, if I don't follow convention and have no underscore, like so:
Person
Id - Integer - PK
Name - String/NVarchar
Address
Id - Integer - PK
LineOne - String/NVarchar
PersonId - Integer - FK
I would expect, adding a convention for the foreign key like in the below code, would have the same effect but just would work without the underscore:
void Main()
{
using var factory = Fluently.Configure()
.Database(MsSqliteConfiguration.Standard.UsingFile("C:\\Temp\\Test.db"))
.Mappings(mappings =>
{
mappings
.FluentMappings
.AddFromAssemblyOf<AddressClassMap>()
.Conventions.Add(FluentNHibernate.Conventions.Helpers.ForeignKey.EndsWith("Id"));
})
.BuildSessionFactory();
using var session = factory.OpenSession();
session.Query<Address>().ToList().Dump();
}
public class PersonClassMap : ClassMap<Person>
{
public PersonClassMap()
{
Table("Person");
Id(x => x.Id);
}
}
public class AddressClassMap : ClassMap<Address>
{
public AddressClassMap()
{
Table("Address");
Id(x => x.Id);
Map(x => x.PersonId);
References(x => x.Person).Not.LazyLoad();
}
}
public class Person
{
public virtual int Id { get; set; }
}
public class Address
{
public virtual int Id { get; set; }
public virtual int PersonId { get; set; }
public virtual Person Person { get; set; }
}
However this would yield a exception:
I would expect that this code would allow me to not have to follow the convention of using '_' but this doesn't seem to be the case without hitting the exception. I would expect in the first example, it should be working the same way as the second.
The behavior I desire would be to have the code be like the first example I gave, but not have to follow the convention of NHibernate. This would enable me to have the foreign key ID set on the entity and allow me to persist the relationship using this foreign key property.
I'm unsure if I may have done something wrong or missed something? I've tried various other options of propertyref, etc, to see if it would yield the behaviour I would expect but no avail so far.
It doesn't work as expected in first example. It creates 2 columns in Address table for person:
PersonId for References(x => x.Person)
Person_Id for Map(x => x.PersonId).Column("Person_Id"))
In second example you are trying to map the same column multiple times hence the exception. NHibernate doesn't know how to build INSERT/UPDATE statements for duplicate columns. You need to mark duplicates as read-only properties (as insert="false" update="false" in xml mapping). Something like:
Map(x => x.PersonId);
References(x => x.Person).Not.LazyLoad().ReadOnly();
Related
I just started to make EntityTypeConfiguration class and did following
public class Xyz
{
public int PlaceId { get; set; }
public string Name { get; set; }
public DbGeography Location { get; set; }
public int HumanTypeId { get; set; }
public int AddressId { get; set; }
}
and in EntityTypeConfiguration class
public sealed class XyzConfiguration:EntityTypeConfiguration<Xyz>
{
public XyzConfiguration()
{
ToTable("Place", "dbo");
HasKey(p => p.PlaceId);
Property(p => p.PlaceId)
.HasColumnName("PlaceId")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(p => p.Name);
Property(p => p.Location). ;
Property(p => p.HumanTypeId);
Property(p => p.AddressId);
}
}
Now how to set DbGeography and foreign key columns HumanTypeId , AddressId ?
Thanks in advance
It depends on what you're going to do with the columns. If you have foreign key columns like AddressId, you probably have some Address entities that you want to relate to your Xyz entities. You need to decide how the entites relate to each other, and configure the mapping you want between them.
You will need a navigation property either in your Address class, or your Xyz class, otherwise there isn't anything to bind the foreign key to, and your foreign ID columns would just be treated as normal columns (which is fine, if that's what you want).
So, if your were to add a navigation property to your Xyz entity
public class Xyz
{
// Your code
public int AddressId { get; set; }
public virtual Address MyAddress { get; set; }
}
// Your Address class
public class Address
{
public int ID;
}
You could configure the mapping by doing something along these lines (it will vary depending on the relationship:
public sealed class XyzConfiguration : EntityTypeConfiguration<Xyz>
{
public XyzConfiguration()
{
// Your code.
this.HasOptional(x => x.MyAddress) // Your Xyz has an optional Address
.WithMany() // Address may be owned by many Xyz objects
.HasForeignKey(x => x.AddressId); // Use this foreign key.
}
}
I haven't tried using spatial types and EF, but I'd start here: http://msdn.microsoft.com/en-us/data/hh859721.aspx
There's a wealth of information on mapping configurations on the getting started with EF pages: http://msdn.microsoft.com/en-us/data/ee712907 try "Fluent API - Configuring/Mapping Properties & Types"
There's also a slightly abridged explanation of the different association types here:
Code First: Independent associations vs. Foreign key associations?
I have a simple many-to-many relationship using Fluent NHibernate and it is working pretty fine.
Here is my first class mapping:
public LobMapping()
{
...
HasManyToMany(x => x.Commodities)
.Table("PPBSYS.LOB_COMMODITY")
.ParentKeyColumn("LOB_ID")
.ChildKeyColumn("COMMODITY_ID")
.Cascade.SaveUpdate()
.LazyLoad();
...
}
Here is my second class mapping:
public CommodityMapping()
{
...
HasManyToMany(x => x.Lobs)
.Table("PPBSYS.LOB_COMMODITY")
.ParentKeyColumn("COMMODITY_ID")
.ChildKeyColumn("LOB_ID")
.Inverse()
.Cascade.All()
.LazyLoad();
...
}
And finally, my Lob object has a list of commodities:
public class Lob
{
...
public virtual IEnumerable<Commodity> Commodities { get; set; }
...
}
However I am not happy with the fact that I must reference the entire commodity inside the Lob class. I really would like to do:
var lob = new Lob();
lob.Commodities = new [] { new Commodity { CommodityId = 1 }};
repository.SaveLob(lob);
But if I run the code above, NHibernate will try to update the Commodity table setting the columns to null for the commodity with ID = 1.
So actually I must get the entire object, before saving the Lob:
var lob = new Lob();
lob.Commodities = new [] { repostitory.GetCommodit(1) };
repository.SaveLob(lob);
And I know that the commodity exists, because the user just have selected them.
It is possible to accomplish my task?
I assume your repository is calling session.Get<T>(id) under the covers. There is also session.Load<T>(id).
lob.Commodities = new [] { session.Load<Commodity>(1) };
From the NHibernate documentation on Load()...
If the class is mapped with a proxy, Load() returns an object that is an uninitialized proxy and does not actually hit the database until you invoke a method of the object. This behaviour is very useful if you wish to create an association to an object without actually loading it from the database.
Daniel's answer sounds promising, let me know if in Save the proxy won't hit the database to fill all the properties of the entity.
Another answer would be a little forcefully on Nhibernate, i tested it and it's working,
working code :
public class Com
{
public virtual Guid ID { get; set; }
public virtual string Name { get; set; }
public virtual IList<Lob> Lobs { get; set; }
}
public class Lob
{
public virtual Guid ID { get; set; }
public virtual string Name { get; set; }
public virtual IList<Com> Coms { get; set; }
}
class LobMap : ClassMap<Lob>
{
public LobMap()
{
Id(x => x.ID).GeneratedBy.GuidComb();
Map(x => x.Name);
HasManyToMany(x => x.Coms)
.Table("LOB_COM")
.ParentKeyColumn("LOB_ID")
.ChildKeyColumn("COM_ID")
.Cascade.SaveUpdate()
.LazyLoad();
}
}
class ComMap : ClassMap<Com>
{
public ComMap()
{
Id(x => x.ID).GeneratedBy.GuidComb();
Map(x => x.Name);
HasManyToMany(x => x.Lobs)
.Table("LOB_COM")
.ParentKeyColumn("COM_ID")
.ChildKeyColumn("LOB_ID")
.Inverse()
.Cascade.All()
.LazyLoad();
}
}
Now - I set a dummy entity to mimic the connection table and map it :
public class ComLobConnection
{
public virtual Guid ComID { get; set; }
public virtual Guid LobID { get; set; }
}
public class ComLobConnectionMap : ClassMap<ComLobConnection>
{
public ComLobConnectionMap()
{
Table("LOB_COM");
Id();
Map(x => x.ComID,"COM_ID");
Map(x => x.LobID,"LOB_ID");
}
}
Notice that i'm mapping to exact same fields and Table as the ManyToMany, with no ID set up (that's the empty Id() call)
Now all that's left is to save a ComLobConnection and it will be added to the Com.Lobs and Lob.Coms
Saved some test Com,Lob
var testCom = new Com() { Name = "TestCom" };
var testLob = new Lob() { Name = "TestLob" };
session.SaveOrUpdate(testCom);
session.SaveOrUpdate(testLob);
after save - took their ID's and saved a connection to test
var testConnection = new ComLobConnection()
{
ComID = Guid.Parse("D3559F53-8871-45E9-901D-A22800806567"),
LobID = Guid.Parse("D372D430-2E61-44F2-BA89-A228008065F1")
};
session.SaveOrUpdate(testConnection);
This forced a new record in the manytomany table, and worked when getting both Lob and Com after.
I'm not recommending this :), It just interested me if it can be done, and it can.
This way you will not hit the DB for loading Coms or Lobs that you know exist to save their connection.
Hopefully Load() will not hit the DB for that too :)
Edit : can be done with any kind of ID
I think I have a design issue here.
essentially I have a class called office
class Office
{
public virtual long Id { get; set; }
public virtual string Code { get; set; }
public virtual IList<Person> Managers { get; set; }
public virtual IList<Person> Developers { get; set; }
public virtual IList<Person> TeaMakers { get; set; }
}
and a class called Person
class Person
{
public virtual long Id { get; set; }
public virtual string Name {get; set;}
public virtual StaffType Type { get; set;}
public virtual Office Office { get; set; }
}
and an enum called StaffType
public enum StaffType
{
MANAGER,
DEVELOPER,
TEAMAKER
}
Mapping the Person table is easy:
public class PersonMap: ClassMap<Person>
{
public PersonMap()
{
Table("Person");
Id(x => x.Id);
Map(x => x.Name);
References(x => x.Office).ForeignKey("Id").Not.Nullable()
Map(x => x.Type).CustomType<StaffType>();
}
}
but i am stumped on the office map. how to i get the map to use the enum to filter the 3 lists?
if i do this:
public class OfficeMap: ClassMap<Office>
{
public static string TableName = "Office";
public static string MappingColumn = TableName + "Id";
public OfficeMap()
{
Table(TableName);
Id(x => x.Id);
Map(x = x.Code);
HasMany(x => x.Managers)
.Cascade.AllDeleteOrphan()
.Fetch.Select()
.Inverse().KeyColumn(MappingColumn);
HasMany(x => x.Developers)
.Cascade.AllDeleteOrphan()
.Fetch.Select()
.Inverse().KeyColumn(MappingColumn);
HasMany(x => x.TeaMakers)
.Cascade.AllDeleteOrphan()
.Fetch.Select()
.Inverse().KeyColumn(MappingColumn);
}
}
fluency won't have the foggiest idea how to split the 3 collections up by the StaffType enum
Thanks for the help
Extra note: the Person table's Type field allways gets mapped as an int.
NHibernate supports filtering as a part of the mapping. Please, read here more 6.2. Mapping a Collection.
The trick is to add more SQL into the mapping. In fact, some WHERE Condition, to be evaluated during the collection load. Small extract from the documentation:
<map // or <set or <bag ...
name="propertyName" (1)
table="table_name" (2)
...
where="arbitrary sql where condition" (9)
And the description of the WHERE:
where (optional) specify an arbitrary SQL WHERE condition to be used
when retrieving or removing the collection (useful if the collection
should contain only a subset of the available data)
In your case, the fluent syntax is similar: ...Where("MyColumn = 'myValue' ");
A draft for your solution:
...
HasMany(x => x.Managers)
.Cascade.AllDeleteOrphan()
.Fetch.Select()
.Inverse().KeyColumn(MappingColumn)
.Where("Type = 1") // the Column name in the Person table
; // and the value 1 as the enum of the Manager
...
// The same for the others
I would model this as a simple one-to-many (Office has many Person) and add an extension method to IEnumerable<Person> to filter by StaffType. If needed, you can encapsulate access to the Person collection through AddManager etc. methods that enforce business rules.
Problem:
There are searches that can be stored in the DB. Each search has a collection of filters. Also there are roles. Each role may have (nullable column) a default search assigned to it. Also, each search is visible to zero or many roles (many-to-many relationship).
When I try to access the search filters, NH tries to access filters.DefaultSearchId, which doesn't exist in filters table.
DB:
CREATE TABLE [dbo].[Searches]
(
Id int identity(1,1) primary key,
Description nvarchar(2000) not null
);
CREATE TABLE [dbo].[Filters]
(
Id int identity(1,1) primary key,
Description nvarchar(2000) not null,
SearchId int not null references Searches(Id)
);
CREATE TABLE [dbo].[Roles]
(
Id int identity(1,1) primary key,
Name nvarchar(255) not null,
DefaultSearchId int null references Searches(Id)
);
CREATE TABLE [dbo].[SearchesRoles]
(
SearchId int not null references Searches(Id),
RoleId int not null references Roles(Id)
);
Entities:
public class Search {
public virtual int Id { get; set; }
public virtual string Description { get; set; }
public virtual ICollection<Filter> Filters { get; set; }
public virtual ICollection<Role> Roles { get; set; }
}
public class Filter {
public virtual int Id { get; set; }
public virtual string Description { get; set; }
public virtual Search Search { get; set; }
}
public class Role {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Search DefaultSearch { get; set; }
}
Mappings:
public class SearchMap : ClassMap<Search>{
public SearchMap() {
Table("Searches");
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.Description);
HasMany(x => x.Filters).Inverse().Cascade.All().AsBag();
HasManyToMany(x => x.Roles).Table("SearchesRoles").ParentKeyColumn("SearchId").ChildKeyColumn("RoleId");
}
}
public class FilterMap : ClassMap<Filter> {
public FilterMap() {
Table("Filters");
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.Description);
References(x => x.Search).Column("SearchId");
}
}
public class RoleMap : ClassMap<Role> {
public RoleMap() {
Table("Roles");
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.Name);
References(x => x.DefaultSearch).Column("DefaultSearchId");
}
}
Code:
class Program {
static void Main() {
var sessionFactory = CreateSessionFactory();
using (var session = sessionFactory.OpenSession()) {
var search = session.Get<Search>(1);
foreach (var filter in search.Filters) {
Console.WriteLine(filter);
}
}
}
static ISessionFactory CreateSessionFactory(){
string connectionString = #"server=.\sql2008; user id = sa; pwd=1; database = nhbug;";
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(connectionString))
.Mappings(m=>m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())).BuildSessionFactory();
}
}
ERROR:
When accessing the search.Filters property, NHibernate tries to access Filters.DefaultSearchId db column which is not supposed to be there. This column exists in Roles table but not in filters.
QUESTION:
Is it invalid configuration, Fluent NHibernate or NHibernate bug?
I'm using SQL Server 2008 R2, NHibernate 2.1.2 and Fluent NHibernate 1.1.0.685, although this issue exists in NHibernate 3 beta 2 as well.
Thank you.
UPDATE:
Here is the actual SQL generated
UPDATE2: CDMDOTNET, same error, same sql, unfortunately.
UPDATE3: Actual exception
UPDATE4: This is a particular use case of a general bug: Entity references other entities as 'many-to-many' and on the other side of 'many-to-many' assoc. the other entity references the source entity (DefaultQuery in my case). NH goes nuts when accessing any child collection (one-to-many) of a source entity (Filters in my case).
UPDATE5: Sample data
UPDATE6: XML issued by Fluent NHibernate
Update the HasMany mapping on the SearchMap to include the KeyColumn():
HasMany(x => x.Filters).KeyColumn("SearchId").Inverse().Cascade.All().AsBag();
You didn't specify the column name for the Filters bag. It should be set to SearchId.
New to FluentNHibernate =D
I have a parent/children classes as follows:
public class Parent
{
public virtual int ID { get; private set; }
public virtual string Name { get; set; }
public virtual IList<Child> Children { get; set; }
}
public class Child
{
public virtual int ID { get; private set; }
public virtual string Name { get; set; }
public virtual Parent ActiveParent { get; set; }
}
With mappings of:
public ParentMap()
{
Id(x => x.ID);
Map(x => x.Name);
HasMany(x => x.Children)
.Inverse();
.Cascade.All();
}
public ChildMap()
{
Id(x => x.ID);
Map(x => x.Name);
//Map(x => x.ActiveParent)
// .Column(ParentID);
}
The commented out area of the child map is the question I'm currently having trouble with. I'd like to be able to create a child object and call its' parent(ie, someChild.ActiveParent), but am unsure on how to map this via the fluent interface.
The table structure for the child table holds a parentid, with the intent of lazy loading the parent object if called. Any help is always greatly appreciated.
References(x => x.Parent);
Adding to mxmissile's answer, you will want to add a LazyLoad() to the end of the References() call, and also you might want to do something like this in your configuration:
.Mappings(m =>
m.FluentMappings.AddFromAssemblyOf<ParentMap>()
.ConventionDiscovery.Add(ForeignKey.EndsWith("ID")))
The last line instructs Fluent NHibernate to expect foreign keys named like ParentID rather than the default (Parent_Id ?), so you no longer need to specify the column name explicitly in every relationship mapping.