I'm using Entity Framework 4.3.1 Code-First and I need to split an entity between two tables. The tables have a primary key shared, and it is 1-to-1, but the columns are not named the same on each table.
I don't control the data layout, nor can I request any changes.
So for example, the SQL tables could be
And this would be my entity...
public class MyEntity
{
public int Id {get; set;}
public string Name {get;set}
public string FromAnotherTable {get;set;}
}
And here is the mapping I have.
public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
public MyEntityMapping()
{
this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
this.Property(e => e.Name).HasColumnName("MyDatabaseName");
this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
this.Map(m =>
{
m.Properties(e =>
{
e.Id,
e.Name
});
m.ToTable("MainTable");
});
this.Map(m =>
{
m.Properties(e =>
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
}
Since the key shared between them has a different column name, I'm not sure how to map it. This mapping will compile, but fails at runtime because EF emits SQL looking for the "ThePrimaryKeyId" column on the "ExtendedTable" table, which doesn't exist.
EDIT
To clarify, what I have defined above can (and does) work if the PK on the "ExtendedTable" followed naming conventions. But it doesn't and I can't change the schema.
Basically, what I need EF to emit is a SQL statement like
SELECT
[e1].*, /*yes, wildcards are bad. doing it here for brevity*/
[e2].*
FROM [MainTable] AS [e1]
INNER JOIN [ExtendedTable] AS [e2] /*Could be left join, don't care. */
ON [e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]
But the only thing it seems to want to emit is
SELECT
[e1].*,
[e2].*
FROM [MainTable] AS [e1]
INNER JOIN [ExtendedTable] AS [e2]
ON [e1].[ThePrimaryKeyId] = [e2].[ThePrimaryKeyId] /* this column doesn't exist */
Edit
I tried the 1-to-1 approach again at NSGaga's suggestion. It didn't work, but here are the results.
Entities
public class MyEntity
{
public int Id { get; set; }
public int Name { get; set; }
public virtual ExtEntity ExtendedProperties { get; set; }
}
public class ExtEntity
{
public int Id { get; set; }
public string AnotherTableColumn { get; set; }
public virtual MyEntity MainEntry { get; set; }
}
Here are the mapping classes
public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
public MyEntityMapping()
{
this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
this.Property(e => e.Name).HasColumnName("MyDatabaseName");
this.ToTable("MainTable");
this.HasKey(e => e.Id);
this.HasRequired(e => e.ExtendedProperties).WithRequiredPrincipal(f => f.MainEntry);
}
}
public class ExtEntityMapping : EntityTypeConfiguration<ExtEntity>
{
public ExtEntityMapping()
{
this.Property(e => e.Id).HasColumnName("NotTheSameName");
this.Property(e => e.AnotherTableColumn).HasColumnName("AnotherTableColumn");
this.ToTable("ExtendedTable");
this.HasKey(e => e.Id);
this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties);
}
}
This setup gets the message
"Column or attribute 'MyEntity_ThePrimaryKeyId' is not defined in 'ExtendedTable'"
Changing the final map line to
this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties).Map(m => M.MapKey("NotTheSameName"));
Returns this message
"Each property name in a type must be unique. property name 'NotTheSameName' was already defined."
Changing the mapped key to use the column from the parent table, MapKey("ThePrimaryKeyId"). returns this message
"Column or attribute 'ThePrimaryKeyId' is not defined in 'ExtendedTable'"
Removing the Id property from the ExtEntity class throws an error because then the entity doesn't have a defined key.
I have been working on this very issue for a few days, what I finally did was to set the column name of the Id field within the context of the mapping fragment. This way you can give the Id (or the foreign key dependent on the Id) a different name from the Id of the main table.
this.Map(m =>
{
m.Property(p => p.Id).HasColumnName("NotTheSameName");
m.Properties(e =>
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
If you run and debug this, you would find that it would give you something like what you want:
[e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]
I can't find anything that specifically states that the name of the column has to be the same in both tables; but neither can I find anything that says it doesn't, or explains how you would map that scenario. Every example I can find has the key with the same name in both tables. It looks to me like this is a hole in the DbContext design.
Move the HasColumnName to within the mapping:
this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
this.Map(m =>
{
m.Properties(e => new
{
e.Id,
e.Name
});
m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
m.Property(e => e.Name).HasColumnName("MyDatabaseName");
m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
m.ToTable("MainTable");
});
this.Map(m =>
{
m.Properties(e => new
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
}
No Visual Studio here, but try this with the 1-to-1 approach:
this.HasRequired(e => e.ExtendedProperties).HasConstraint((e, m) => e.Id == m.Id);
Update:
Here are some links that might help (could not find a real reference link)
How to declare one to one relationship using Entity Framework 4 Code First (POCO)
Entity Framework 4 CTP 4 Code First: how to work with unconventional primary and foreign key names
And just to provide (as I promised) a 1-to-1 (two entities, two tables) mapping, for what it's worth.
Here is what works for me and should in your case...
public class MainTable
{
public int ThePrimaryKeyId { get; set; }
public string Name { get; set; }
}
public class ExtendedTable
{
public int NotTheSameNameID { get; set; }
public string AnotherTableColumn { get; set; }
public MainTable MainEntry { get; set; }
}
public class MainDbContext : DbContext
{
public DbSet<MainTable> MainEntries { get; set; }
public DbSet<ExtendedTable> ExtendedEntries { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<MainTable>()
.HasKey(x => new { x.ThePrimaryKeyId });
modelBuilder.Entity<ExtendedTable>()
.HasKey(x => new { x.NotTheSameNameID });
// Extended To Main 1 on 1
modelBuilder.Entity<ExtendedTable>()
.HasRequired(i => i.MainEntry)
.WithRequiredDependent();
}
}
...and a test code something like...
using (var db = new UserDbContext())
{
foreach (var userid in Enumerable.Range(1, 100))
{
var main = new MainTable { Name = "Main" + userid };
db.MainEntries.Add(main);
var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, MainEntry = main };
db.ExtendedEntries.Add(extended);
}
int recordsAffected = db.SaveChanges();
foreach (var main in db.MainEntries)
Console.WriteLine("{0}, {1}", main.Name, main.ThePrimaryKeyId);
foreach (var extended in db.ExtendedEntries)
Console.WriteLine("{0}, {1}, {2}, {3}", extended.AnotherTableColumn, extended.NotTheSameNameID, extended.MainEntry.Name, extended.MainEntry.ThePrimaryKeyId);
}
That creates the following SQL script, tables...
CREATE TABLE [MainTables] (
[ThePrimaryKeyId] [int] NOT NULL IDENTITY,
[Name] [nvarchar](4000),
CONSTRAINT [PK_MainTables] PRIMARY KEY ([ThePrimaryKeyId])
)
CREATE TABLE [ExtendedTables] (
[NotTheSameNameID] [int] NOT NULL,
[AnotherTableColumn] [nvarchar](4000),
CONSTRAINT [PK_ExtendedTables] PRIMARY KEY ([NotTheSameNameID])
)
CREATE INDEX [IX_NotTheSameNameID] ON [ExtendedTables]([NotTheSameNameID])
ALTER TABLE [ExtendedTables] ADD CONSTRAINT [FK_ExtendedTables_MainTables_NotTheSameNameID] FOREIGN KEY ([NotTheSameNameID]) REFERENCES [MainTables] ([ThePrimaryKeyId])
And a note, as per our discussion above...
This ain't the 'splitting' - but
(a) code first IMO doesn't allow anything like that (I tried that first and also modifying the migrations manually but it's 'internally' all based on the expected column names being the same and there seems to be no way around it, for this version of EF at least.
(b) table structure wise - the tables could be made to look exactly what you need (as I said before I used it to relate the existing aspnet membership tables (which I could not change) into my user-table which has an own user-id pointing to outside/aspnet table and id.
True, you cannot make it using one C# model class - but the C# side is much more flexible and if you can control the C# that should give the same effect, to my opinion at least (like in the test, you can access it always through the extended entity, both extended and the main columns and they're always matched 1 to 1 and stay 'in sync'.
Hope this helps some
NOTE: you don't have to worry about the fk id etc. - just always access and add the Main entry via MainEntry, and id-s will be fine.
EDIT:
You could also do the following, to gain the appearance of having to deal with just one class (i.e. sort of a split)
public class ExtendedTable
{
public int NotTheSameNameID { get; set; }
public string AnotherTableColumn { get; set; }
public string Name { get { return MainEntry.Name; } set { MainEntry.Name = value; } }
// public int MainID { get { return MainEntry.ThePrimaryKeyId; } set { MainEntry.ThePrimaryKeyId = value; } }
internal MainTable MainEntry { get; set; }
public ExtendedTable()
{
this.MainEntry = new MainTable();
}
}
...and use it like this...
var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, Name = "Main" + userid };
...also you can revert the direction of the fk by doing the WithRequiredPrincipal instead of dependent.
(also all references have to be w/o 'virtual' if you have required one-to-one)
(and MainTable can be made 'internal' as it's here, so it's not visible from outside - it cannot be nested as that EF doesn't allow - is treated like NotMapped)
...well, that's the best I could do:)
I would like to suggest using some data annotations like this:
MainTable
---------
MainTableId
DatabaseName
ExtendedTable
----------
NotTheSameName
AnotherColumn
public class MainTable
{
[Key]
public int MainTableId { get; set; }
public string DatabaseName { get; set; }
[InverseProperty("MainTable")]
public virtual ExtendedTable ExtendedTable { get; set; }
}
public class ExtendedTable
{
[Key]
public int NotTheSameName { get; set; }
public string AnotherColumn { get; set; }
[ForeignKey("NotTheSameName")]
public virtual MainTable MainTable { get; set; }
}
Looks like it's been fixed in Entity Framework 6. See this issue http://entityframework.codeplex.com/workitem/388
I faced this issue, and solved by add Column attribute to match the both column names.
[Key]
[Column("Id")]
public int GroupId { get; set; }
Related
I am new at C# entity framework. I am trying to build an API, but stuck in retrieving data from relational table.
I have a pei_crops table in MS SQL database, where c_id is the primary key. I have another table called pei_pests, where p_id is the primary key. Another table is pei_cropspests where I have built relation for which pest attack which crop. Multiple pests can attack one crop and one pest can attack multiple crops. In this pei_cropspests table I have put p_id as primary and foreign key and c_id as primary and foreign key as well.
pei_crops table:
c_id
c_name
c_description
1
Corn
NULL
pei_pests table:
p_id
p_name
p_URL
1
pest1
NULL
2
pest2
NULL
pei_cropspests table:
p_id
c_id
1
1
2
1
Now In my API I want to show something like that
[
{
"cId":1,
"pests":[
{
"pId":1,
"pName": pest1,
"pURL": null
},
{
"pId":2,
"pName": pest2,
"pURL": null
}
]
}
]
My get request looks like this so far in C# web API project:
[Route("Getspecific/{cropId}")]
[HttpGet]
public async Task<IActionResult> GetSpecific(int cropId)
{
var cropDetails = await _db.PeiCrops.Where(c=>c.CId == cropId).Include(i=>i.PeiCropspests).ToListAsync();
return Ok(cropDetails);
}
This code returns me only the pID and URL of the pest that effects cID number 1. But I also want the pest name and URL along with their id.
Could someone please show me how to do it. Maybe there is some way to join two table and show the data? I just do not know how to do it in C#. Any help appreciated. Thank you.
Entities class:
PeiCrop:
using System;
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiCrop
{
public PeiCrop()
{
PeiCropimages = new HashSet<PeiCropimage>();
PeiCropsdiseases = new HashSet<PeiCropsdisease>();
PeiCropspests = new HashSet<PeiCropspest>();
}
public int CId { get; set; }
public string CName { get; set; }
public string CPhotoUrl { get; set; }
public string CDescription { get; set; }
public virtual ICollection<PeiCropimage> PeiCropimages { get; set; }
public virtual ICollection<PeiCropsdisease> PeiCropsdiseases { get; set; }
public virtual ICollection<PeiCropspest> PeiCropspests { get; set; }
}
}
PeiPest:
using System;
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiPest
{
public PeiPest()
{
PeiCropspests = new HashSet<PeiCropspest>();
PeiPestimages = new HashSet<PeiPestimage>();
}
public int PId { get; set; }
public string PName { get; set; }
public string PPhotoUrl { get; set; }
public string PDescription { get; set; }
public virtual ICollection<PeiCropspest> PeiCropspests { get; set; }
public virtual ICollection<PeiPestimage> PeiPestimages { get; set; }
}
}
PeiCropspest:
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiCropspest
{
public int PId { get; set; }
public int CId { get; set; }
public virtual PeiCrop CIdNavigation { get; set; }
public virtual PeiPest PIdNavigation { get; set; }
}
}
You're pretty close, but you're also not entirely using EF like you could, I mean you do not actually have to make the relationship table yourself but could refer directly to a list of the entity pei_pests from the entity pei_crop and let EF create the other.
//Example just getting one property from each,
//but you can new a composite return type up if you wish, using select
var cropDetails = await _db.PeiCrops
.Where(c=>c.CId == cropId)
.Include(i=>i.PeiCropspests)
.ThenInclucde(t => t.Pests)
.Select(s => new { CropId = s.p_id, PestName = s.PeiCropsPests.Pest.p_name })
.ToListAsync();
https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.select?view=net-5.0
First, you need to configure the relationships :
class MyContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PeiCropspest>()
.HasKey(cp => new { cp.PId, cp.CId });
//Configure one PeiPest to many PeiCropspest
modelBuilder.Entity<PeiCropspest>()
// Specify PeiCropspest's navigation property to one PeiPest
.HasOne(cp => cp.PIdNavigation)
// Specify PeiPest's navigaton property to many PeiCropspest
.WithMany(p => p.PeiCropspests)
// Specify PeiCropspest's navigation property
// to use this PeiCropspest's property as foreign key
.HasForeignKey(cp => cp.PId);
//Configure one PeiCrop to many PeiCropspest
modelBuilder.Entity<PeiCropspest>()
// Specify PeiCropspest's navigation shadow property to one PeiCrop
.HasOne<PeiCrop>()
// Specify PeiCrop's navigaton property to many PeiCropspest
.WithMany(c => c.PeiCropspests)
// Specify PeiCropspest's navigation shadow property
// to use this PeiCropspest's property as foreign key
.HasForeignKey(cp => cp.CId);
}
public DbSet<PeiCrop> PeiCrops { get; set; }
}
Then you can do a projection in the LINQ query :
public async Task<IActionResult> GetSpecific(int cropId)
{
var cropDetails = await _db.PeiCrops
.Where(c=>c.CId == cropId)
.Select(c => new {
cId = c.CId,
pests = c.PeiCropspests.Select(p => new {
pId = p.PIdNavigation.PId,
pName = p.PIdNavigation.PName,
pUrl = p.PIdNavigation.PPhotoUrl
})
})
.ToListAsync();
return Ok(cropDetails);
}
Do you know? From EF Core 5, it's possible to do many to many relationship without intermediary entity. This can simplify your entity model. cf. the documentation
I have a class for tracking attachments to a Record. Each Record can have multiple RecordAttachments, but there is a requirement that there can only be one RecordAttachment per-Record that is marked as IsPrimary.
public class RecordAttachment
{
public int Id { get; set; }
public int RecordId { get; set; }
public string Details { get; set; }
public bool IsPrimary { get; set; }
public Record Record { get; set; }
}
I can't just use .HasIndex(e => new { e.RecordId, e.IsPrimary }).IsUnique(true) because there can be multiple false values per Record.
Basically I need a unique constraint on RecordId and IsPrimary == true, although this didn't work:
entity.HasIndex(e => new { e.RecordId, IsPrimary = (e.IsPrimary == true) }).IsUnique(true)
Edit:
Looking at answers like this: Unique Constraint for Bit Column Allowing Only 1 True (1) Value it appears this would be possible creating the constraint directly with SQL, but then it wouldn't be reflected in my Model.
You can specify index filter using the HasFilter fluent API.
Unfortunately it's not database agnostic, so you have to use the target database SQL syntax and actual table column names.
For Sql Server it would be something like this:
.HasIndex(e => new { e.RecordId, e.IsPrimary })
.IsUnique()
.HasFilter("[IsPrimary] = 1");
or
.HasIndex(e => new { e.RecordId, e.IsPrimary })
.IsUnique()
.HasFilter($"[{nameof(RecordAttachment.IsPrimary)}] = 1");
For more information, see Relational Database Modeling - Indexes documentation topic.
I'm currently learning to work with Entity Framework by using it in a new MVC application. I had some struggles setting up a Many-To-Many relation between 2 tables, but got it working for showing data. However, when updating an entity, EF inserts duplicate records for the linked table.
My setup is as follows:
With BusinessUnit presenting an entity that groups WindowsLogins and WindowsGroups together for usage through the BusinessUnit. BusinessUnitWindowsLogin and BusinessUnitWindowsGroup serve as junction tables for the Many-To-Many relationship.
The entities are defined in C# as follows:
BusinessUnit
public class BusinessUnit
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public bool IsPersonal { get; set; }
public virtual IList<WindowsGroup> WindowsGroups { get; set; }
public virtual IList<WindowsLogin> WindowsLogins { get; set; }
}
WindowsGroup (WindowsLogin being similar)
public class WindowsGroup
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Domain { get; set; }
public string Name { get; set; }
public ICollection<BusinessUnit> BusinessUnits { get; set; }
}
I wrote this inside the OnModelCreating method of my DbContext to register the junction tables and foreign keys:
modelBuilder.Entity<BusinessUnit>()
.HasMany(businessUnit => businessUnit.WindowsGroups)
.WithMany(windowsGroup => windowsGroup.BusinessUnits)
.Map(mc =>
{
mc.MapLeftKey("BusinessUnitId");
mc.MapRightKey("WindowsGroupId");
mc.ToTable("BusinessUnitWindowsGroup", "Config");
});
modelBuilder.Entity<BusinessUnit>()
.HasMany(businessUnit => businessUnit.WindowsLogins)
.WithMany(windowsLogin => windowsLogin.BusinessUnits)
.Map(mc =>
{
mc.MapLeftKey("BusinessUnitId");
mc.MapRightKey("WindowsLoginId");
mc.ToTable("BusinessUnitWindowsLogin", "Config");
});
I wrote my update like this:
public void Update(BusinessUnit businessUnit)
{
var oldBusinessUnit = _unitOfWork.BusinessUnits.GetById(businessUnit.Id);
oldBusinessUnit.Name = businessUnit.Name;
oldBusinessUnit.WindowsGroups.Clear();
oldBusinessUnit.WindowsLogins.Clear();
oldBusinessUnit.WindowsGroups = businessUnit.WindowsGroups;
oldBusinessUnit.WindowsLogins = businessUnit.WindowsLogins;
_unitOfWork.Complete();
}
I had to clear both lists of WindowsGroups and WindowsLogins to correctly update the junction table, which now works. But as soon as I assign the new list of WindowsGroups or WindowsLogins, duplicate WindowsGroups or WindowsLogins are inserted by Entity Framework. The junction table is updated with the new Id's, so it looks correct in the application, but it's wrong in the database.
I'm open for any suggestions and feedback. Thank you in advance!
Reading a bit more into the change tracking that Entity Framework does to the entities, I figured out a way to solve my problem. I altered my update statement as follows:
public void Update(BusinessUnit businessUnit)
{
var oldBusinessUnit = _unitOfWork.BusinessUnits.GetById(businessUnit.Id);
oldBusinessUnit.Name = businessUnit.Name;
oldBusinessUnit.WindowsGroups.Clear();
oldBusinessUnit.WindowsLogins.Clear();
if (businessUnit.WindowsGroups != null)
{
var windowsGroupIds = businessUnit.WindowsGroups.Select(x => x.Id).ToList();
foreach (var winGroup in _unitOfWork.WindowsGroups.Find(winGroup => windowsGroupIds.Contains(winGroup.Id)).ToList())
{
oldBusinessUnit.WindowsGroups.Add(_unitOfWork.WindowsGroups.GetById(winGroup.Id));
}
}
if (businessUnit.WindowsLogins != null)
{
var windowsLoginIds = businessUnit.WindowsLogins.Select(x => x.Id).ToList();
foreach (var winLogin in _unitOfWork.WindowsLogins.Find(winLogin => windowsLoginIds.Contains(winLogin.Id)).ToList())
{
oldBusinessUnit.WindowsLogins.Add(_unitOfWork.WindowsLogins.GetById(winLogin.Id));
}
}
_unitOfWork.Complete();
}
I'll first check if both list actually contain something. Then I loop over the list that I got back from my view and use the Id's to fetch the entities directly from my context (via UoW). I don't know if it's the best solution, but it works.
Sorry, first let me say I did try to search this problem online and there are some but none specifically related to my scenario. I spent couple of hours and could not figure out.
I post my code below, stripping out all irrelevant information
First, here are my classes
public class StudentDm
{
public int Id { get; set; }
public virtual List<StudentParentDm> StudentParents { get; set; }
// other properties ...
}
// constructs a many to many relationship with some additional info in this model
public class StudentParentDm : EntityBaseDm
{
public int Id { get; set; }
public int StudentId { get; set; }
public virtual StudentDm Student { get; set; }
public int ParentId { get; set; }
public virtual ParentDm Parent { get; set; }
// other properties ...
}
public class ParentDm
{
public int Id { get; set; }
// other properties ...
}
Mappings:
public StudentMap()
{
HasMany(m => m.StudentParents).WithRequired().HasForeignKey(m => m.StudentId).WillCascadeOnDelete(false);
}
public StudentParentMap()
{
HasRequired(m => m.Student).WithMany().HasForeignKey(m => m.StudentId).WillCascadeOnDelete(false);
HasRequired(m => m.Parent).WithMany().HasForeignKey(m => m.ParentId).WillCascadeOnDelete(false);
}
public ParentMap()
{
HasMany(m => m.StudentParents).WithRequired().HasForeignKey(m => m.ParentId).WillCascadeOnDelete(false);
}
Then the code, here I am trying to create multiple new StudentParents, each with its own new Parent, to a student.
foreach (StudentParentDm studentParent in studentParents) // foreach new studentParent
{
StudentParentDm trackedStudentParent;
if (studentParent.Id == 0)
{
trackedStudentParent = new StudentParentDm
{
Parent = new ParentDm()
};
// map from studentParent to trackedStudentParent, including the Parent
// ...
trackedStudent.StudentParents.Add(trackedStudentParent);
} else
{
// unimportant
}
}
unitOfWork.Commit() // blows up with error message
Then I get this message:
Unable to determine the principal end of the 'Cobro.BusinessObjects.DatabaseContextServices.ParentDm_StudentParents' relationship. Multiple added entities may have the same primary key.
This only happens when I try to add more than 1 StudentParent at a time. I am not sure why the number of StudentParents would matter? I think I have the relationships set up correctly.
It also works with adding multiple StudentGrades, but the difference is that StudentGrade model is flat (does not have any child like StudentParent has a Parent)
Since nobody answered. Ill post what I discovered in case it can help anyone.
In StudentParentMap, add
m => m.Parent
such that
HasMany(m => m.StudentParents).WithRequired(m => m.Parent).HasForeignKey(m => m.ParentId).WillCascadeOnDelete(false);
Not sure why this mattered.. the mapping seemed sufficient enough such that the code-first generated database relationship did not change with this new addition. However, it was needed for EF to figure out how to configure FK during a multiple records adding transaction. It was pretty subtle for me.
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)