I'm trying to use some classes from another assembly in my own project as entities that I can persist using EF7, rather than writing a series of very similar classes that are more database-friendly.
Simplified versions look like this:
interface IMediaFile
{
string Uri { get; }
string Title { get; set; }
}
class CMediaFile : IMediaFile
{
public CMediaFile() { }
public string Uri { get; set; }
public string Title { get; set; }
}
//The following types are in my project and have full control over.
interface IPlaylistEntry
{
IMediaFile MediaFile { get; }
}
class CPlaylistEntry<T> : IPlaylistEntry where T : IMediaFile
{
public CPlaylistEntry() { }
public T MediaFile { get; set; }
}
There are multiple implementations of IMediaFile, I am showing only one. My PlaylistEntry class takes a generic argument to enable different traits for those various implementations, and I just work with the IPlaylistEntry.
So I've started to model it like so:
var mediaFile = _modelBuilder.Entity<CMediaFile>();
mediaFile.Key(e => e.Uri);
mediaFile.Index(e => e.Uri);
mediaFile.Property(e => e.Title).MaxLength(256).Required();
var mediaFilePlaylistEntry = _modelBuilder.Entity<CPlaylistEntry<CMediaFile>>();
mediaFilePlaylistEntry.Key(e => e.MediaFile);
mediaFilePlaylistEntry.Reference(e => e.MediaFile).InverseReference();
As a simple test, I ignore the CPlaylistEntry<> and just do:
dbContext.Set<CMediaFile>().Add(new CMediaFile() { Uri = "irrelevant", Title = "" });
dbContext.SaveChanges()
This throws:
NotSupportedException: The 'MediaFile' on entity type 'CPlaylistEntry' does not have a value set and no value generator is available for properties of type 'CMediaFile'. Either set a value for the property before adding the entity or configure a value generator for properties of type 'CMediaFile'`
I don't even understand this exception, and I don't see why CPlaylistEntry is appearing when I'm only trying to store a CMediaFile entity. I'm guessing this is related to my model definition - specifically defining the primary key of the CPlaylistEntry as not a simple type, but a complex type - another entity. However I would expect EF to be smart enough to work out that it all boils down to a string Uri, because that complex type has its own primary key declared already, and I have declared the property as a foreign key to that type.
Is it possible to model these classes in EF without radically redesigning them to look closer to what corresponding database tables might be? I've worked with EF6 database-first in the past, so this is my first attempt into a code-first pattern, and I'm really hoping that I can isolate the mess that a database might look like to just my model definition, and keep "clean" classes that I interact with in .NET.
If more explanation of these types and their relationship is required, just ask - I'm attempting to keep this brief.
Doubt this is currently supported (unsure if it eventually will or not).| I've tried to recreate your model with slight changes and when trying to create the database I get:
System.NotSupportedException: The property 'PlaylistEntry`1MediaFile'
cannot be mapped because it is of type 'MediaFile' which is currently
not supported.
Update 1
I think that the fact that you are putting MediaFile as a key is creating problems. I've done a few changes to your model. I hope this will not break anything negative on your end:
public interface IPlaylistEntry<T>
where T : IMediaFile
{
T MediaFile { get; set; }
}
public class PlaylistEntry<T> : IPlaylistEntry<T>
where T : IMediaFile
{
public int Id { get; set; }
public string PlaylistInfo { get; set; } //added for testing purposes
public T MediaFile { get; set; }
}
Mappings:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ForSqlServer().UseIdentity();
builder.Entity<MediaFile>().ForRelational().Table("MediaFiles");
builder.Entity<MediaFile>().Key(e => e.Uri);
builder.Entity<MediaFile>().Index(e => e.Uri);
builder.Entity<MediaFile>().Property(e => e.Title).MaxLength(256).Required();
builder.Entity<PlaylistEntry<MediaFile>>().ForRelational().Table("MediaFileEntries");
builder.Entity<PlaylistEntry<MediaFile>>().Key(e => e.Id);
builder.Entity<PlaylistEntry<MediaFile>>().Reference(e => e.MediaFile).InverseReference();
}
Usage:
var mediaFile = new MediaFile() {Uri = "irrelevant", Title = ""};
context.Set<MediaFile>().Add(mediaFile);
context.SaveChanges();
context.Set<PlaylistEntry<MediaFile>>().Add(new PlaylistEntry<MediaFile>
{
MediaFile = mediaFile,
PlaylistInfo = "test"
});
context.SaveChanges();
This works and saves the correct data to the database.
You can retrieve the data using:
var playlistEntryFromDb = context.Set<PlaylistEntry<MediaFile>>()
.Include(plemf => plemf.MediaFile).ToList();
Update 2
Since you do not want to have an identity as key, you can add a Uri property to your playlistentry class that will be used for the relationship between PlaylistEntry and MediaFile.
public class PlaylistEntry<T> : IPlaylistEntry<T>
where T : IMediaFile
{
public string Uri { get; set; }
public string PlaylistInfo { get; set; }
public T MediaFile { get; set; }
}
Here is what the mapping in this case would look like:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<MediaFile>().ForRelational().Table("MediaFiles");
builder.Entity<MediaFile>().Key(e => e.Uri);
builder.Entity<MediaFile>().Index(e => e.Uri);
builder.Entity<MediaFile>().Property(e => e.Title).MaxLength(256).Required();
builder.Entity<PlaylistEntry<MediaFile>>().ForRelational().Table("MediaFileEntries");
builder.Entity<PlaylistEntry<MediaFile>>().Key(e => e.Uri);
builder.Entity<PlaylistEntry<MediaFile>>().Reference(e => e.MediaFile).InverseReference().ForeignKey<PlaylistEntry<MediaFile>>(e => e.Uri);
}
Usage to insert data stays the same:
var mediaFile = new MediaFile() { Uri = "irrelevant", Title = "" };
context.Set<MediaFile>().Add(mediaFile);
context.SaveChanges();
context.Set<PlaylistEntry<MediaFile>>().Add(new PlaylistEntry<MediaFile>
{
MediaFile = mediaFile,
PlaylistInfo = "test"
});
context.SaveChanges();
This code above will put "irrelevant" in the PlaylistEntry Uri property since it is used as the foreign key.
And to retrieve data:
var mediaFiles = context.Set<PlaylistEntry<MediaFile>>().Include(x => x.MediaFile).ToList();
The join will occur on the Uri field in both tables.
Related
This may seem like a duplicate question EF Core One-to-Many relationship list returns null, but the answer to that question didn't help me. My situation:
public class Section
{
public int Id { get; set; }
// ...
public IEnumerable<Topic> Topics { get; set; }
}
public class Topic
{
public int Id { get; set; }
// ...
public int SectionId { get; set; }
public Section Section { get; set; }
}
But I have not implemented the OnModelCreating method in DbContext because in that case, errors occurs with users identity. There are topics in the database with the specified SectionId. But no matter how I try to get the section, I get null in the Topics property. For example:
var section = _dbContext.Sections.Include(s => s.Topics).FirstOrDefault(s => s.Id == id);
What is the reason for this problem? Have I declared something wrong? Or maybe there is a problem in creating a topic?
UPDATE
I tried to override the OnModelCreating method this way:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Section>()
.HasMany(s => s.Topics)
.WithOne(t => t.Section);
base.OnModelCreating(modelBuilder);
}
And this way:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Section>()
.HasMany(s => s.Topics)
.WithOne(t => t.Section)
.HasForeignKey(prop => prop.SectionId);
base.OnModelCreating(modelBuilder);
}
I also added the virtual attribute to the dependencies again. It did not help. Added a test migration (thought there might be something wrong with the database structure), but the migration was empty.
SOLUTION
As a result, I solved the problem with a crutch:
var section = _dbContext.Sections.Include(s => s.Topics).FirstOrDefault(s => s.Id == id);
if (section == null)
{
return Error();
}
section.Topics = _dbContext.Topics.Where(t => t.SectionId == section.Id).Include(t => t.Author).ToList();
foreach(var topic in section.Topics)
{
topic.Author = _dbContext.Users.FirstOrDefault(u => u.Id == topic.AuthorId);
topic.Posts = _dbContext.Posts.Where(t => t.TopicId == topic.Id).ToList();
}
As you can see, I had to explicitly get data from the dbContext and assign them to the appropriate properties. Include method calls can be deleted because they do not perform the desired action.
Several suggestions:
Try to make your Section define Topics as ICollection rather than IEnumerable and virtual so that they can be lazy loaded if necessary
public class Section
{
public int Id { get; set; }
// ...
public virtual ICollection<Topic> Topics { get; set; }
}
In your solution sample, you should be able to leverage EF Core's ThenInclude method to get the Sections, Topics, and Authors in one fell swoop:
var section = _dbContext.Sections
.Include(s => s.Topics)
.ThenInclude(t => t.Authors)
.FirstOrDefault(s => s.Id == id);
However to get the collection of authors and posts in the same child collection, you may want to consider a custom projection into a new type. EF Core 3.1 should piece all of this into a single query. Earlier versions of Core would break it apart into multiple database queries depending on the version and exact syntax you used. Something like:
var items =
from section in _dbContext.Sections
from topic in section.Topics
select new {
section.Name,
topic.Description,
Authors = topic.Authors.ToList(),
Posts = topic.Posts.ToList()
};
Following the guide on this link https://www.learnentityframeworkcore.com/lazy-loading
Install the Microsoft.EntityFrameworkCore.Abstractions package into the project containing your model classes:
[Package Manager Console]
install-package Microsoft.EntityFrameworkCore.Abstractions
[Dotnet CLI]
add package Microsoft.EntityFrameworkCore.Abstractions
Alter the principal entity to include
a using directive for Microsoft.EntityFrameworkCore.Infrastructure
a field for the ILazyLoader instance
an empty constructor, and one that takes an ILazyLoader as a parameter (which can be private, if you prefer)
a field for the collection navigation property
a getter in the public property that uses the ILazyLoader.Load method
using Microsoft.EntityFrameworkCore.Infrastructure;
public class Author
{
private readonly ILazyLoader _lazyLoader;
public Author()
{
}
public Author(ILazyLoader lazyLoader)
{
_lazyLoader = lazyLoader;
}
private List<Book> _books;
public int AuthorId { get; set; }
public List<Book> Books
{
get => _lazyLoader.Load(this, ref _books);
set => _books = value;
}
}
The solution in the answer will not work in case of many-to-many relationship.
It is just an illustratory example, I understand the relations in this example do not make sense perse, but it plots relations in a way I need the solution. So please do not comment about that.
I am searching for a solution in which I can ignore saving a navigational property;
public class ClassRoom {
public Guid Id { get; set; }
public Guid? ClassRoomInformationId { get; set; }
public virtual ClassRoomInformation { get; set; }
public virtual Collection<Student> Students { get; set;
}
public class Student {
public Guid Id { get; set; }
public Guid? ClassRoomId { get; set; }
public Guid? StudentInformationId { get; set; }
public virtual StudentInformation { get; set; }
}
public class StudentEntityConfiguration : IEntityTypeConfiguration<Student> {
public void Configure(EntityTypeBuilder<Student> builder) {
builder.ToTable("Student");
builder.HasKey(s => s.Id);
builder.HasOne(s => s.StudentInformation)
.WithOne()
.HasForeignKey<Student>(s => s.StudentInformationId);
}
}
public class ClassRoomEntityConfiguration : IEntityTypeConfiguration<ClassRoom> {
public void Configure(EntityTypeBuilder<ClassRoom> builder) {
builder.ToTable("ClassRoom");
builder.HasKey(c => c.Id);
builder.HasOne(c => c.ClassRoomInformation)
.WithOne()
.HasForeignKey<ClassRoom>(c => c.ClassRoomInformationId);
builder.HasMany(c => c.Students)
.WithOne()
.HasForeignKey(c => c.ClassRoomInformation);
}
}
To clearify my question (Using EF 2.2); I want to update the student through it's own StudentRepository. And when I save a classroom through the ClassRoomRepository and the student might change in any way, I do not want that change to be persisted (even though it is included to be able to 'view' the data).
I have tried to add the following to the ClassRoomEntityConfiguration:
//BeforeSaveBehavior neither works
builder.Property(c => c.Students).Metadata.AfterSaveBehavior = PropertySaveBehavior.Ignore;
However this gives the following exception:
... Cannot be used as a property on ... because it is configured as a navigation.'
Another thing I tried is setting the componentmodel readonly attribute on the Students list in the ClassRoom. This seems to be ignored as well.
I call this the goldilocks problem. You have a hierarchy of objects (Customer, Order, OrderDetails) and you only want to save at "just the right level" of the object-graph.
A work around is to load the object......change only thing things that you care about, then save it.
In the below, I am NOT saving the inputItem.
I am using inputItem to set a small subset of the values of the foundEntity.
public async Task<MyThing> UpdateAsync(MyThing inputItem, CancellationToken token)
{
int saveChangesAsyncValue = 0;
MyThing foundEntity = await this.entityDbContext.MyThings.FirstOrDefaultAsync(item => item.MySurrogateKey == inputItem.MySurrogateKey, token);
if (null != foundEntity)
{
/* alter JUST the things i want to update */
foundEntity.MyStringPropertyOne = inputItem.MyStringPropertyOne;
foundEntity.MyStringPropertyTwo = inputItem.MyStringPropertyTwo;
this.entityDbContext.Entry(foundEntity).State = EntityState.Modified;
saveChangesAsyncValue = await this.entityDbContext.SaveChangesAsync(token);
/* an exception here would suggest another process changed the "context" but did not commit the changes (usually by SaveChanges() or SaveChangesAsync() */
if (1 != saveChangesAsyncValue)
{
throw new ArgumentOutOfRangeException(string.Format("The expected count was off. Did something else change the dbcontext and not save it? {0}", saveChangesAsyncValue), (Exception)null);
}
}
else
{
ArgumentOutOfRangeException argEx = new ArgumentOutOfRangeException(string.Format(" SAD FACE {0} ", entity.MyThingKey), (Exception)null);
this.logger.LogError(argEx);
throw argEx;
}
return foundEntity;
}
SIDE NOTE:
2.2 is no longer supported (see link below). Dot Net Core 2.2 End of Lifetime is listed as "December 23, 2019"
You should upgrade to 3.1 or downgrade to 2.1. (downgrading is counter intuitive I know).
See
https://dotnet.microsoft.com/platform/support/policy/dotnet-core
I'm trying to create an entity object that has many to many relationships with other entities. The relationships are indicated as follows.
public class Change {
// Change Form Fields
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ChangeId { get; set; }
public string ChangeTitle { get; set; }
public string ChangeType { get; set; }
public DateTime DateSubmitted { get; set; }
public DateTime TargetDate { get; set; }
//Many to Many Collections
public virtual ICollection<Change_CriticalBankingApp> Change_CriticalBankingApps { get; set; } = new List<Change_CriticalBankingApp>();
public virtual ICollection<Change_ImpactedBusiness> Change_ImpactedBusinesses { get; set; } = new List<Change_ImpactedBusiness>();
public virtual ICollection<Change_ImpactedService> Change_ImpactedServices { get; set; } = new List<Change_ImpactedService>();
public virtual ICollection<Change_TestStage> Change_TestStages { get; set; } = new List<Change_TestStage>();
public virtual ICollection<Change_TypeOfChange> Change_TypeOfChanges { get; set; } = new List<Change_TypeOfChange>();
And the DbContext set up is as follows
public class ChangeContext : DbContext {
public ChangeContext(DbContextOptions<ChangeContext> options) : base(options) {
Database.Migrate();
}
public DbSet<Change> Change { get; set; }
public DbSet<TestStage> TestStage { get; set; }
public DbSet<TypeOfChange> TypeOfChange { get; set; }
public DbSet<CriticalBankingApp> CriticalBankingApp { get; set; }
public DbSet<ImpactedBusiness> ImpactedBusiness { get; set; }
public DbSet<ImpactedService> ImpactedService { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Change_CriticalBankingApp>().HasKey(t => new { t.ChangeId, t.CriticalBankingAppId });
modelBuilder.Entity<Change_ImpactedBusiness>().HasKey(t => new { t.ChangeId, t.ImpactedBusinessId });
modelBuilder.Entity<Change_ImpactedService>().HasKey(t => new { t.ChangeId, t.ImpactedServiceId });
modelBuilder.Entity<Change_TestStage>().HasKey(t => new { t.ChangeId, t.TestStageId });
modelBuilder.Entity<Change_TypeOfChange>().HasKey(t => new { t.ChangeId, t.TypeOfChangeId });
}
}
Where I start running into problems is I'm not generating an Id using Entity Framework, the primary key is an identity in SQL Server 2012 and I get that back once the insert is completed, as opposed to using a GUID (which I've read pretty much everywhere is super frowned upon in the DBA world).
So what ends up happening is I either try and do the insert and it tries to insert the many to many relationships with changeId in the junction table being null (because it isn't generated yet) or when I try what I have below to do an insert and an update in one post method. It errors out because the ChangeId key value is already being tracked. Here is what I'm attempting below.
Controller method
public IActionResult CreateChange([FromBody] ChangeModel change) {
if (change == null) {
return BadRequest();
}
//Remove many to many from Change to insert without them (as this can't be done until primary key is generated.
List<Change_CriticalBankingAppModel> criticalApps = new List<Change_CriticalBankingAppModel>();
criticalApps.AddRange(change.Change_CriticalBankingApps);
List<Change_ImpactedBusinessModel> impactedBusinesses = new List<Change_ImpactedBusinessModel>();
impactedBusinesses.AddRange(change.Change_ImpactedBusinesses);
List<Change_ImpactedServiceModel> impactedServices = new List<Change_ImpactedServiceModel>();
impactedServices.AddRange(change.Change_ImpactedServices);
List<Change_TestStageModel> testStages = new List<Change_TestStageModel>();
testStages.AddRange(change.Change_TestStages);
List<Change_TypeOfChangeModel> changeTypes = new List<Change_TypeOfChangeModel>();
changeTypes.AddRange(change.Change_TypeOfChanges);
change.Change_CriticalBankingApps.Clear();
change.Change_ImpactedBusinesses.Clear();
change.Change_ImpactedServices.Clear();
change.Change_TestStages.Clear();
change.Change_TypeOfChanges.Clear();
//Map Change model to change entity for inserting
var changeEntity = Mapper.Map<Change>(change);
_changeRepository.AddChange(changeEntity);
if (!_changeRepository.Save()) {
throw new Exception("Creating change failed on save.");
}
var changetoReturn = Mapper.Map<ChangeModel>(changeEntity);
//Iterate through Many to many Lists to add generated changeId
foreach (var criticalApp in criticalApps) {
criticalApp.ChangeId = changetoReturn.ChangeId;
}
foreach (var impactedBusiness in impactedBusinesses) {
impactedBusiness.ChangeId = changetoReturn.ChangeId;
}
foreach (var impactedService in impactedServices) {
impactedService.ChangeId = changetoReturn.ChangeId;
}
foreach (var testStage in testStages) {
testStage.ChangeId = changetoReturn.ChangeId;
}
foreach (var changeType in changeTypes) {
changeType.ChangeId = changetoReturn.ChangeId;
}
//Add many to many lists back to change to update
changetoReturn.Change_CriticalBankingApps = criticalApps;
changetoReturn.Change_ImpactedBusinesses = impactedBusinesses;
changetoReturn.Change_ImpactedServices = impactedServices;
changetoReturn.Change_TestStages = testStages;
changetoReturn.Change_TypeOfChanges = changeTypes;
changeEntity = Mapper.Map<Change>(changetoReturn);
_changeRepository.UpdateChange(changeEntity);
if (!_changeRepository.Save()) {
throw new Exception("Updating change with many to many relationships failed on save.");
}
changetoReturn = Mapper.Map<ChangeModel>(changeEntity);
return CreatedAtRoute("GetChange",
new { changeId = changetoReturn.ChangeId },
changetoReturn);
}
Relevant Repository methods
public Change GetChange(int changeId) {
return _context.Change.FirstOrDefault(c => c.ChangeId == changeId);
}
public void AddChange(Change change) {
_context.Change.Add(change);
}
public void UpdateChange(Change change) {
_context.Change.Update(change);
}
public bool ChangeExists(int changeId) {
return _context.Change.Any(c => c.ChangeId == changeId);
}
I encounter this error on the update attempt.
I understand that if I were to have entity framework generate the guid instead of having the database generate the identity int that I would have a much easier time with this but a requirement for this project is to not use Guid's.
Any help on how to successfully process this would be greatly appreciated.
EDIT: In case it helps, here is the http post I'm using with postman.
{
"changeTitle": "Test",
"changeType": "Test",
"dateSubmitted": "02/12/2018",
"targetDate": "02/12/2018",
"change_CriticalBankingApps": [
{
"criticalBankingAppId" : 1,
"description" : "Very critical"
},
{
"criticalBankingAppId" : 2,
"description" : "Moderately critical"
}
],
"change_impactedBusinesses": [
{
"ImpactedBusinessId" : 1
},
{
"ImpactedBusinessId" : 2
}
]
}
The error you are getting has nothing to do with the guid vs db identity.
You are getting it because you are:
Fetching an entity from the database
Creating new entity (not tracked) from within your controller (the mapper does this)
Try to update the entity that is not tracked by entity framework
The update will try to add the entity to the EF repository, but will fail because it already contains an entity with the given ID.
If you plan to make changes to an entity, you need to make sure entity framework tracks the entity prior to calling the update method.
If EF does not track your entity, it does not know which fields have been updated (if any).
Edit:
If you want to get rid of the error, you could detach your original entity. Make sure you do it prior to mapping the changetoReturn back into your changeEntity.
dbContext.Entry(entity).State = EntityState.Detached;
But since your new entity won't be tracked, I don't think anything will be updated (EF does not know what has been changed).
Edit 2:
Also take a look at this to get your changes back into your original entity.
Change this:
changeEntity = Mapper.Map<Change>(changetoReturn);
Into this:
Mapper.Map(changetoReturn, changeEntity);
Using Automapper to update an existing Entity POCO
add new entities via joint table...that way, entities are tracked both in the joint table and their individual respective tables
Ok, whether this is an elegant solution is up for debate, but I was able to detach the entity state from changeEntity after doing the initial insert as follows
_changeRepository.AddChange(changeEntity);
_changecontext.Entry(changeEntity).State = EntityState.Detached;
Then after reattaching all of the many to many lists back to changeToReturn, I created a new Change entity and added that entity state, and updated on that as follows.
var newChangeEntity = Mapper.Map<Change>(changeToReturn);
_changecontext.Entry(newChangeEntity).State = EntityState.Added;
_changeRepository.UpdateChange(newChangeEntity);
Then I returned this mapped back to a view model.
It seems hacky and perhaps through a deeper understanding of entity framework I'll discover a much better way of going about this but this works for now.
I'm using Fluent-NHibernate and attempting to persist an object hierarchy using the table per subclass method:
public class AbstractProduct
{
public int ProductId { get; set; }
public string Name { get; set; }
}
public class SingleProduct : AbstractProduct
{
public int SingleProductId { get; set; }
public string SomeField { get; set; }
}
when saving an object
var singleProduct = new SingleProduct();
session.SaveOrUpdate(singleProduct);
I get this error:
NHibernate.Exceptions.GenericADOException: could not insert: [FluentNHibernateSubClassTest.SingleProduct#3][SQL: INSERT INTO SingleProductData (Field1, AbstractProduct_id) VALUES (?, ?)] ---> System.Data.SqlClient.SqlException: Invalid column name 'AbstractProduct_id'.
despite having the following overrides:
public class AbstractProductOverrides : IAutoMappingOverride<AbstractProduct>
{
public void Override(AutoMapping<AbstractProduct> mapping)
{
mapping.Id(x => x.ProductId).Column("ProductId");
//this mapping provided to illustrate the overrides are picked up
mapping.Table("ProductsData");
mapping.JoinedSubClass<SingleProduct>("ProductId");//ignored??
}
}
public class SingleProductOverrides : IAutoMappingOverride<SingleProduct>
{
public void Override(AutoMapping<SingleProduct> mapping)
{
mapping.Id(x => x.SingleProductId);
mapping.Table("SingleProductData");
mapping.Map(x => x.SomeField).Column("Field1");
}
}
It doesn't appear to matter what column name I supply to JoinedSubClass it ignores it and uses AbstractProduct_id instead.
How can I tell nhibernate the key column is ProductId and not AbstractProduct_id?
I have a test project demonstrating the issue available here (you need to create the db)
UPDATE
I've got around this by providing the following convention:
public class JoinedSubclassConvention : IJoinedSubclassConvention
{
public void Apply(IJoinedSubclassInstance instance)
{
if (instance.EntityType == typeof(SingleProduct))
instance.Key.Column(("ProductId"));
}
}
which works but feels like its the wrong way or a hack.
mapping.Id in SingleProductOverrides is flawed. Subclasses don't have their own id, they inherit the Id from their base classes. Even mapping.JoinedSubClass<SingleProduct>("ProductId"); is redundant (probably ignored) if SingleProduct is automapped as well (it is as seen from the Override for it). JoinedSubclassConvention is the right way to do this.
I can't get my head around why this isn't working..
I have a relatively clean entity model consisting of POCOs created with DDD in mind (while probably not following most rules even loosely).
I am using Fluent NHibernate to do the mapping. I am also using SchemaExport to create the database schema, with minimum input from me on how to do it. NHibernate is free to choose the best way.
I have two entities with Many-to-many relationships with each other (non-interesting code removed); MediaItem and Tag; MediaItems can have many tags, Tags can be applied to many MediaItems, and I want collections on both sides so I can easily get at stuff.
(A bit of a formatting issue below, sorry)
MediaItem:
public class MediaItem
{
private IList<Tag> _tags;
public virtual long Id { get; set; }
public virtual string Title { get; set; }
public virtual IEnumerable<Tag> Tags { get { return _tags; } }
public MediaItem()
{
_tags = new List<Tag>();
}
public virtual void AddTag(Tag newTag)
{
_tags.Add(newTag);
newTag.AddMediaItem(this);
}
}
Tag:
public class Tag
{
private IList<MediaItem> _mediaItems;
public virtual long Id { get; set; }
public virtual string TagName { get; set; }
public virtual IEnumerable<MediaItem> MediaItems { get { return _mediaItems; } }
public Tag()
{
_mediaItems = new List<MediaItem>();
}
protected internal virtual void AddMediaItem(MediaItem newItem)
{
_mediaItems.Add(newItem);
}
}
I have tried to be smart about only exposing the collections as IEnumerable, and only allowing adding items through the methods. I also hear that only one side of the relationship should be responsible for this - thus the contrived AddMediaItem() on Tag.
The MediaItemMap looks like this:
public class MediaItemMap : ClassMap<MediaItem>
{
public MediaItemMap()
{
Table("MediaItem");
Id(mi => mi.Id);
Map(mi => mi.Title);
HasManyToMany<Tag>(mi => mi.Tags)
.Access.CamelCaseField(Prefix.Underscore)
.Cascade.SaveUpdate();
}
}
The Tag mapping looks like this:
public class TagMap : ClassMap<Tag>
{
public TagMap()
{
Table("Tag");
Id(t => t.Id);
Map(t => t.TagName);
HasManyToMany<MediaItem>(mi => mi.MediaItems)
.Access.CamelCaseField(Prefix.Underscore)
.Inverse();
}
}
Now I have some test code that drops the database schema, recreates it (since I am shotgun debugging my brains out here), and then runs the following simple code:
Tag t = new Tag { TagName = "TestTag" };
MediaItem mi = new MediaItem { Title = "TestMediaItem" };
mi.AddTag(t);
var session = _sessionFactory.OpenSession();
session.Save(mi);
Yep, this is test code, it will never live past the problem in this post.
The MediaItem is saved, and so is the Tag. However, the association between them is not. NHibernate does create the association table "MediaItemsToTags", but it doesn't attempt to insert anything into it.
When creating the ISessionFactory, I specify ShowSQL() - so I can see all the DDL sent to the SQL server. I can see the insert statement for both the MediaItem and the Tag tables, but there is no insert for MediaItemsToTags.
I have experimented with many different versions of this, but I can't seem to crack it. Cascading is one possible problem, I've tried with Cascade.All() on both sides, Inverse() on both sides etc., but no dice.
Can anyone tell me what is the correct way to map this to get NHibernate to actually store the association whenever I store my MediaItem?
Thanks!
You need to define the many-to-many table and parent and child key columns:
public class MediaItemMap : ClassMap<MediaItem>
{
public MediaItemMap()
{
Table("MediaItem");
Id(mi => mi.Id);
Map(mi => mi.Title);
HasManyToMany<Tag>(mi => mi.Tags)
.Table("MediaItemsToTags").ParentKeyColumn("Id").ChildKeyColumn("Id")
.Access.CamelCaseField(Prefix.Underscore)
.Cascade.SaveUpdate();
}
}
The syntax is identical in TagMap because both key columns are named "Id".