I'm having some trouble with my EF Code First model when saving a relation to a many to many relationship. My Models:
public class Event
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Event> Events { get; set; }
}
In my controller, I map one or many TagViewModels into type of Tag, and send it down to my servicelayer for persistence. At this time by inspecting the entities the Tag has both Id and Name (The Id is a hidden field, and the name is a textbox in my view)
The problem occurs when I now try to add the Tag to the Event. Let's take the following scenario:
The Event is already in my database, and let's say it already has the related tags C#, ASP.NET
If I now send the following list of tags to the servicelayer:
ID Name
1 C#
2 ASP.NET
3 EF4
and add them by first fetching the Event from the DB, so that I have an actual Event from my DbContext, then I simply do
myEvent.Tags.Add
to add the tags.. Problem is that after SaveChanges()
my DB now contains this set of tags:
ID Name
1 C#
2 ASP.NET
3 EF4
4 C#
5 ASP.NET
开发者_高级运维
This, even though my Tags that I save has it's ID set when I save it (although I didn't fetch it from the DB)
I think I know what happend in your code. Let me explain my opinion in simple example:
using (var context = new Context())
{
// Just let assume these are your tags received from view model.
var csharp = ...;
var aspnet = ...;
var ef4 = ...;
// Now you load Event
var e = context.Events.Where(e => e.Id == someId);
// Ups first access to Tag collection triggers lazy loading which
// is enabled by default so, all current tags are loaded
e.Tags.Add(csharp);
e.Tags.Add(aspnet);
e.Tags.Add(ef4);
// Now e.Tags.Count == 5 !!! Why?
context.SaveChanges();
}
The first problem: Because a dynamic proxy created on top of your Event
instance uses HashSet
for Tags
it checks if added entity already exists in the collection. If not, it adds the entity othewise it skips the entity. To be able to do this check correctly YOU MUST IMPLEMENT Equals and GetHashCode in Tag! So because you didn't do it, it takes added tags as new ones with temporary keys and it adds them to Tags
table with autogenerated key.
The second problem: Even if you implement Equals
and GetHashCode
you will solve only duplicity of C# and ASP.NET tags. At the moment the context doesn't track EF4 tag so this tag is still considered as a new one. You must inform the context that EF4 tag exists in DB. So lets Attach
all tags to the context before you trigger lazy loading on Tags
collection. Attaching entity to the context by default sets its state to Unchanged
:
using (var context = new Context())
{
foreach (var tag in TagsFromView)
{
context.Attach(tag);
}
// Now you load Event
var e = context.Events.Where(e => e.Id == someId);
foreach(var tag in TagsFromView)
{
// First access will trigger lazy loading but already
// attached instances of tags are used
e.Tags.Add(tag);
}
// Now you must delete all tags present in e.Tags and not
// present in TagsFromView
context.SaveChanges();
}
This works if you don't create new tags in your view. If you want to do it as well, you mustn't attach new tags to the context. You have to differ between existing tags and new tags (for example new tags can have Id = 0).
You need to get the tags from the db, otherwise EF will treat them as new items and override the id.
精彩评论