mapping nhibernate parent/child relationship the 'other' way round - c#

using FluentNhibernate;
I am trying to persist a seemingly simple object model:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Config Config { get; set; }
}
public class Config
{
public int ConfigId { get; set; }
public int ProductId { get; set; }
public string ConfigField1 { get; set; }
public string ConfigField2 { get; set; }
}
and the database looks like: (not syntactically correct)
CREATE TABLE [dbo].[Products](
[ProductId] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](50) NULL
)
CREATE TABLE [dbo].[Config](
[ConfigId] [int] IDENTITY(1,1) NOT NULL,
[ProductId] [int] NOT NULL,
[ConfigField1] [varchar](50) NULL,
[ConfigField2] [varchar](50) NULL
) ON [PRIMARY]
Out of the box fluent-nhibernate will try and map this as a foreign key on the Products table, eg:
INSERT INTO Products (Name, Config_id) VALUES (?, ?);
I don't want it to do this, rather I was hoping the mapping would insert Products first then the config second with ProductId being inserted into the Config table.
I've pulled my hair out trying overrides and reading links such as this and this but still can't get it to do what I want. I am working with existing data and code so I would rather not change the database table definitions or the domain object. There is a bit more going on than what this example paints so if we could avoid discussions on domain model design that would be great. I have a link to a spike of this project here (assumes database exists)
my current fluent mappings are:
public class ProductOverrides : IAutoMappingOverride<Product>
{
public void Override(AutoMapping<Product> mapping)
{
mapping.Id(x => x.Id).Column("ProductId");
mapping.Table("Products");
}
}
public class ConfigOverrides : IAutoMappingOverride<Config>
{
public void Override(AutoMapping<Config> mapping)
{
mapping.Id(x => x.ConfigId);
}
}

You are trying to map a one-to-one relationship as a one-to-many by mapping what would be the many side twice. That won't work. NHibernate is strict in its definition of one-to-one and requires that both sides have the same primary key, so that won't work either.
I've butted heads with this before and the best workaround I found is to model it as a standard on-to-many but only allow one item in the collection by encapsulating access to the it. In your case I would guess that Product would be the one side and Config the many.

I'm not sure if Config is used elsewhere but you could ignore ConfigId as its identity
public class Config
{
public int Id { get; set; }
public Product Product { get; set; }
public string ConfigField1 { get; set; }
public string ConfigField2 { get; set; }
}
public class ProductMap : ClassMap<Product>
{
public class ProductMap()
{
HasOne(p => p.Config);
}
}
public class ConfigMap : ClassMap<Config>
{
public class ConfigMap()
{
Id(c => c.Id, "ProductId").GeneratedBy.Foreign("Product");
References(c => c.Product, "ProductId");
Map(c => ...);
}
}

Another idea is to join and map as Component
public class ProductMap : ClassMap<Product>
{
public class ProductMap()
{
Join("Config", join =>
{
join.KeyColumn("ProductId");
join.Component(p => p.Config, c =>
{
c.Map(...);
});
}
}
}
Disadvantage is that you can not query Config directly, you have to query through Product

Related

there was an error running the selected code generator: Unable to retrieve MetaData for xxx.Goal.. being caused by a foreign key to application user

I am pretty new to asp.net MVC 5 and entityframework 6
I keep getting the above error when I try to create a controller with views
Here is my model I am trying to create a view for:
public class Goal
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int GoalId { get; set; }
[StringLength(120, ErrorMessage = "Max length is 120 and Min length is {0}", MinimumLength = 4)]
public string Title { get; set; }
public GoalStatus Status { get; set; }
public string OwnerUserId { get; set; }
public ApplicationUser OwnerUser { get; set; }
public List<GoalPlan> Plans { get; set; }
}
here is my dbcontext class code
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public virtual DbSet<Goal> Goals { get; set; }
public virtual DbSet<GoalPlan> GoalPlans { get; set; }
public virtual DbSet<GoalTask> GoalTasks { get; set; }
public virtual DbSet<LinkableItem> LinkableItems { get; set; }
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
}
when I comment out the OwnerUserId lines it works successfully.
The database is created.
I re-built the project each time I changed the code before creating the controller.
This also fails if I use the [ForeignKey("OwnerUser")] attribute on OwnerUserId.
Can anyone help me with why I am getting the metadata error?
id like to keep learning c# mvc.
EDIT...
so I changed OwnerUserId to int and that allowed the scaffolding to work. but the problem is not solved...
AspNetUsers is created with the following SQL by the code first initial code migration....
CREATE TABLE [dbo].[AspNetUsers] (
[Id] NVARCHAR (128) NOT NULL,
[Email] NVARCHAR (256) NULL,
[EmailConfirmed] BIT NOT NULL,
the line in the goal/index method is
var items = await db.Goals.Include(x => x.OwnerUser).ToListAsync();
in the view is
Html.DisplayFor(modelItem => item.OwnerUser.UserName)
and finally which is the issue, the goal table is created as follows
CREATE TABLE [dbo].[Goals] (
[GoalId] INT IDENTITY (1, 1) NOT NULL,
[Title] NVARCHAR (120) NULL,
[Status] INT NOT NULL,
[OwnerUserId] INT NOT NULL,
[OwnerUser_Id] NVARCHAR (128) NULL,
CONSTRAINT [PK_dbo.Goals] PRIMARY KEY CLUSTERED ([GoalId] ASC),
CONSTRAINT [FK_dbo.Goals_dbo.AspNetUsers_OwnerUser_Id] FOREIGN KEY
([OwnerUser_Id]) REFERENCES [dbo].[AspNetUsers] ([Id])
);
the value gets stored in OwnerUserId;
thanks again;
ok so here is the answer to my own question
the goal properties should be
[ForeignKey("OwnerUser")]
public string OwnerUserId { get; set; }
public ApplicationUser OwnerUser { get; set; }
as was in my original code, including the annotation, but Im not sure if its requried....
then the ApplicationDbContext was updated with the following fluent
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
modelBuilder.Entity<Goal>()
.HasRequired(x => x.OwnerUser)
.WithMany(x => x.Goals)
.HasForeignKey(x => x.OwnerUserId);
modelBuilder.Entity<GoalTask>()
.HasRequired(x => x.AssignedToUser)
.WithMany(x => x.GoalTasks)
.HasForeignKey(x => x.AssignedToUserId);
}
database table gets created correctly with a string.
The controller scaffolding works
The index method correctly displays the username.

Entity splitting with EF Code First issue

So I am trying to achieve entity splitting in EF 6.1 with Code First, and I am running into an error.
I have the following tables:
CREATE TABLE [dbo].[Organization]
(
[OrganizationId] INT NOT NULL PRIMARY KEY IDENTITY,
[TenantId] INT NOT NULL,
[Name] NVARCHAR(80) NOT NULL
)
CREATE TABLE [dbo].[OrganizationSettings]
(
[OrganizationSettingsId] INT NOT NULL PRIMARY KEY IDENTITY,
[OrganizationId] INT NOT NULL,
[AllowMultipleTimers] BIT NOT NULL,
CONSTRAINT [FK_OrganizationSettings_Organization] FOREIGN KEY (OrganizationId) REFERENCES Organization(OrganizationId)
)
With the following model objects:
public partial class Organization
{
public int OrganizationId { get; set; }
public int TenantId { get; set; }
public string Name { get; set; }
public OrganizationSettings Settings { get; set; }
}
public class OrganizationSettings
{
public int OrganizationSettingsId { get; set; }
public int OrganizationId { get; set; }
public bool AllowMultipleTimers { get; set; }
}
With the following config code:
var org = modelBuilder.Entity<Organization>();
org.Map(u =>
{
u.Properties(m => new { m.TenantId, m.Name });
})
.ToTable("Organization");
org.Map(u =>
{
u.Property(m => m.Settings.AllowMultipleTimers).HasColumnName("AllowMultipleTimers");
u.ToTable("OrganizationSettings");
});
Then just the following query:
context.Organizations.FirstOrDefault();
Which yields the following error:
The property 'Settings.AllowMultipleTimers' on type 'Organization'
cannot be mapped because it has been explicitly excluded from the
model or it is of a type not supported by the DbModelBuilderVersion
being used.
What am I doing wrong here?
Update: I forgot to mention that I created the database by hand, and am using the CF fluent API to map my models, rather than using "real" Code First.
While I was pretty sure I had this mapping working before, I went ahead and went a little different route.
First I got rid of the surrogate key on `OrganizationSettings (probably not strictly necessary), and then mapped it as an entity with a 1:1 relationship.
My OrganizationSettings is now:
public class OrganizationSettings
{
public int OrganizationId { get; set; }
public bool AllowMultipleTimers { get; set; }
}
OrganizationId is both a primary key and a foreign key.
And the config is:
var org = modelBuilder.Entity<Organization>()
.Map(u =>
{
u.Properties(m => new { m.TenantId, m.Name });
})
.HasRequired(m => m.Settings)
.WithRequiredPrincipal();
modelBuilder.Entity<OrganizationSettings>()
.HasKey(m => m.OrganizationId);
And this seems to work just fine. Since I'm not exposing a DbSet for OrganizationSettings it keeps the conceptual modeling of OrganizationSettings as a value object intact.
Were you trying to set up OrganizationSettings as a complex type while using entity splitting as well? Something like this, perhaps:
public partial class Organization
{
public int OrganizationId { get; set; }
public int TenantId { get; set; }
public string Name { get; set; }
public OrganizationSettings Settings { get; set; }
}
public class OrganizationSettings
{
public bool AllowMultipleTimers { get; set; }
}
// if you don't have a key defined on OrganizationSettings, this might not be needed
modelBuilder.ComplexType<OrganizationSettings>();
modelBuilder.Entity<Organization>()
.Map(u =>
{
u.Properties(m => new { m.OrganizationId, m.TenantId, m.Name });
u.ToTable("Organization");
})
.Map(u =>
{
u.Properties(m => new { m.OrganizationId, m.Settings.AllowMultipleTimers });
u.ToTable("OrganizationSettings");
// If you wanted to set the key column name
u.Property(m => m.OrganizationId).HasColumnName("OrganizationSettingsId");
});

Nhibernate creating object which has an object property

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.

NHibernate Mapping 1 Object To 2 Tables

We want to use NHibernate as our persistence layer in our application. We are also using Fluent NHibernate for the mappings.
We get Person data from a 3rd party and we need to save the data to our database. It works better in the code to have all properties on one object, but it makes more sense to save the data to 2 tables in our database.
Our object looks like this:
public class Person
{
public virtual long VersionNumber { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string IdentificationNumber { get; set; }
}
Our database tables look like this:
CREATE TABLE PersonVersion (
[PK] VersionNumber bigint NOT NULL,
[FK] PersonDemographicsId int NOT NULL
)
CREATE TABLE PersonDemographics (
[PK] PersonDemographicsId int NOT NULL,
IdentificationNumber nvarchar(9) NOT NULL,
FirstName nvarchar(100) NOT NULL,
LastName nvarchar(100) NOT NULL
)
When we receive new data, the version number can change, but the rest of the demographics could be the same. What we need NHibernate to do is save a new record to the PersonVersion table which links to the existing PersonDemographics record. If the demographics have changed, then we will create a new record in both tables. But most of the time, once we've downloaded the initial data, the demographics won't change as often as the version number. We need to keep track of all version numbers so that's why it's necessary to create a new PersonVersion record.
How would we accomplish this using NHibernate and mappings using Fluent NHibernate?
Also, as you can see, our Person object currently does not have a PersonDemographicsId because it is not needed by our application at all; it is just an ID for the table relationship which is needed in the database. In order to properly map this in NHibernate, do we have to add a PersonDemographicsId property on our Person object?
Thanks for the help!
This article http://ayende.com/blog/2327/multi-table-entities-in-nhibernate explains a way to map a single class to two tables in the database.
just an idea, maybe has to be tweaked
public class Person
{
public virtual int Id { get; set; }
internal protected virtual IList<long> VersionNumbers { get; set; }
public virtual long VersionNumber {
get { return VersionNumbers[VersionNumbers.Count - 1]; }
set { VersionNumbers.Add(value); }
}
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string IdentificationNumber { get; set; }
}
public class PersonMap : ClassMap<Person>
{
public PersonMap()
{
Table("PersonDemographics");
Id(p => p.Id, "PersonDemographicsId").GeneratedBy.Assigned();
Map(p => p.FirstName);
Map(p => p.LastName);
Map(p => p.IdentificationNumber);
HasMany(p => p.VersionRecord)
.Table("PersonVersion")
.KeyColumn("PersonDemographicsId")
.Element("VersionNumber")
.OrderBy("VersionNumber")
.Cascade.All();
}
}

NHibernate + Fluent NHibernate exception

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.

Categories

Resources