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.
Related
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();
I want to have a intermediate table with only two foreign keys (as a ComposedId).
But NHibernate is automatically creating a "id" property.
I have the following classes
public class Lace
{
public virtual int Id { get; set; }
public virtual string Hostname { get; set; }
public virtual IList<LaceHasCard> LaceHasCards { get; set; }
}
public class Card
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<LaceHasCard> LaceHasCards { get; set; }
}
and this manually created intermediate table
public class LaceHasCard
{
public virtual Card Card { get; set; }
public virtual Lace Lace { get; set; }
}
Mappings
public LaceMapping()
{
Id(x => x.Id, map => map.Generator(Generators.Native));
Property(x => x.Hostname);
Bag(x => x.LaceHasCards, col =>
{
col.Key(k => k.Column("LaceId"));
col.Inverse(true);
}, r => r.OneToMany());
}
public CardMapping()
{
Id(x => x.Id, map => map.Generator(Generators.Native));
Property(x => x.Name);
Bag(x => x.LaceHasCards, col =>
{
col.Key(k => k.Column("CardId"));
col.Inverse(true);
}, r => r.OneToMany());
}
intermediate table mapping
public LaceHasCardMapping()
{
//ComposedId(map =>
//{
// map.Property(x => x.Card.Id, a =>
// {
// a.Column("CardId");
// });
// map.Property(x => x.Lace.Id, a =>
// {
// a.Column("LaceId");
// });
//});
ManyToOne(x => x.Card, map =>
{
map.Column("CardId");
});
ManyToOne(x => x.Lace, map =>
{
map.Column("LaceId");
});
}
If I create the schema with the ComposedId commented out, NHibernate will create a "id" property in the table.
CREATE TABLE [dbo].[LaceHasCard] (
[id] INT NOT NULL,
[CardId] INT NULL,
[LaceId] INT NULL,
PRIMARY KEY CLUSTERED ([id] ASC),
CONSTRAINT [FKDC6D54711CD160AE] FOREIGN KEY ([CardId]) REFERENCES [dbo].[Card] ([Id]),
CONSTRAINT [FKDC6D547151F8AF85] FOREIGN KEY ([LaceId]) REFERENCES [dbo].[Lace] ([Id])
);
If I try to create the schema with the ComposedId, I get the following error message:
Unable to instantiate mapping class (see InnerException):
EmpLaceMgmt.Models.Mappings.LaceHasCardMapping
What would be the right way to tell NHibernate to create a composed Id?
Let me give you suggestion, just my point of view - do not use composite id. Use standard primary key in DB and its C# / entity representation as Id { get; set; }
Chapter 24. Best Practices
...
Declare identifier properties on persistent classes.
NHibernate makes identifier properties optional. There are all sorts of reasons why you should use them. We recommend that identifiers be 'synthetic' (generated, with no business meaning) and of a non-primitive type. For maximum flexibility, use Int64 or String.
See also more about synthetic, surrogate keys at wiki.
From my experience, we should not be worry about having pairing object like this:
public class LaceHasCard
{
public virtual int Id { get; set; } // the key
public virtual Card Card { get; set; }
public virtual Lace Lace { get; set; }
}
Because later it would become so easy to access it:
session.Get<LaceHasCard>(id)
And also to use it in Subqueries (for filtering Card with Laces and vice versa)
One column in DB, autogenerated, should not have any extra bad impact. But handling such table is a bit (a lot) easier...
So, summary, my suggestion would be, make all entities first level citizens, with full rights (including synthetic/surrogate key)
I'm just starting to work with NHibernate. I have two objects:
public class Supplier
{
public virtual int id{get;set;}
public virtual SupplierAddress address{get;set;}
public virtual string Name{get;set;}
}
public class SupplierAddress
{
public virtual int id{get;set;}
public virtual Supplier{get;set;}
public virtual string SupplierAddressLine{get;set;}
}
When I want to create a new Supplier I create a new object:
var supplierAddress = new SupplierAddress {
SupplierAddressLine = "someLine"
}
var supplier = new Supplier
{
Name = "someName",
SupplierAddress = SupplierAddressLine
}
Then, when i try to save using:
_session.Save(supplier);
I get the error: "Cannot insert the value NULL into column 'id'
Update 1 Mappings
for SupplierAddress
Id(x => x.Id).GeneratedBy.Identity().Column("Id");
References(x => x.Supplier).Column("SupplierId");
Map(x => x.AddressLine1).Column("AddressLine1").Not.Nullable().Length(255);
for Supplier
Id(x => x.Id).GeneratedBy.Identity().Column("Id");
References(x => x.SupplierAddress).Column("SupplierAddressId").Not.Nullable();
HasMany(x => x.SupplierAddresses).KeyColumn("SupplierId");
You should set some cascade rules on the Supplier → SupplierAddress relationship:
References(s => s.SupplierAddress)
.Column("SupplierAddressId")
.Not.Nullable()
.Cascade.All(); /* Cascade operations that happen on `Supplier` */
Otherwise, NHibernate does not know that saving the parent (Supplier) should also save the child (SupplierAddress)
Edit: I think you're actually using References incorrectly here.
In a mapping, when you say an entity References another, you're basically telling NHibernate that the other side of this relationship is a HasMany.
In your case, neither Supplier nor SupplierAddress actually has many SupplierAddresses or Suppliers, respectively.
With that in mind, you probably mean one of two things:
A SupplierAddress is shared by multiple Suppliers. This would mean that SupplierAddress actually has many Suppliers, but a Supplier only has one SupplierAddress.
In the C# class, this would mean that SupplierAddress has a collection of Suppliers (OR has no reference to Supplier at all).
In this case, your database tables would look like this:
create table [SupplierAddress]
(
[Id] int identity(1,1) primary key clustered,
[AddressLine1] nvarchar(255) not null
);
create table [Supplier]
(
[Id] int identity(1,1) primary key clustered,
[SupplierAddressId] int not null references [SupplierAddress]([Id])
)
Your C# classes would look like this:
public class Supplier
{
public virtual int Id { get; set; }
public virtual SupplierAddress SupplierAddress { get; set; }
public virtual string Name { get; set; }
}
public class SupplierAddress
{
public virtual int Id { get; set; }
public virtual string AddressLine1 { get; set; }
}
And your mappings would look like this:
public class SupplierMap : ClassMap<Supplier>
{
public SupplierMap()
{
Id(s => s.Id).GeneratedBy.Identity().Column("Id");
References(s => s.SupplierAddress)
.Column("SupplierAddressId")
.Cascade.All();
}
}
public class SupplierAddressMap : ClassMap<SupplierAddress>
{
public SupplierAddressMap()
{
Id(s => s.Id).GeneratedBy.Identity().Column("Id");
Map(s => s.AddressLine1)
.Column("AddressLine1")
.Not.Nullable()
.Length(255);
}
}
A Supplier has one SupplierAddress, and a SupplierAddress is only ever associated with one Supplier. Another way to think of this is that the entire SupplierAddress table could be logically merged into Supplier.
In this case, your database tables would look like this:
create table [SupplierAddress]
(
[Id] int identity(1,1) primary key clustered,
[AddressLine1] nvarchar(255) not null,
[SupplierId] int not null
);
create table [Supplier]
(
[Id] int identity(1,1) primary key clustered,
[SupplierAddressId] int references [SupplierAddress]([Id])
);
alter table [SupplierAddress]
add constraint [FK_SupplierAddress_Supplier]
foreign key ([SupplierId]) references [Supplier]([Id])
Your C# classes would look like this:
public class Supplier
{
private SupplierAddress supplierAddress;
public virtual int Id { get; set; }
public virtual SupplierAddress SupplierAddress
{
get { return this.supplierAddress; }
set
{
this.supplierAddress = value;
this.supplierAddress.Supplier = this;
}
}
public virtual string Name { get; set; }
}
public class SupplierAddress
{
public virtual int Id { get; set; }
public virtual string AddressLine1 { get; set; }
public virtual Supplier Supplier { get; set; }
}
And your mappings would look like this:
public class SupplierMap : ClassMap<Supplier>
{
public SupplierMap()
{
Id(s => s.Id).GeneratedBy.Identity().Column("Id");
HasOne(s => s.SupplierAddress).PropertyRef(s => s.Supplier)
.Access.CamelCaseField()
.Cascade.All();
}
}
public class SupplierAddressMap : ClassMap<SupplierAddress>
{
public SupplierAddressMap()
{
Id(s => s.Id).GeneratedBy.Identity().Column("Id");
Map(s => s.AddressLine1).Column("AddressLine1");
References(s => s.Supplier).Column("SupplierId").Unique();
}
}
Note that when Supplier.SupplierAddress is set, the address's Supplier property is set.
I am using Nhibernate 3.2, along with a build of FluentNhibernate compatible with NH 3.2 and I have come to map a legacy part of my system. I believe it is possible to do what I require, but need some assistance in mapping it correctly.
I have a User Class below, with a list of Applications.
public class User
{
public virtual string UserID { get; set; }
public virtual int Id { get; set; }
public virtual IList<Application> Applications { get; set; }
}
I also have a Application class which has a foreign key back to the User Class which is not the "primary key". See Below
public class Application
{
// Not sure if this 'User' is needed?
public virtual User User { get; set; }
public virtual int IdFromUserTable { get; set; }
public virtual string ApplicationName { get; set; }
}
I have the corresponding ClassMaps, I think the UserMap is where the issue lies
UserMap
public class UserMap : ClassMap<User>
{
public UserMap()
{
Id(x => x.UserID);
HasMany(x => x.Applications)
.AsBag()
.KeyColumn("UserID")
.PropertyRef("Id")
.LazyLoad()
.Inverse();
Table("usertable");
ReadOnly();
}
}
ApplicationMap
public class ApplicationMap : ClassMap<Application>
{
public ApplicationMap()
{
CompositeId()
.KeyProperty(x => x.ApplicationName)
.KeyProperty(x => x.IdFromUserTable, "UserID");
Table("applicationstable");
}
}
The Table structures are as follows:
User Table
Primary Key - string, ColumnName = UserID
int (Identity) - int, ColumnName = Id
Applications Table
Composite Key - string, ColumnName = ApplicationName
and
int, ColumnName = UserId (references Id column in user table)
My question is how to get the mapping to work for the above scenario.
One caveat is: I cannot change the structure of the database, but I can change the classes / classmaps
Any help greatly appreciated..
PS - I did get this to work by adding in Fetch.Join in the HasMany userMap, but I would prefer to lazily evaluate the Application List when needed
Thanks, Mark
I've got a simple phone directory app using Fluent NHibernate 1.1. In the app, a "Person" object has many "PhoneNumber" objects. I'm trying to delete a Person and I want to cascade deletes to PhoneNumbers. I set a convention of DefaultCascade.All() after reading this answer. However, attempting to delete the parent object still throws an exception--it appears that NHibernate is trying to update the child table to set the parent ID to null instead of just deleting the record:
{"could not delete collection: [Person.PhoneNumbers#473][SQL: UPDATE phone_numbers SET person_id = null WHERE person_id = #p0]"}
InnerException:
{"Cannot insert the value NULL into column 'person_id', table 'directory.dbo.phone_numbers'; column does not allow nulls. UPDATE fails.\r\nThe statement has been terminated."}
My Fluent config is:
public static ISessionFactory CreateSessionFactory() {
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008
.ConnectionString(ConfigurationManager.ConnectionStrings[ConfigurationManager.AppSettings["activeConnStr"]].ConnectionString))
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<Person>()
.Conventions.Add(DefaultCascade.All())
)
.BuildSessionFactory();
}
The parent class is:
public class Person {
public Person() {
PhoneNumbers = new List<PhoneNumber>();
EmailAddresses = new List<string>();
}
public virtual int Id { get; private set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string Company { get; set; }
public virtual IList<PhoneNumber> PhoneNumbers { get; set; }
public virtual IList<string> EmailAddresses { get; set; }
}
The child class (PhoneNumber) is:
public class PhoneNumber {
public virtual string Number { get; set; }
public virtual PhoneNumberType NumberType { get; set; }
public virtual Person Person { get; set; }
}
My code to delete a person is:
public static void DeletePerson(int id) {
using (var session = Dalc.Instance.SessionFactory.OpenSession()) {
using (var trans = session.BeginTransaction()) {
session.Delete(session.Load<Person>(id));
trans.Commit();
}
}
}
What am I doing wrong?
I'm not sure about configuring the Fluent part, but I recently had the same problem with ActiveRecord.
You need to set your association, on the Person side, as Inverse = true.
From looking at the Getting Started documentation...
I belive, you need to set this when defining your HasMany relationship in Person. It should look something like this:
public PersonMap()
{
//<...SNIP...>
HasMany(x => x.PhoneNumbers)
.Inverse();
//<...SNIP...>
}
It works; Here is what each cascade option means:
none - do not do any cascades, let the users handles them by themselves.
save-update - when the object is saved/updated, check the associations and save/update any object that require it (including save/update the associations in many-to-many scenario).
delete - when the object is deleted, delete all the objects in the association.
delete-orphan - when the object is deleted, delete all the objects in the association. In addition to that, when an object is removed from the association and not associated with another object (orphaned), also delete it.
all - when an object is save/update/delete, check the associations and save/update/delete all the objects found.
all-delete-orphan - when an object is save/update/delete, check the associations and save/update/delete all the objects found. In additional to that, when an object is removed from the association and not associated with another object (orphaned), also delete it.
public class PersonMap : ClassMap<Person>
{
public PersonMap()
{
Table("Person");
Id(x => x.Id);
Map(x => x.Name);
HasMany<PhoneNumber>(x => x.PhoneNumberList)
.KeyColumn("PersonId")
.Cascade.All()
.Inverse().LazyLoad();
}
}