问题
I have a Business
and a Category
model.
Each Business
has many Categories
via an exposed collection (Category
is disregarding the Business
entity).
Now here is my controller-action:
[HttpPost]
[ValidateAntiForgeryToken]
private ActionResult Save(Business business)
{
//Context is a lazy-loaded property that returns a reference to the DbContext
//It's disposal is taken care of at the controller's Dispose override.
foreach (var category in business.Categories)
Context.Categories.Attach(category);
if (business.BusinessId > 0)
Context.Businesses.Attach(business);
else
Context.Businesses.Add(business);
Context.SaveChanges();
return RedirectToAction("Index");
}
Now there are several business.Categories
that have their CategoryId
set to an existing Category
(the Title
property of Category
is missing tho).
After hitting SaveChanges
and reloading the Business
from server, the Categories
are not there.
So my question is what's the proper way to set Business.Categories
with a given array of existing CategoryId
s.
When create a new Business
however, the following DbUpdateException
exception is thrown when calling SaveChanges
:
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
Inner exception (OptimisticConcurrencyException
):
Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.
Update
after answer, here's the update code:
var storeBusiness = IncludeChildren().SingleOrDefault(b => b.BusinessId == business.BusinessId);
var entry = Context.Entry(storeBusiness);
entry.CurrentValues.SetValues(business);
//storeBusiness.Categories.Clear();
foreach (var category in business.Categories)
{
Context.Categories.Attach(category);
storeBusiness.Categories.Add(category);
}
When calling SaveChanges
, I'm getting the following DbUpdateException
:
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
Here's how the Business/Category models look like:
public class Business
{
public int BusinessId { get; set; }
[Required]
[StringLength(64)]
[Display(Name = "Company name")]
public string CompanyName { get; set; }
public virtual BusinessType BusinessType { get; set; }
private ICollection<Category> _Categories;
public virtual ICollection<Category> Categories
{
get
{
return _Categories ?? (_Categories = new HashSet<Category>());
}
set
{
_Categories = value;
}
}
private ICollection<Branch> _Branches;
public virtual ICollection<Branch> Branches
{
get
{
return _Branches ?? (_Branches = new HashSet<Branch>());
}
set
{
_Branches = value;
}
}
}
public class Category
{
[Key]
public int CategoryId { get; set; }
[Unique]
[Required]
[MaxLength(32)]
public string Title { get; set; }
public string Description { get; set; }
public int? ParentCategoryId { get; set; }
[Display(Name = "Parent category")]
[ForeignKey("ParentCategoryId")]
public virtual Category Parent { get; set; }
private ICollection<Category> _Children;
public virtual ICollection<Category> Children
{
get
{
return _Children ?? (_Children = new HashSet<Category>());
}
set
{
_Children = value;
}
}
}
Just to make it clear again, the Category
I'm attaching to existing/new Business
es already exist in the DB and have an ID, which is what I'm using to attach it with.
回答1:
I treat the two cases - updating an existing business and adding a new business - separately because the two problems you mentioned have different reasons.
Updating an existing Business entity
That's the if
case (if (business.BusinessId > 0)
) in your example. It is clear that nothing happens here and no change will be stored to the database because you are just attaching the Category
objects and the Business
entity and then call SaveChanges
. Attaching means that the entities are added to the context in state Unchanged
and for entities that are in that state EF won't send any command to the database at all.
If you want to update a detached object graph - Business
plus collection of Category
entities in your case - you generally have the problem that a collection item could have been removed from the collection and an item could have been added - compared to the current state stored in the database. It might be also possible that a collection item's properties and that the parent entity Business
have been modified. Unless you have tracked all changes manually while the object graph was detached - i.e. EF itself could not track the changes - which is difficult in a web application because you had to do this in the browser UI, your only chance to perform a correct UPDATE of the whole object graph is comparing it with the current state in the database and then put the objects into the correct state Added
, Deleted
and Modified
(and perhaps Unchanged
for some of them).
So, the procedure is to load the Business
including its current Categories
from the database and then merge the changes of the detached graph into the loaded (=attached) graph. It could look like this:
private ActionResult Save(Business business)
{
if (business.BusinessId > 0) // = business exists
{
var businessInDb = Context.Businesses
.Include(b => b.Categories)
.Single(b => b.BusinessId == business.BusinessId);
// Update parent properties (only the scalar properties)
Context.Entry(businessInDb).CurrentValues.SetValues(business);
// Delete relationship to category if the relationship exists in the DB
// but has been removed in the UI
foreach (var categoryInDb in businessInDb.Categories.ToList())
{
if (!business.Categories.Any(c =>
c.CategoryId == categoryInDb.CategoryId))
businessInDb.Categories.Remove(categoryInDb);
}
// Add relationship to category if the relationship doesn't exist
// in the DB but has been added in the UI
foreach (var category in business.Categories)
{
var categoryInDb = businessInDb.Categories.SingleOrDefault(c =>
c.CategoryId == category.CategoryId)
if (categoryInDb == null)
{
Context.Categories.Attach(category);
businessInDb.Categories.Add(category);
}
// no else case here because I assume that categories couldn't have
// have been modified in the UI, otherwise the else case would be:
// else
// Context.Entry(categoryInDb).CurrentValues.SetValues(category);
}
}
else
{
// see below
}
Context.SaveChanges();
return RedirectToAction("Index");
}
Adding a new Business entity
Your procedure to add a new Business
together with its related Categories
is correct. Just attach all Categories
as existing entities to the context and then add the new Business
to the context:
foreach (var category in business.Categories)
Context.Categories.Attach(category);
Context.Businesses.Add(business);
Context.SaveChanges();
If all Categories
you are attaching really have a key value that exists in the database this should work without exception.
Your exception means that at least one of the Categories
has an invalid key value (i.e. it does not exist in the database). Maybe it has been deleted in the meantime from the DB or because it is not correctly posted back from the Web UI.
In case of an independent association - that is an association without FK property BusinessId
in Category
- you get indeed this OptimisticConcurrencyException
. (EF seems to assume here that the category has been deleted from the DB by another user.) In case of a foreign key association - that is an association which has a FK property BusinessId
in Category
- you would get an exception about a foreign key constraint violation.
If you want to avoid this exception - and if it occurs in fact because another user deleted a Category
, not because the Category
is empty/0
since it doesn't get posted back to the server (fix this with a hidden input field instead) - you better load the categories by CategoryId
(Find
) from the database instead of attaching them and if one doesn't exist anymore ignore it and remove it from the business.Categories
collection (or redirect to an error page to inform the user or something like that).
回答2:
I had this exception as well. The problem with me was that the primary key of the added object was not being set by ADO Entity Framework. This resulted in the problem that the foreign key of the added object in the database could not be set as well.
I solved this problem by making sure the primary key was being set by the database itself. If you're using SQL Server you can do that by adding the IDENTITY keyword with the column declaration in the CREATE TABLE statement.
Hope this helps
回答3:
I was getting this exception on a field I was trying to set beyond its MaxLength attribute. The field also had a Required attribute. I'm working against an Oracle back-end database. It wasn't until I increased the length did I finally get a descriptive error message from the underlying database engine that the length was too long. I suspect the foreign key related error message vaguely means that not all relationships could be updated because one of the inserts failed and didn't get their Key updated.
来源:https://stackoverflow.com/questions/17005787/how-to-set-a-collection-property-with-fks