I'm developing a website using MVC4 with EF code first approach.
I have some problems with deleting the children in entities with one to many relations.
Edit to clearify: In my Edit view, I add/remove/update existing childen in the childrens collection on the parent, add/remove is done using javascript. When I receive the updated parent in the post request in the controller method, I want to sync/update the parent and child entities in the database.
The parent object is in a detached state when updated in the view. So, when I attach the parent again, I want it to do all the updates that have been done during detached state.
The entity relations are set up so that when removing a child entity from the parent collection, the child entity is also deleted from the child table (cascade delete sort of?), and this works when in attached state.
However, when attching the parent and saving the changes, only added/updated children are added/modified in the database. But removed children from the parent collection are not deleted in the database (which I want them to be).
How can this be solved??
The entities are:
class Parent
{
public virtual ICollection<Child> Children { get; set; }
}
class Child
{
public string Text { get; set; }
}
This works and will remove child from database:
void RemoveChildFromCollection()
{
// get the first parent and remove the first child in collection
var context = new DatabaseContext();
var parent = context.Parents.First();
parent.Children.Remove(parent.Children.First());
context.SaveChanges();
}
ControllerMethod: This does not work as above, removed children are not removed from the childrens table
public ActionResult Edit(Parent parent)
{
var context = new DatabaseContext();
context.Entry(parent).State = System.Data.EntityState.Modified;
context.SaveChanges();
return View();
}
The modelbuilder are setup to delete child entity from child table when removing them from parent collection
// Use Identifying relation. Define complex key for ChildObject containing both Id and
ParentObjectId
modelBuilder.Entity<Child>()
.HasKey(c => new {c.ChildID, c.ParentID});
// Because defining such key will remove default convention for auto incremented Id you must redefine it manually
modelBuilder.Entity<Child>()
.Property(c => c.ChildID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
// Set cascade delete
modelBuilder.Entity<Parent>()
.HasMany(p => p.Children)
.WithRequired()
.HasForeignKey(c => c.ParentID)
.WillCascadeOnDelete();
Cascade delete only deletes child entities, when the parent entity is also removed, as you've noted, not when you sever the relationship.
You can override your SaveChanges() in your Context to clean up your orphaned Child entities like this:
public override int SaveChanges()
{
Children
.Local
.Where(c => c.Parent == null)
.ToList()
.ForEach(child => Children.Remove(child));
return base.SaveChanges();
}
This blog post has more info on handling orphaned entities.
Your entities - parent and children - are in a detached state when you do the modification in the view. So, EF couldn't track those changes. When you attach the object graph to the context - by setting the parent's state to Modified - EF takes this attached object graph as the current state and doesn't know anything about the removal of the child that happened during the detached phase in the view.
To solve the problem you must load the current object graph - parent including children - from the database, compare it with the object graph from your view and merge the changes into the loaded graph. Then save the changes. There can be several possible changes:
The parent's scalar properties could have been changed
The children's scalar properties can been changed
A new child could have been added to the Children collection
A child could have been removed from the Children collection
Your current code - setting the parent's state to Modified - will only handle the first case correctly but not the other three cases.
To deal with all four cases you need to follow the procedure described above. An example how to do that is shown here (see the Edit section in that answer).
The code in your Edit post action would then look like this:
public ActionResult Edit(Parent parent)
{
using (var context = new DatabaseContext())
{
var parentInDb = context.Parents
.Include(p => p.Children)
.Single(p => p.ParentId == parent.ParentId);
context.Entry(parentInDb).CurrentValues.SetValues(parent);
foreach (var childInDb in parentInDb.Children.ToList())
if (!parent.Children.Any(c =>
c.ChildId == childInDb.ChildId &&
c.ParentId == childInDb.ParentId)) // or == parent.ParentId
context.Children.Remove(childInDb);
// here
// parentInDb.Children.Remove(childInDb);
// should work too because you have an identifying relationship
foreach (var child in parent.Children)
{
var childInDb = parentInDb.Children.SingleOrDefault(c =>
c.ChildId == child.ChildId &&
c.ParentId == child.ParentId); // or == parent.ParentId
if (childInDb != null)
context.Entry(childInDb).CurrentValues.SetValues(child);
else
parentInDb.Children.Add(child);
}
context.SaveChanges();
return View();
}
}
Related
I have a parent child relationship in neo4j and I want to do a save on the parent entity which will implicitly save the child entity
If the entity extends AuditEntity, then I want to set the createTimestamp and updateTimestamp before saving the parent and child entity
To achieve this I was thinking of adding BeforeSaveEvent Application Listner and setting the values there. But this will set the values only for the parent entity. How can I set them on the child entities ?
Or is there any better approach to do this ?
I am using SDN 4.1.2
#Bean
ApplicationListener<BeforeSaveEvent> beforeSaveEventApplicationListener() {
return new ApplicationListener<BeforeSaveEvent>() {
#Override
public void onApplicationEvent(BeforeSaveEvent event) {
if(event.getEntity() instanceof AuditEntity) {
(AuditEntity) auditEntity = (AuditEntity)event.getEntity();
auditEntity.setCreateTimeStamp(new Timestamp((new Date()).getTime()));
auditEntity.setUpdateTimeStamp(new Timestamp((new Date()).getTime()));
}
}
};
}
I am trying to add a new record in an MVC controller method using Entity framework.
When i just used "InsertOrUpdate" the audittype got duplicated. Based on the answer from Entity Framework adding record with a related object i hoped to fix it pretty qiock. This is the code I have right now:
Controller:
if (ModelState.IsValid)
{
Audit newAudit = Factory.GetNew();
newAudit.Name = model.Name;
newAudit.Deadline = model.Deadline;
newAudit.AuditType = auditTypeRepository.Find(model.SelectedAuditTypeId);
Repository.InsertOrUpdate(newAudit);
Repository.Save();
return RedirectToAction(MVC.Audits.Details(newAudit.Id));
}
Repository:
public override void InsertOrUpdate(Qdsa.WebApplications.AuditMaster.Data.Audit model)
{
if (model.Id == default(int))
{
// New entity
context.Audits.Add(model);
}
else
{
// Existing entity
model.ModifiedOn = DateTime.Now;
context.Entry(model).State = EntityState.Modified;
}
//If I leave out the code below the AuditType will be duplicated
if (model.AuditType != null)
{
context.Entry<AuditType>(model.AuditType).State = EntityState.Unchanged;
}
}
public virtual void Save()
{
context.SaveChanges();
}
So i thought I fixed the problem. However, AuditType has Child objects too. And now these childobjects get duplicated.
What is the right way to add entities with child objects which already exists?
Because the AuditType is required I can't save it without first and then update it. any suggestions?
UPDATE:
Both the AuditRepostory and the AuditTypeRepository inherit from BaseRepository which has the context as:
protected DBContext context = new DBContext ();
public virtual T Find(int id)
{
return All.SingleOrDefault(s => s.Id == id);
}
I can imagine two reasons for the problem:
Either auditTypeRepository.Find performs a no tracking query (with .AsNoTracking())
Or you are using a context instance per repository, so that Repository and auditTypeRepository are working with two different contexts which will indeed result in a duplication of the AuditType because you don't attach it to the the context that corresponds with Repository (except in the line with your comment).
If the latter is the case you should rethink your design and inject a single context instance into all repositories instead of creating it inside of the repositories.
I think the problem is from here:
newAudit.AuditType = auditTypeRepository.Find(model.SelectedAuditTypeId);
Change that like this:
newAudit.AuditTypeId = model.SelectedAuditTypeId;
When I update my model I get an error on a child relation which I also try to update.
My model, say Order has a releationship with OrderItem. In my view I have the details of the order together with an editortemplate for the orderitems. When I update the data the link to Order is null but the orderid is filled, so it should be able to link it, TryUpdateModel returns true, the save however fails with:
InvalidOperationException: The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.]
My update method:
public ActionResult ChangeOrder(Order model)
{
var order = this.orderRepository.GetOrder(model.OrderId);
if (ModelState.IsValid)
{
var success = this.TryUpdateModel(order);
}
this.orderRepository.Save();
return this.View(order);
}
I tried all solutions I saw on SO and other sources, none succeeded.
I use .Net MVC 3, EF 4.3.1 together with DBContext.
There are a number of code smells here, which I'll try to be elegant with when correcting :)
I can only assume that "Order" is your EF entity? If so, I would highly recommend keeping it separate from the view by creating a view model for your form and copying the data in to it. Your view model should really only contain properties that your form will be using or manipulating.
I also presume orderRepository.GetOrder() is a data layer call that retrieves an order from a data store?
You are also declaring potentially unused variables. "var order =" will be loaded even if your model is invalid, and "var success =" is never used.
TryUpdateModel and UpdateModel aren't very robust for real-world programming. I'm not entirely convinced they should be there at all, if I'm honest. I generally use a more abstracted approach, such as the service / factory pattern. It's more work, but gives you a lot more control.
In your case, I would recommend the following pattern. There's minimal abstraction, but it still gives you more control than using TryUpdateModel / UpdateModel:
public ActionResult ChangeOrder(OrderViewModel model) {
if(ModelState.IsValid) {
// Retrieve original order
var order = orderRepository.GetOrder(model.OrderId);
// Update primitive properties
order.Property1 = model.Property1;
order.Property2 = model.Property2;
order.Property3 = model.Property3;
order.Property4 = model.Property4;
// Update collections manually
order.Collection1 = model.Collection1.Select(x => new Collection1Item {
Prop1 = x.Prop1,
Prop2 = x.Prop2
});
try {
// Save to repository
orderRepository.SaveOrder(order);
} catch (Exception ex) {
ModelState.AddModelError("", ex.Message);
return View(model);
}
return RedirectToAction("SuccessAction");
}
return View(model);
}
Not ideal, but it should serve you a bit better...
I refer you to this post, which is similar.
I assume that the user can perform the following actions in your view:
Modify order (header) data
Delete an existing order item
Modify order item data
Add a new order item
To do a correct update of the changed object graph (order + list of order items) you need to deal with all four cases. TryUpdateModel won't be able to perform a correct update of the object graph in the database.
I write the following code directly using a context. You can abstract the use of the context away into your repository. Make sure that you use the same context instance in every repository that is involved in the following code.
public ActionResult ChangeOrder(Order model)
{
if (ModelState.IsValid)
{
// load the order from DB INCLUDING the current order items in the DB
var orderInDB = context.Orders.Include(o => o.OrderItems)
.Single(o => o.OrderId == model.OrderId);
// (1) Update modified order header properties
context.Entry(orderInDB).CurrentValues.SetValues(model);
// (2) Delete the order items from the DB
// that have been removed in the view
foreach (var item in orderInDB.OrderItems.ToList())
{
if (!model.OrderItems.Any(oi => oi.OrderItemId == item.OrderItemId))
context.OrderItems.Remove(item);
// Omitting this call "Remove from context/DB" causes
// the exception you are having
}
foreach (var item in model.OrderItems)
{
var orderItem = orderInDB.OrderItems
.SingleOrDefault(oi => oi.OrderItemId == item.OrderItemId);
if (orderItem != null)
{
// (3) Existing order item: Update modified item properties
context.Entry(orderItem).CurrentValues.SetValues(item);
}
else
{
// (4) New order item: Add it
orderInDB.OrderItems.Add(item);
}
}
context.SaveChanges();
return RedirectToAction("Index"); // or some other view
}
return View(model);
}
Entity:
public class Page
{
//...
public virtual Page Parent { get; set; }
}
Need to set Parent field to null. Tried this, but no luck:
// Existing entity
Page pageAttached = db.Pages.First(x => x.Id == page.Id);
db.Entry(pageAttached).CurrentValues.SetValues(page);
if (model.ParentId != null)
pageAttached.Parent = db.Pages.First(x => x.Id == model.ParentId);
else
pageAttached.Parent = null; //does nothing
db.SaveChanges();
Parent is not a "complex field", it is a "Navigation Property".
Does it work if you do this?
// Existing entity
Page pageAttached = db.Pages.Include(x => x.Parent).First(x => x.Id == page.Id);
db.Entry(pageAttached).CurrentValues.SetValues(page);
if (model.ParentId != null)
pageAttached.Parent = db.Pages.First(x => x.Id == model.ParentId);
else
pageAttached.Parent = null; //does nothing
db.SaveChanges();
Response to comment 1
No, I meant .Include(x => x.Parent). I prefer strongly-typing using the lambda overload. Keeps magic strings out of the code.
The reason this works is because DbContext uses dynamically generated proxy classes for lazy loading. When you only query for .First(x => x.Id == page.Id), the object returned is really a class that implements your Page entity as its base class. (This is why collection and navigation properties have to be marked virtual, so they can be overridden in the dynamic proxies.) Furthermore, the dynamically generated proxy has a null Parent reference, even if there is a parent in the db.
It is not until the Parent property get method is invoked that EF hits the db to lazily load the parent. This is when it finds out whether the db actually has a null or non-null Parent property. So, when you set .Parent = null before the parent is actually lazily loaded, EF does nothing because it's already null.
The code I suggested uses .Include to eager load the Parent property. This means that the db gets both the child + its parent in a single db call. Now when you set null, the DbContext will track the change and remove the relationship during your next SaveChanges.
I'm using EF4 with POCO and the code below is what I have to update a license. I only pasted here the section of the code that is relevant to the problem, which is adding LicenseDetails.
The problem is that for each LicenseDetail that are inserted, EF also unexpectadly adds rows to the "Item" lookup table. Why?!
The relationship of both tables defined in the database (SQLServer 2008) is shown below and I do Database first, therefore EF generates the entities' relationship based on this.
ALTER TABLE [dbo].[LicenseDetail]
WITH CHECK ADD CONSTRAINT [FK_LicenseDetail_Item] FOREIGN KEY([ItemId])
REFERENCES [dbo].[Item] ([Id])
GO
ALTER TABLE [dbo].[LicenseDetail] CHECK CONSTRAINT [FK_LicenseDetail_Item]
GO
the update method:
public static void UpdateLicense(License license)
{
using (ALISEntities context = new ALISEntities())
{
context.ContextOptions.ProxyCreationEnabled = true;
var storedLicense =
context.Licenses.Include("LicenseDetails")
.Where(o => o.Id == license.Id).SingleOrDefault();
//////////////////////////////////////////////////////////////
// license details to add
List<LicenseDetail> toAdd = new List<LicenseDetail>();
foreach (LicenseDetail ld in license.LicenseDetails)
{
if (storedLicense.LicenseDetails
.Where(d => d.ItemId == ld.ItemId).Count() == 0)
{
toAdd.Add(ld);
}
}
toAdd.ForEach(i => storedLicense.LicenseDetails.Add(i));
context.SaveChanges();
}
}
When you add a new LicenseDetails to context you also add Items which are referenced by those LicenseDetails. Because context doesn't know that Items already exists in the database it adds them. You need to tell context that Items are already in the database by calling context.Items.Attach(licenseDetail.Item).
You might also try using
context.Licenses.Include("LicenseDetails").Find(license.Id);
instead of
context.Licenses.Include("LicenseDetails")
.Where(o => o.Id == license.Id).SingleOrDefault();
and there is no need to use toAdd list at all - just keep adding licenseDetails in the first foreach loop.
I ended up having to issue context.ObjectStateManager.ChangeObjectState(d.Item, EntityState.Unchanged) for each LicenseDetail added. That solved the problem.