I have these classes:
public class Entity
{
public virtual Guid Id { get; set; }
public virtual string EntityName { get; set; }
public virtual IDictionary<string, Property> { get; set; }
// ...
}
public class Property
{
public virtual int? IntValue { get; set; }
public virtual decimal? DecimalValue { get; set; }
}
Is it possible to create Fluent NHibernate mappings so that executing the resulting schema will give these tables:
[Entities]
* Id : UNIQUEIDENTIFIER NOT NULL
* EntityName : NVARCHAR(50) NOT NULL
with a clustered index on "Id"
[Properties]
* EntityId : UNIQUEIDENTIFIER NOT NULL
* PropertyName : VARCHAR(50) NOT NULL
* IntValue : INT NULL
* DecimalValue : DECIMAL(12,6) NULL
with a clustered index on "EntityId" and "PropertyName"
Or do I need to change my classes?
An answer more verbose than yes/no will be much appreciated :)
Besides your clustered index which you would need to manually create, yes. Do you absolutely require FNH to generate your schema?
Why don't you just generate the schema yourself specific to your requirements and then map it accordingly.
(not tested or anything, written off the top of my head)
public class EntityMap : ClassMap<Entity>
{
public EntityMap()
{
Table("Entities");
Id(x => x.Id).GeneratedBy.GuidComb();
Map(x => x.Name).CustomSqlType("NVARCHAR").Length(50).Not.Nullable();
HasMany<Property>(x => x.Properties)
.Table("Properties")
.KeyColumn("PropertyName")
.Inverse()
.AsBag();
}
}
public class PropertyMap : ClassMap<Property>
{
public PropertyMap()
{
Table("Properties");
Id(x => x.Id).GeneratedBy.GuidComb();
Map(x => x.PropertyName).Length(50).Not.Nullable();
Map(x => x.IntValue);
Map(x => x.DecimalValue);
}
}
Here's the mapping that I have come up with:
public sealed class EntityMap : ClassMap<Entity>
{
public EntityMap()
{
Table("Entities");
Id(c => c.Id);
Map(c => c.EntityName).CustomSqlType("nvarchar(50)").Not.Nullable();
HasMany(c => c.Properties)
.KeyColumn("EntityId")
.AsMap<string>("PropertyName")
.Component(part =>
{
part.Map(x => x.IntValue);
part.Map(x => x.DecimalValue).Precision(12).Scale(6);
});
}
}
Schema generation yields this:
create table Entities (
Id UNIQUEIDENTIFIER not null,
EntityName nvarchar(50) not null,
primary key (Id)
)
create table Properties (
EntityId UNIQUEIDENTIFIER not null,
IntValue INT null,
DecimalValue DECIMAL(12, 6) null,
PropertyName INT not null,
primary key (EntityId, PropertyName)
)
alter table Properties
add constraint FK63646D8550C14DC4
foreign key (EntityId)
references Entities
Which is pretty much what I need, with an exception of the column order (minor issue) and PropertyName being nvarchar(255) instead of varchar(50) (something I actually care about).
Related
How can I Include the relationships of properties on a collection where the collection type is of a Table per Hierarchy design and the relationships are defined on the derived types with lazy loading disabled?
What I am trying to avoid is to declare the relationships on the base type or to have multiple DB calls to retrieve the relationships. Is there a way around this?
Attempts and Errors
I have tried using OfType but receive this message.
System.ArgumentException - The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties.
I have tried using as but receive this error
System.InvalidOperationException - A specified Include path is not valid. The EntityType 'Sandbox.TPHMcve.Person' does not declare a navigation property with the name 'Buildings'.
Example
Please do not focus on the business details of this silly example, it is an mcve to illustrate the error I am receiving. My actual project has nothing to do with schools / school systems etc.
Structure
A school in a University has Persons
A Person is either Student or Teacher
Student has zero or more course books on loan
Teacher can be assigned as the care taker / guardian for one or more campus buildings
Code
SchoolService.cs
This is the actual code of interest that produces the Exceptions
public sealed class SchoolService
{
private readonly UniversityDbContext _dbContext;
public SchoolService(UniversityDbContext dbContext)
{
_dbContext = dbContext;
}
public School GetSchoolAndPeopleWithDetails_OfType(int schoolId)
{
return _dbContext.Schools.Where(x => x.SchoolId == schoolId)
.Include(x => x.EntitiesOfSchool.OfType<Teacher>().Select(y => y.Buildings))
.Include(x => x.EntitiesOfSchool.OfType<Student>().Select(y => y.BooksOnLoan))
.SingleOrDefault();
}
public School GetSchoolAndPeopleWithDetails_Cast(int schoolId)
{
return _dbContext.Schools.Where(x => x.SchoolId == schoolId)
.Include(x => x.EntitiesOfSchool.Select(y => (y as Teacher).Buildings))
.Include(x => x.EntitiesOfSchool.Select(y => (y as Student).BooksOnLoan))
.SingleOrDefault();
}
}
Entities.cs
public sealed class School
{
public int SchoolId { get; set; }
public string Name { get; set; }
public List<Person> EntitiesOfSchool { get; set; }
}
public abstract class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public School PrimarySchool { get; set; }
public int SchoolId { get; set; }
}
public sealed class Teacher : Person
{
public ICollection<Building> Buildings { get; set; }
}
public sealed class Student : Person
{
public ICollection<CourseBook> BooksOnLoan { get; set; }
}
public sealed class Building
{
public int BuildingId { get; set; }
public string Name { get; set; }
public Teacher AssignedGuardian { get; set; }
public int GuardianId { get; set; }
}
public sealed class CourseBook
{
public int CourseBookId { get; set; }
public int BookNumber { get; set; }
public Student AssignedTo { get; set; }
public int? AssignedToId { get; set; }
}
EntityMappings.cs does not include Building or CourseBook mapping types as they are not relevant
public sealed class SchoolMap : EntityTypeConfiguration<School>
{
public SchoolMap()
{
ToTable("Schools");
HasKey(x => x.SchoolId);
Property(x => x.SchoolId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(x => x.Name).IsRequired().IsUnicode(true).HasMaxLength(200);
HasMany(x => x.EntitiesOfSchool).WithRequired(x => x.PrimarySchool).HasForeignKey(person => person.SchoolId);
}
}
public sealed class PersonMap : EntityTypeConfiguration<Person>
{
public PersonMap()
{
ToTable("Persons");
HasKey(x => x.PersonId);
Property(x => x.PersonId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(x => x.Name).IsRequired().IsUnicode(true).HasMaxLength(256);
Map<Teacher>(configuration => configuration.Requires("PersonType").HasValue(1));
Map<Student>(configuration => configuration.Requires("PersonType").HasValue(2));
}
}
public sealed class TeacherMap : EntityTypeConfiguration<Teacher>
{
public TeacherMap()
{
HasMany(x => x.Buildings).WithRequired(x => x.AssignedGuardian).HasForeignKey(x => x.GuardianId);
}
}
public sealed class StudentMap : EntityTypeConfiguration<Student>
{
public StudentMap()
{
HasMany(x => x.BooksOnLoan).WithOptional(x => x.AssignedTo).HasForeignKey(x => x.AssignedToId);
}
}
UniversityDbContext.cs
public sealed class UniversityDbContext : DbContext
{
public UniversityDbContext() : base("Name=default")
{
this.Configuration.LazyLoadingEnabled = false; // disable lazy loading
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new SchoolMap());
modelBuilder.Configurations.Add(new PersonMap());
modelBuilder.Configurations.Add(new TeacherMap());
modelBuilder.Configurations.Add(new StudentMap());
}
public DbSet<School> Schools { get; set; }
}
Sql code for table DDE and seed of data.sql
CREATE TABLE [dbo].[Schools](
[SchoolId] [int] IDENTITY(1,1) NOT NULL,
[Name] Nvarchar (200) not null
CONSTRAINT [PK_Schools] PRIMARY KEY CLUSTERED
(
[SchoolId] ASC
)) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Persons](
[PersonId] [int] IDENTITY(1,1) NOT NULL,
[SchoolId] [int] not null,
[PersonType] [int] not null,
[Name] nvarchar(256) not null
CONSTRAINT [PK_Persons] PRIMARY KEY CLUSTERED
(
[PersonId] ASC
)) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Buildings](
[BuildingId] [int] IDENTITY(1,1) NOT NULL,
[Name] nvarchar(200) not null,
[GuardianId] [int] not null,
CONSTRAINT [PK_Buildings] PRIMARY KEY CLUSTERED
(
[BuildingId] ASC
)) ON [PRIMARY]
GO
CREATE TABLE [dbo].[CourseBooks](
[CourseBookId] [int] IDENTITY(1,1) NOT NULL,
[BookNumber] varchar(200) not null,
[AssignedToId] [int] null,
CONSTRAINT [PK_CourseBooks] PRIMARY KEY CLUSTERED
(
[CourseBookId] ASC
)) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Persons] WITH CHECK ADD CONSTRAINT [FK_Schools_Persons] FOREIGN KEY([SchoolId])
REFERENCES [dbo].[Schools] ([SchoolId])
ALTER TABLE [dbo].[Buildings] WITH CHECK ADD CONSTRAINT [FK_Persons_Buildings] FOREIGN KEY([GuardianId])
REFERENCES [dbo].[Persons] ([PersonId])
ALTER TABLE [dbo].[CourseBooks] WITH CHECK ADD CONSTRAINT [FK_Persons_CourseBooks] FOREIGN KEY([AssignedToId])
REFERENCES [dbo].[Persons] ([PersonId])
INSERT INTO Schools (Name) values (N'Business'), (N'Education')
INSERT INTO Persons (Name, PersonType, SchoolId) values ('Eddy',1, 1), ('Fran',1, 1), ('Joe',2, 1), ('Kim',2, 1)
INSERT INTO Buildings (Name, GuardianId) values (N'Offsite staff', 1), (N'Main Business Building', 1)
INSERT INTO CourseBooks(AssignedToId, BookNumber) values (3, 'Course A book 1'),(3, 'Course C book 31')
I just started learning C# and Nhibernate. I'm trying to solve following problem for hours.
Can anyone see the problem?
Table Line:
CREATE TABLE [dbo].[Line]
(
[ID] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
[Name] NVARCHAR (45) NOT NULL,
[ID_Color] UNIQUEIDENTIFIER NULL,
PRIMARY KEY CLUSTERED ([ID] ASC),
UNIQUE NONCLUSTERED ([Name] ASC),
FOREIGN KEY ([ID_Color])
REFERENCES [dbo].[Line_Color] ([ID]) ON DELETE SET NULL
);
Table Line_Color:
CREATE TABLE [dbo].[Line_Color]
(
[ID] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
[Color] NVARCHAR (45) NOT NULL,
PRIMARY KEY CLUSTERED ([ID] ASC),
UNIQUE NONCLUSTERED ([Color] ASC)
);
Entity.cs:
public class Entity<T> where T : Entity<T>
{
public virtual Guid Id { get; set; }
private int? _oldHashCode;
public override Boolean Equals(object obj)
{
var other = obj as T;
if (other == null)
return false;
// handle the case of comparing two NEW objects
var otherIsTransient = Equals(other.Id, Guid.Empty);
var thisIsTransient = Equals(Id, Guid.Empty);
if (otherIsTransient && thisIsTransient)
return ReferenceEquals(other, this);
return other.Id.Equals(Id);
}
public override Int32 GetHashCode()
{
if (_oldHashCode.HasValue)
return _oldHashCode.Value;
var thisIsTransient = Equals(Id, Guid.Empty);
if (thisIsTransient)
{
_oldHashCode = base.GetHashCode();
return _oldHashCode.Value;
}
return Id.GetHashCode();
}
public static Boolean operator ==(Entity<T> x, Entity<T> y)
{
return Equals(x, y);
}
public static Boolean operator !=(Entity<T> x, Entity<T> y)
{
return !(x == y);
}
}
Line.cs:
public class Line : Entity<Line>
{
public virtual String Name { get; set; }
public virtual Color Color { get; set; }
}
LineMap.cs:
public class LineMap : ClassMap<Line>
{
public LineMap()
{
Id(x => x.Id);
Map(x => x.Name);
References(x => x.Color);
}
}
Color.cs:
public class Color : Entity<Color>
{
public virtual String ColorS { get; set; }
}
ColorMap.cs:
public class ColorMap : ClassMap<Color>
{
public ColorMap()
{
Id(x => x.Id);
Map(x => x.ColorS);
}
}
On every query I do, I get something like
An unhandled exception of type
'NHibernate.Exceptions.GenericADOException' occurred in NHibernate.dll
Additional information: could not execute query
or NULL.
There are 2 problems in your code:
An entity is named Color and the corresponding table Line_Color;
A property is named ColorS and the corresponding column Color.
They must correspond, or you have to tell NHibernate the DB names. Do some renaming (preferably) or adjust your mapping.
ColorMap.cs:
public class ColorMap : ClassMap<Color>
{
public ColorMap()
{
Table("Line_Color");
Id(x => x.Id);
Map(x => x.ColorS).Column("Color");
}
}
Entities represents tables (or table likes).
You don't need an table (and entity/map) for "color".
You don't need to have a class for "Color".
Line need to have a property and a map for property "color".
The problem Hibernate acuses is because it can't match the Entity color to any table.
I have these tables/classes (example):
table FirstTable (
Id INTEGER NOT NULL DEFAULT AUTOINCREMENT,
Name VARCHAR(100) NOT NULL,
Document VARCHAR(20) NOT NULL
)
table SecondTable (
Id INTEGER NOT NULL,
Something VARCHAR(100) NULL,
FOREIGN KEY (Id) REFERENCES FirstTable (Id)
)
public class FirstClass {
public string Name { get; set; }
public string Document { get; set; }
public SecondClass SecondClass { get; set; }
}
public class SecondClass {
public string Something { get; set; }
public FirstClass FirstClass { get; set; }
}
The mapping is:
public class FirstClassMap : ClassMap<FirtsClass> {
Table("FirstTable");
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.Name);
Map(x => x.Document);
References(x => x.SecondClass, "Id").ForeignKey();
}
public class SecondClassMap : ClassMap<SecondClass> {
Table("SecondTable");
Id(x => x.Id).GeneratedBy.Foreign("FirstClass");
Max(x => x.Something);
HasOne(x => x.FirstClass).PropertyRef(x => x.SecondClass).Cascade.SaveUpdate();
}
FirstClass can have (0,1) SecondClass, and SecondClass can have (1,1) FirstClass.
The bellow code return the error "attempted to assign id from null one-to-one property: SecondClass"
var test = new SecondClass();
test.FirstClass = new FirstClass();
test.Something = "New test";
test.FirstClass.Name = "My name";
test.FirstClass.Document = "My document";
// ... commands to save.
It seems like NH is trying to save SecondClass first and fails to grab a generated ID from the not yet saved FirstClass.
Try to move the .Cascade.SaveUpdate() to the References declaration in FirstClassMap and call the save command on FirstClass.
I'm trying to generate db schema using fluent nhibernate, nhibernate 3.0, spring.net 1.3.1 and SQLite. The create/update script generated by NHibernate is
create table LogEntries (Id UNIQUEIDENTIFIER not null, Hostname TEXT not null, LoggerName TEXT not null, LogLevel INTEGER not null, Message TEXT not null, primary key (Id))
create table Properties (Id INTEGER not null, Key TEXT, Value TEXT, LogEntry_id UNIQUEIDENTIFIER, Index INTEGER, primary key (Id))
But it fails with the following error
System.Data.SQLite.SQLiteException: SQLite error
near "Index": syntax error
The entities:
public class LogEntry
{
public virtual Guid Id { get; set; }
public virtual string LoggerName { get; set; }
public virtual string Message { get; set; }
public virtual int LogLevel { get; set; }
public virtual string Hostname { get; set; }
public virtual IList<Property> Properties { get; set; }
}
public class Property
{
public virtual int Id { get; set; }
public virtual string Key { get; set; }
public virtual string Value { get; set; }
}
And the mapping classes
public class LogEntryMap : ClassMap<LogEntry>
{
public LogEntryMap()
{
Table("LogEntries");
Id(x => x.Id).GeneratedBy.GuidComb();
Map(x => x.Hostname).Not.Nullable();
Map(x => x.LoggerName).Not.Nullable();
Map(x => x.LogLevel).Not.Nullable();
Map(x => x.Message).Not.Nullable();
HasMany(x => x.Properties).Cascade.AllDeleteOrphan().AsList();
}
}
public class PropertyMap : ClassMap<Property>
{
public PropertyMap()
{
Table("Properties");
Id(x => x.Id).GeneratedBy.Increment();
Map(x => x.Key);
Map(x => x.Value);
}
}
I'm currently learning NHibernate myself (reading NHibernate 3.0 Cookbook), so in no way am I an expert.
I have the same problem at the moment, having a HasMany-map Parent.Children in an SQLite environment. This also crashes on the Index syntax error.
From what I've managed to deduce, Index is a reserved keyword (isn't it in almost every RDBMS?). It seems these keywords are not escaped by default, and hence, the SQL-script is invalid.
According to the book, you can escape the columnnames by adding a backtick to the column-name:
HasMany(x => x.Children).Cascade.AllDeleteOrphan().AsList(p => p.Column("`Index"));
However, even though this "works", it generates the following SQL-query, which seems to have dropped the x:
create table Child (
Id INTEGER not null,
ChildType TEXT not null,
Version INTEGER not null,
Content TEXT,
Title TEXT not null,
Parent_id INTEGER,
"Inde" INTEGER,
primary key (Id)
)
So, either consider:
specifying a custom index columnname which isn't a keyword,
rely on the backtick auto-escape (no clue what's happening here, no time to check)
use a different collection type if you don't actually need an ordered list. See List vs Set vs Bag in NHibernate
Question: is there a way to map a single foreign key to a number of mutually exclusive tables, based on a context?
Background...
In my specific example, I have the following domain graph, representing an insurance claim which can be against a vehicle or property:
public enum InvolvedPartyContext
{
Vehicle = 1,
Property = 2
}
public class Claim
{
public virtual Guid Id { get; set; }
public virtual InvolvedPartyContext InvolvedPartyContext { get; set; }
public virtual Vehicle Vehicle { get; set; } // set if Context = Vehicle
public virtual Property Property { get; set; } // set if Context = Property
}
public class Vehicle { //... }
public class Property { //... }
The SQL looks like this (notice the single foreign key InvolvedPartyId):
CREATE TABLE Claims (
Id uniqueidentifier NOT NULL,
InvolvedPartyContext int NOT NULL,
InvolvedPartyId uniqueidentifier NOT NULL
)
CREATE TABLE Vehicles (
Id uniqueidentifier NOT NULL,
Registration varchar(20) NOT NULL
)
CREATE TABLE Properties (
Id uniqueidentifier NOT NULL,
PostCode varchar(20) NOT NULL
)
The Fluent NHibernate mapping file for Claim:
public ClaimMap()
{
Id(x => x.Id);
Map(x => x.InvolvedPartyContext).CustomTypeIs(typeof(InvolvedPartyContext));
References(x => x.Vehicle, "InvolvedPartyId");
References(x => x.Property, "InvolvedPartyId");
}
This throws an "Invalid index {n} for this SqlParameterCollection with Count {m}" exception, since the same field (InvolvedPartyId) is mapped twice. A simple fix would be to create VehicleId and PropertyId fields, but in the real world there are many more contexts, so this isn't very flexible.
Personally, I wouldn't go with the design you have. Instead I'd create subclasses of your Claim class, VehicleClaim and PropertyClaim respectively.
public class VehicleClaim : Claim
{
public virtual Vehicle Vehicle { get; set; }
}
Then change your mappings to use your InvolvedPartyContext column as a discriminator (the column which NHibernate uses to determine which class the row represents), and create subclass mappings for each subclass.
public class ClaimMap : ClassMap<Claim>
{
public ClaimMap()
{
Id(x => x.Id);
DiscriminateSubClassesOnColumn("InvolvedPartyContext");
}
}
public class VehicleClaimMap : SubclassMap<VehicleClaim>
{
public VehicleClaimMap()
{
DiscriminatorValue(1);
References(x => x.Vehicle);
}
}
If you really do want to run with what you've got, you should look into the any mappings; there isn't a lot of documentation on them, but you use the ReferencesAny method.