I'm new to MVC and Entity frame work but i've come across an issue and i'm not sure how to resolve it some guidance is more then welcome. I'm editing an object which has a sub list of different objects. I've got a working edit screen but i cant get the details stored back the database. I think the issue is around the context of the db but as i said i'm new to this. Here are the 2 classes i'm using:
public class Role
{
[Key]
public string Role { get; set; }
public virtual ICollection<Template> Templates { get; set; }
}
public class Template
{
[Key][Required]
public string TemplateID { get; set; }
[Required]
public string Header { get; set; }
[Required][DataType(DataType.MultilineText)]
public string Description { get; set; }
public virtual ICollection<CorrespondenceRole> Roles { get; set; }
}
I also have this in my context
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Template>()
.HasMany(c => c.Roles)
.WithMany(c => c.Templates)
.Map(m => m.ToTable("Template_Roles")
.MapLeftKey("TemplateID")
.MapRightKey("Role"));
}
If i use
public ActionResult Edit(Template ctemplate)
{
db.Entry(ctemplate).State = EntityState.Modified;
db.SaveChanges();
}
The changes to the template object are recorded to the database but any changes to the roles list are totally ignored even if i hardcode ctemplate.roles.add(somerole) before the db save.
However if i use
public ActionResult Edit(Template ctemplate)
{
Template ct = db.Templates.Find(ctemplate.TemplateID);
ct.Roles.Add(db.Roles.Find("Other Party"));
db.Entry(ct).State = EntityState.Modified;
db.SaveChanges();
}
The "role" is saved against in the template in the database. So i could use this code to find the object then copy all the form fields into it and save it but it seems long winded when the first option does actually work but it just does not save my list. Any comments or suggestions how best to save the changes to the object and its associated list of different objects? Thanks.
UPDATE
I found this worked for me, but i have no idea if its the best way to go about it.
db.Entry(ct).State = EntityState.Modified;
foreach (var entity in db.Roles.Where(cr => cr.TemplateID == ct.ID))
db.Roles.Remove(entity);
foreach (Role cr in ct.Roles)
db.Roles.Add(cr);
db.SaveChanges();
This worked for a similar problem for me !!
foreach (var entity in db.Roles.Where(cr => cr.TemplateID == ct.ID)) {
db.Entry(entity).State = EntityState.Modified;
}
db.Roles.Remove(entity);
Are the objects present in ctemplate in the save procedure? In your view are you also doing this:
foreach (var item in model.sub_object)
{
#Html.TextBoxFor(m => item.whatever)
}
Try something like this:
for (int i; i < model.sub_object.count(); i++)
{
#Html.TextBoxFor(m => model.sub_object[i].field)
}
There's not a fast way that I'm aware of. When you fetch your Template from the database, you want to load the associated roles as well (look up Eager Loading); that's not related to your problem but will save you on db round-trips.
Once your Roles are loaded by the context, you'll have to iterate over them and modify whatever's changed and add new ones and delete the old ones. It's manual, but it ensures that the datacontext knows what you want changed on the objects.
After that, calling save on the template should trickle down to the roles.
If your problem is what I think it is, this will solve it. The problem is that you need to change the EntityState of each element on your lists. Try this:
public ActionResult Edit(Template ctemplate)
{
db.Entry(ctemplate).State = EntityState.Modified;
foreach (CorrespondenceRole role in ctemplate.Roles)
{
//Change role State -> EntityState.Modified
}
db.SaveChanges();
}
If your elements are attached to your context, they should persist to the database... Let me know if you have any question...
Hope this helps.
Related
This should be really simple but I think I'm having possible issues with my model. I have been working with linq over a year and I should have this simple remove easily done. Please help! It's removing both records from the database when I only want one deleted
I have a database table with these properties.
Email, EmployeeName, StoreId
jsch#m.com,Joe Schneider,9
jsch#m.com,Joe Schneider,8
I need to delete Joe Schneider with storeId 9
So I run this simple query and remove process.
var temp2 = difference[i];
var PersonToRemove = db.Permissions.SingleOrDefault(s => s.EmployeeName == temp2 && s.StoreId == Persons.StoreId);
if (PersonToRemove.EmployeeName != null)
{
db.Permissions.Remove(PersonToRemove);
db.SaveChanges();
}
I am assuming one is going to say, hey your model is not right and don't put the name as a key, but I can't just be changing the model because other parts of the app are based on this model and would cause huge breaks. Could you give me advise how to edit the linq query to not delete both records?
model
[Table("Permissions")]
public class Permissions
{
[Key]
public String EmployeeName { get; set; }
public string Department { get; set; }
public int? StoreId { get; set; }
public String Email { get; set; }
}
[Table("Permissions")]
public class Permissions
{
[Key]
public String EmployeeName { get; set; }
}
The problem is here you are defining a primary key which has no length constraint on it. (MaxLength). This leads to EF generate a column with NVARCHAR(MAX). As mentioned here VARCHAR(MAX) columns are not allowed to be primary key. So correct definition should be like below
[Table("Permissions")]
public class Permissions
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)] <--
[MaxLength(255)] // <---
public String EmployeeName { get; set; }
}
Edit: You need to recreate the database in order to associated tables initialized with correct settings.
Edit 2 : Also you may need a DatabaseGenerated(DatabaseGeneratedOption.None) since its not identity column.
you can set Deleted state on individual entity like so:
var temp2 = difference[i];
var PersonToRemove = db.Permissions.SingleOrDefault(s => s.EmployeeName == temp2 && s.StoreId == Persons.StoreId);
if (PersonToRemove.EmployeeName != null)
{
db.Entry(PersonToRemove).State = EntityState.Deleted; // do this instead
db.SaveChanges();
}
EF should then figure out which entity you wanted to delete
UPD
I am assuming you are using EF6 and DB-first approach. I am also assuming you've got your DB context class set up with default convention model builder. It seems EF's default object tracking based on Key will not work as your key is not unique (this is a bigger problem, but I understand you're already aware of that).
You might try circumvent that convention by adding custom model builder configuration like so:
class MyDbContext : DbContext {
public virtual DbSet<Permissions> Permissions {get;set;}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Permissions>().HasKey(p => new { p.EmployeeName, p.StoreId});
}
}
since you didn't share your DbContext definition this is just a snippet but hopefully gives you some ideas to explore.
this is the API reference: https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.modelconfiguration.entitytypeconfiguration-1?view=entity-framework-6.2.0
I have a Customer class that has a relationship to an Address class:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
}
public class Address
{
public int Id { get; set; }
public string Street1 { get; set; }
//Snip a bunch of properties
public virtual Customer Customer { get; set; }
}
I have an edit form which displays all the fields for both the customer and address. When this form is submitted, it calls the Edit method in the controller:
public ActionResult Save(Customer customer)
{
if (!ModelState.IsValid)
{
var viewModel = new CustomerFormViewModel
{
Customer = customer,
CustomerTypes = _context.CustomerTypes.ToList()
};
return View("CustomerForm", viewModel);
}
if (customer.Id == 0)
_context.Customers.Add(customer);
else
{
var existingCustomer = _context.Customers
.Include(c => c.Addresses)
.Single(c => c.Id == customer.Id);
existingCustomer.Name = customer.Name;
existingCustomer.TaxId = customer.TaxId;
existingCustomer.CustomerTypeId = customer.CustomerTypeId;
existingCustomer.CreditLimit = customer.CreditLimit;
existingCustomer.Exempt = customer.Exempt;
existingCustomer.Addresses = customer.Addresses;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customers");
}
This doesn't work and creates duplicate entries in the Addresses table in the DB. I think I understand why (EF isn't smart enough to know the Addresses inside the collection need to be added/modified/deleted as the case may be). So, what is the best way to fix this?
My instinct is that I need to iterate over the Addresses collections and compare them manually, adding any new ones from the form that don't exist for the customer, updating ones that do exist, and deleting ones that were not sent by the form but exist in the DB for the customer. Something like (ignoring the delete functionality for now):
foreach(Address address in customer.Addresses)
{
if (address.Id == 0)
// Add record
else
// Fetch address record from DB
// Update data
}
// Save context
Is this the best way to go about this, or are there any EF tricks to iterating and syncing a child collection to the DB?
Oh, and one question which has me scratching my head - I can sort of understand how a new address record is getting created in the DB, but what I don't get is the existing address record is also updated to have its customer_id set to NULL...how the heck does that happen? That leads me to believe that EF does see the original address record is somehow linked (as it is modifying it) but it's not smart enough to realize the record I'm passing in should replace it?
Thanks -- also, this is EF6 and MVC5
The problem comes from the line
existingCustomer.Addresses = customer.Addresses;
in your code. This like assigns field Addresses from customer coming from the model. So far ok. The point is that customer does not have any relation to the database model at this point (it's not coming from the database but from the view).
If you would like to update existingCustomer.Addresses with the data coming from the model, you need to merge the data instead of replacing it. The following "pseudo code" might give you a direction:
void MergeAddresses(var existingAddresses, var newAddresses) {
foreach(var address in newAddresses) {
if (existingAddresses.Contains(newAddress)) {
// merge fields if applicable
}
else {
// add field to existingAddresses - be ware to use a "cloned" list
}
}
// now delete items from existing list
foreach (var address in existingAddresses.CloneList()) {
if (!newAddresses.Contains(address)) {
// remove from existingAddresses
}
}
}
Is this the best way to go about this, or are there any EF tricks to iterating and syncing a child collection to the DB?
No, there aren't such tricks. EF designers left saving detached entities totally up to us - the developers.
However there is a package called GraphDiff which is addressing that, so you could give it a try. Here is how your code would look like using it:
using RefactorThis.GraphDiff;
...
_context.UpdateGraph(customer, map => map.OwnedCollection(
e => e.Addresses, with => with.AssociatedEntity(e => e.Customer)));
_context.SaveChanges();
I have a teacher entity that has a child entity of ICollection called YogaClasses. When I modify the list of YogaClasses for a teacher and save, entity framework adds the new rows of YogaClasses to the table 'YogaClass' but it doesn't remove or edit the old rows. So I'm left with double the data. Shouldn't "context.SaveChanges()" in my repo be smart enough to know to delete removed classes and add the new ones (edit), not just add new ones?
In my controller I have something like this for a Teacher edit.
string userId = User.Identity.GetUserId();
Teacher teacher = teacherRepository.Find(userId);
//other code left out here
teacher.YogaClasses = GetSelectedClasses(Request.Form[2]);
// other stuff here
teacherRepository.Save();
In my reposity I have this;
public void Save()
{
context.SaveChanges();
}
Here is my teacher and YogaClass entity
public class Teacher
{
public int TeacherId { get; set; }
public virtual ICollection<YogaClass> YogaClasses { get; set; }
}
public class YogaClass
{
public int YogaClassId { get; set; }
[Index]
[Required]
public int TeacherRefId { get; set; }
[ForeignKey("TeacherRefId")]
public virtual Teacher Teacher { get; set; }
}
The problem here is the relationship has not been loaded at the time you set teacher.YogaClasses. So when you set teacher.YogaClasses, it assumes the new relationship will be added. (lazyloading is still a bit late in this case). The solution is somehow you ensure to load all the relationships first (using either Include or some fake access to trigger lazyloading first), then you set the new value normally:
//fake access to trigger lazy loading (of course this works only if
//lazy loading is enabled)
var someClasses = teacher.YogaClasses;
teacher.YogaClasses = GetSelectedClasses(Request.Form[2]);
You can also clear the old classes and add new ones with a foreach loop:
teacher.YogaClasses.Clear();
foreach(var c in GetSelectedClasses(Request.Form[2])){
teacher.YogaClasses.Add(c);
}
I have a dirty solution but it doesn't seem like it's the best one.
before I save the teacher context in my repo I can call this
public void DeleteYogaClasses(Teacher teacher)
{
foreach (var yogaClass in teacher.YogaClasses.ToList())
{
context.Entry(yogaClass).State = EntityState.Deleted;
}
context.SaveChanges();
}
you need to use Attach method of the context and pass in your existing teacher object and then make the changes and call SaveChanges method. From your code it is not clear what is your context, so hard to give working code. Here is a link explaining how to add/modify
You said you need to modify YogaClasses, so from this what I assume is a Teacher object already has some YogaClasses entries and you want to update some of those entries. What you need to do is, have a list of ids of YogaClasses that you need to modify then iterate on that ids list and in that iteration loop find the existing yogaClass and attach it to the context, modify it and then call save changes (preferablly when all the changes are done so that it is not a performance hit)
Here is a suodocode for this
UpdateTeacher(int teacherId)
{
var teacher = teacherRepository.Find(teacherId);
UpdateYoga(teacher);
}
private void UpdateYoga(Teacher teacher)
{
foreach(var yoga in teacher.YogaClasses)
{
db.Context.Attach(yoga);
yoga.YogaStyle = whatEverValue;
}
db.context.SaveChanges();
}
Started a new ASP.NET MVC 3 application and getting the following error:
The primary key value cannot be deleted because references to this key
still exist.
How to solve this?
Models (EF code-first)
public class Journal
{
public int JournalId { get; set; }
public string Name { get; set; }
public virtual List<JournalEntry> JournalEntries { get; set; }
}
public class JournalEntry
{
public int JournalEntryId { get; set; }
public int JournalId { get; set; }
public string Text { get; set; }
}
Controller
//
// POST: /Journal/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Journal journal = db.Journals.Find(id);
db.Journals.Remove(journal);
db.SaveChanges(); // **exception occurs here**
return RedirectToAction("Index");
}
DB Setup
public class FoodJournalEntities : DbContext
{
public DbSet<Journal> Journals { get; set; }
public DbSet<JournalEntry> JournalEntries { get; set; }
}
Found the solution:
public class FoodJournalEntities : DbContext
{
public DbSet<Journal> Journals { get; set; }
public DbSet<JournalEntry> JournalEntries { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Journal>()
.HasOptional(j => j.JournalEntries)
.WithMany()
.WillCascadeOnDelete(true);
base.OnModelCreating(modelBuilder);
}
}
Source
If you delete a record from a table(lets say "blah"), which has other relationships with other tables (xyz,abc). By default, the database will prevent you from deleting a row in "blah" if there are related rows in one of the other tables.
Solution #1:
You can manually delete the related rows first,this may require a lot of work.
Solution #2:
an easy solution is to configure the database to delete them automatically when you delete a "blah" row.
Follow this open your Database diagram,and click on the properties on the relationship
In the Properties window, expand INSERT and UPDATE Specification and set the DeleteRule property to Cascade.
Save and close the diagram. If you're asked whether you want to update the database, click Yes.
To make sure that the model keeps entities that are in memory in sync with what the database is doing, you must set corresponding rules in the data model. Open SchoolModel.edmx, right-click the association line between "blah" and "xyz", and then select Properties.
In the Properties window, expand INSERT and UPDATE Specification and set the DeleteRule property to Cascade.
Solution and images taken from http://www.asp.net/web-forms/tutorials/getting-started-with-ef/the-entity-framework-and-aspnet-getting-started-part-2
In EF Core (3.1.8), the syntax is a bit different than the accepted answer but the same general idea, what worked for me is below:
modelBuilder.Entity<Journal>()
.HasMany(b => b.JournalEntries)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
In your query to select the item to delete or remove from the database you want to make sure that you are explicitly including the items as well, otherwise it will continue to throw a FK error, something like below.
var item = _dbContext.Journal.Include(x => x.JournalEntries).SingleOrDefault(x => x.Id == id);
I found it...
Go TO SQL Server
Make his Database diagrammed
Right click on relation ship line between parent and child and open the property of it.
Set INSERT And Update Specification and simply set DELETE RULE TO CASCADE.
Remember No code is required in Project FOR this PURPOSE and simply debug and enjoy it.
Was trying out some code with the EF "code first" method and ran into a strange problem.
My datacontext:
public class BookmarkerDataContext : DbContext
{
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().HasKey(u => u.UserId);
base.OnModelCreating(modelBuilder);
}
}
Where the user object is:
public class User
{
public long UserId { get; set; }
public ICollection<Tag> Tags { get; set; }
}
In my code I am doing something fairly simple:
public void UpdateUserTags(User user,ICollection<Tag> taglist)
{
user.Tags = new List<Tag>(user.Tags.Union(taglist));
datacontext.Users.Add(user);
datacontext.SaveChanges();
}
The user object I am passing to this function is the result of something like:
datacontext.Users.SingleOrDefault(u => u.UserId==id)
Everytime I call the UpdateUserTags function it seems to create a new Row in the User table instead of updating it. Am I doing something wrong here?
#Donald is correct, you need to Attach to the ObjectContext when making updates.
However, that is only if your entity is detached.
If sounds like you have already retrieved the single entity from the graph:
var user = datacontext.Users.SingleOrDefault(u => u.UserId==id);
Which means you don't need to Attach or get it again. Just do this:
var user = datacontext.Users.SingleOrDefault(u => u.UserId==id);
user.Tags = new List<Tag>(user.Tags.Union(taglist));
context.SaveChanges();
However, i wouldn't recommend replacing the entire Tags collection, add the tags:
user.Tags.Add(someTag);
HTH
I believe you want to Attach your object to the data context, instead of Adding it.
public void UpdateUserTags(User user,ICollection<Tag> taglist)
{
datacontext.Attach(user);
user.Tags = new List<Tag>(user.Tags.Union(taglist));
datacontext.SaveChanges();
}
Once it is attached, then the context becomes aware of the object. Once you save changes, they should be persisted to the database.
Won't this line
datacontext.Users.Add(user);
Always mark the user record as needing to be ADDED to the users table.
I think you have to DETACH the user from the old context and ATTACH it to the new to properly be able to update the record.
but I'm no EF wonk so ymmv
This has nothing to do with your particular issue but for lazy loading you're going to want Tags to be marked virtual
public virtual ICollection<Tag> Tags { get; set; }
Also, it looks like you're trying to add new tags for a user (or update their tags) if that is the case then you're going to want to use Attach as Donald suggested