I'm trying to set up an many to many relationship with groups and students, so students can be assigned to many groups and groups can have many students assigned to it. I keep getting an error when I go to call context.savechanges(). In my objects I do have proper configuration, virtual ICollection in both. My configuration is as follows:
public class DataContext : DbContext
{
public DbSet<Student> StudentsContext { get; set; }
public DbSet<Group> GroupsContext { get; set; }
public DbSet<Phase> PhaseContext { get; set; }
public DbSet<Admin> AdminContext { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Student>()
.HasMany(g => g.Groups)
.WithMany(s => s.GroupMembers)
.Map(x => x.MapLeftKey("StudentId")
.MapRightKey("GroupId")
.ToTable("Student_XRef_Group"));
}
}
Then in controller just as a test I would try:
var phase = phaseRepository.Select().SingleOrDefault(x => x.PhaseId == phaseId);
phase.Groups.Clear();
//Testing
Group testGroup = new Group();
testGroup.GroupNumber = 1;
testGroup.GroupMembers.Add(AllStudents[0]); //Students of type Student
phase.Groups.Add(testGroup);
//Testing
context.SaveChanges();
Then when it reaches context.savechanges I get the following error:
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.
** SOLVED **
Turns out that by me calling phase.Groups.clear() was the problem, why I do not know. I was hoping maybe can now tell me why?
Calling Clear() on your collection in this case only attempts to detach the relationship between the Phase and the Group and not actually delete the objects. You're therefore attempting to set the foreign key reference for each one to null, hence the non-nullable exception.
I can understand where the confusion comes from, after all we can use phase.Groups.Add(...) to add a new entity but it's important to remember that it does not work the other way around for Remove() and Clear().
To achieve what you want, you can use DeleteObject() on each Group to want to delete:
context.Groups.DeleteObject(groupToDelete);
Related
I use Entity framework 6 in my projects and I always have doubts regarding some of the concepts which are used to delete objects using EF.
I still don't know which one works in which scenario. I just try all and if one works I leave it until the code is working. But no wi need to understand this concept once and for all. I did my research my unable to understand the concept clearly.
I have a domain class in EF which have multiple referencing entities. For example. I have a domain class called Course and It has multiple referencing objects mentioned below in the code.
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int CompanyId { get; set; }
public virtual Company Company { get; set; }
public virtual PricingSchedule PricingSchedule { get; set; }
public virtual ICollection<CustomerCourse> AssignedCustomers { get; set; }
public virtual ICollection<License> Licenses { get; set; }
public virtual ICollection<GroupLicense> GroupLicenses { get; set; }
public virtual ICollection<GroupCourse> GroupCourses { get; set; }
public virtual ICollection<Learner> Learners { get; set; }
}
Now I have to delete the course from the DB with all of its referencing entities. For example, If the course is deleting then its properties like AssignedCustomers, Licenses etc all must be deleted.
But I don't understand one thing using Entity framework.
For deleting an entity from DB we have multiple options like.
Remove
RemoveRange
EntityState.Deleted
Sometimes Remove works but sometime RemoveRange Works and sometime Entitystate.Deleted works. Why?
My code is for deleting a Course
var courses = _context.Courses
.Include("AssignedCustomers")
.Include("PricingSchedule")
.Include("Licenses")
.Include("GroupCourses")
.Include("GroupLicenses")
.Where(e => courseIds.Contains(e.Id)).ToList();
if (courses != null && courses.Count > 0)
{
courses.ForEach(currentCourse =>
{
_context.Entry(currentCourse.PricingSchedule).State = EntityState.Deleted;
Sometime remove range works and code run successfully
_context.CustomerCourses.RemoveRange(currentCourse.AssignedCustomers);
Below line of code gives me error but in other scenario it works why?
//currentCourse.AssignedCustomers.ToList().ForEach(ac =>
//{
// //currentCourse.AssignedCustomers.Remove(ac);
// _context.Entry(ac).State = EntityState.Deleted;
//});
_context.Entry(currentCourse).State = EntityState.Deleted;
});
}
_context.SaveChanges();
Can anyone explain to me the difference in which situation I should use what?
The error I receive most of the time is
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.
This error comes up when I use this piece of code
currentCourse.AssignedCustomers.ToList().ForEach(ac =>
{
_context.Entry(ac).State = EntityState.Deleted;
});
OR
currentCourse.AssignedCustomers.ToList().ForEach(ac =>
{
currentCourse.AssignedCustomers.Remove(ac):
});
after that when I hit SaveChanges The error comes up.
You need to set up the cascade rules in your schema and within Entity Framework so that it knows which related entities will be deleted when you go to delete a course. For instance you will want to cascade delete while others like Learner would likely have a null-able key which can be cleared if a course is removed.
Provided it is set up correctly, you should just need to use: context.Courses.Remove(course); and the related entities will be removed or disassociated automatically. Start with a simpler example of your parent-child relationships, one child to cascade delete, another to disassociate with a nullable FK. Your current example looks to also have many-to-many associations (GroupCourses) so depending on the mapping/relationships the approach will vary.
I have an MVC application that uses Entity Framework v6. We have a class
public class ChildObject
{
public string Name { get; set; }
....
}
that maps to a table in the database. This table has 6 rows that are never changed. Neither will there ever be any additions. We have a second class defined along the lines of the following:
public class ParentClass
{
public int ChildObjectId { get; set; }
public ChildObject ChildObject { get; set; }
....
}
Whenever a ParentClass object is created or updated the logic only references the ChildObjectId property. The ChildObject property is only referenced when data is pulled back for viewing. However about once per month an extra row appears in the ChildObject table that is a duplicate of an existing row. This obviously causes issues. However I can't see how this could happen seeing as we only ever save using the Id value. Any thoughts on how this could be occurring would be very much appreciated.
The typical culprit for behavior like you describe is when a new child entity is composed based on existing data and attached to the parent rather than the reference associated to the context. An example might be that you load child objects as a set to select from, and send the data to your view. The user wants to change an existing child reference to one of the 6 selections. The call back to the server passes a child object model where there is code something like:
parent.ChildObject = new ChildObject{ Name = model.Name, ... }
rather than:
var child = context.Children.Single(x => x.Id = model.ChildObjectId);
parent.ChildObject = child;
Depending on how your domain is set up you may run into scenarios where the EF context creates a new child entity when a navigation property is set. Check with a FindUsages on the ChildObject property and look for any use of the setter.
In general you should avoid combining the use of FK properties (ChildObjectId) with navigation properties (ChildObject) because you can get confusing behavior between what is set in the navigation reference vs. the FK. Entities should be defined with one or the other. (Though at this time EF Core requires both if Navigation properties are used.)
A couple notables from your example:
Mark the navigation property as virtual - This ensures that EF assigns a proxy and recognizes it.
Option A - Remove the FK child ID property. For the parent either use an EntityTypeConfiguration or initialize the DbContext to map the FK column:
EntityTypeConfiguration:
public class ParentClassConfiguration : EntityTypeConfiguration<ParentClass>
{
public ParentClassConfiguration()
{
ToTable("ParentTable");
HasKey(x => x.ParentObjectId)
.Property(x => x.ParentObjectId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(x => x.ChildObject)
.WithMany()
.Map(x => x.MapKey("ChildObjectId"));
.WillCascadeOnDelete(false);
}
}
or on context model generation: (Inside your DbContext)
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<ParentObject>().HasRequired(x => x.ChildObject).WithMany().Map(x => x.MapKey("ChildObjectId")).WillCascadeOnDelete(false);
}
or Option B - Ensure the FK is linked to the reference, and take measures to ensure that the two are always kept in sync:
public class ParentClassConfiguration : EntityTypeConfiguration<ParentClass>
{
public ParentClassConfiguration()
{
ToTable("ParentTable");
HasKey(x => x.ParentObjectId)
.Property(x => x.ParentObjectId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(x => x.ChildObject)
.WithMany()
.HasForeignKey(x => x.ChildObjectId));
.WillCascadeOnDelete(false);
}
}
or on context model generation: (Inside your DbContext)
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<ParentObject>().HasRequired(x => x.ChildObject).WithMany().HasForeignKey(x => x.ChildObjectId)).WillCascadeOnDelete(false);
}
Option B is the only one currently available with EF Core to my knowledge, and it may help mitigate your issue but you still have to take care to avoid discrepancies between the navigation property and the FK. I definitely recommend option A, though it will likely require a bit of change if your code is commonly accessing the FK column.
I have this domain:
public class Phone {
public int Id { get; set; }
public string Number { get; set; }
public Person Person { get; set; }
}
public class Person {
public int Id { get; set; }
public IList<Phone> Phones { get; set; }
}
I load a Person and clear its Phones. But the operation cause an error:
// actually loads person from repository...
var person = _personRepository.Include(p => p.Phones).Where(p => p.Id == 1).First();
person.Phones.Clear();
_personRepository.Update(person);
Above you can see the simpled logic of loading a Person and clearing its Phones. But this error occurs:
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.
Actually I want to clear all of Person.Phones and add some new items. But I want to clearing them in one query, not delete them one by one.
Have you any idea? Can you help me please? Thanks in advance.
You can't generate set based SQL in EF. So there's no way in EF to generate a single SQL statement that deletes all Phone records given a Person.Id.
You can write the SQL yourself and pass it to either ObjectContext.ExecuteStoreCommand or DbContext.Database.ExecuteSqlCommand depending on your model.
foreach(var phone in person.Phones)
{
context.DeleteObject(phone);
}
person.Phones.Clear();
may help.
Say I have two entities like so:
public class Response
{
public int Id { get; set; }
public int PatientId { get; set; }
public virtual Patient Patient { get; set; }
public string Text { get; set; }
}
public class Patient
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Response> Responses { get; set; }
}
I want to be able to call
Patient.Responses.Remove(someResponse);
And have entity delete not only the relationship but the Response entity as well. At present if I just delete the relationship I get the following error:
System.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.
Reading this blog post http://blogs.msdn.com/b/dsimmons/archive/2010/01/31/deleting-foreign-key-relationships-in-ef4.aspx I realised I can achieve this by having the following mapping:
modelBuilder.Entity<Response>().HasKey(m => new { m.Id, m.PatientId });
But I don't want to change my primary key. What I want to do is override DbContext.SaveChanges() and mark for deletion any Responses where the Patient relationship has been deleted. I tried this:
public override int SaveChanges()
{
// Need to manually delete all responses that have been removed from the patient, otherwise they'll be orphaned.
var orphanedResponses = ChangeTracker.Entries().Where(
e => e.State == EntityState.Modified &&
e.Entity is Response &&
e.Reference("Patient").CurrentValue == null);
foreach (var orphanedResponse in orphanedResponses)
{
Responses.Remove(orphanedResponse.Entity as Response);
}
return base.SaveChanges();
}
But I found it's possible to attach a Response with only Response.PatientId set and not Response.Patient, entity wont have loaded the Response.Patient property so my code thinks it's been orphaned and should be deleted.
In summary
What I want to know is how can I can tell that an entity has been modified because it's FK relationship has been removed.
Use this instead:
public override int SaveChanges()
{
var responses = Responses.Local.Where(r => r.Patient == null);
foreach (var response in responses.ToList())
{
Responses.Remove(response);
}
return base.SaveChanges();
}
You need to configure the mappings such that a cascade delete will occur. To do that you need to map the model with WillCascadeOnDelete to true.
public class MyContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Patient>()
.HasMany(patient=> patient.Responses)
.WithRequired(response => response.Patient)
.HasForeignKey(response => response.PatientId)
.WillCascadeOnDelete(true);
}
}
I think my problem is not with the code but rather with how I assume entity's Attach() method works. I assumed that if I attach a response with PatientId set but not Patient property then entity would populate the Patient property for me.
In fact what I think happens is entity attaches it as it is, then if I mark that entity as modified and save it, entity sees the null Patient property and assumes I want to remove the relationship, so throws an error because it would be orphaned (can't null Response.PatientId). So perhaps everything is working as designed and my SaveChanges() solution works.
Okay, this is getting ridiculous as this is turning out to be much more difficult than it has any right to be.
If I use my original code with no FluentAPI mapping, I have a ParentID field which is not used, and a new field called Node_ID is used.
public class Node {
public long ID { get; private set; }
public long ParentID { get; set; }
public ICollection<Node> Children { get; set; }
}
Here are my various attempts:
protected override void OnModelCreating(DbModelBuilder mb)
{
mb.Entity<Node>()
.HasMany<Node>(h => h.Children)
.WithOptional()
.HasForeignKey(h => h.ParentID);
}
DbUpdateException: Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.
protected override void OnModelCreating(DbModelBuilder mb)
{
mb.Entity<Node>()
.HasMany<Node>(h => h.Children)
.WithOptional()
.Map(m => m.MapKey("ParentID"));
}
MetadataException: Schema specified is not valid. Errors:
(82,6) : error 0019: Each property name in a type must be unique. Property name 'ParentID' was already defined.
[ForeignKey("ParentID")]
public ICollection<Node> Children { get; set; }
protected override void OnModelCreating(DbModelBuilder mb)
{
mb.Entity<Node>()
.HasMany<Node>(h => h.Children)
.WithOptional()
}
DbUpdateException: Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.
Update
Using the Fluent API code from my first attempt code above (.HasForeignKey), and by making ParentID nullable (public long? ParentID), I have gotten the database to successfully map. Is there any way to do this without making the FK nullable? I would like the key to be 0 when no parent exists. If not, oh well, I will deal.
No there is no way to avoid nullable ParentId - you told EF that parent is optional (it must be otherwise you will not be able to use the table) and because of that related FK property must be nullable.