简体   繁体   中英

Updating entity child collection in EF6

I have a model called Driver which contains a list of 'DriverQualifications' and on update I would want to add/remove/update values of current DriverQualifications.

My current attempt to update by first clearing the list and readding all elements:

public void UpdateOne(Driver val)
{
    using (var db = new COMP1690Entities())
    {
        Driver d = db.Drivers.Where((dr) => dr.Id == val.Id).Include("DriverQualifications.Qualification").FirstOrDefault();
        d.DriverQualifications.Clear();
        foreach (DriverQualification q in val.DriverQualifications)
        {
            q.Fk_Qualifications_Id = q.Qualification.Id;
            q.Qualification = null;
            d.DriverQualifications.Add(q);
        }
        d.Phone_Number = val.Phone_Number;
        db.SaveChanges();
    }
}

This results in 'Multiplicity constraint violated. The role 'Drivers' of the relationship 'COMP1690Model.DriverQualifications_ibfk_1' has multiplicity 1 or 0..1.'

How I'm adding values to the DB:

    public void CreateOne(Driver val)
    {
        using (var db = new COMP1690Entities())
        {
            foreach(DriverQualification q in val.DriverQualifications)
            {
                q.Fk_Qualifications_Id = q.Qualification.Id;
                q.Qualification = null;
            }
            db.Drivers.Add(val);
            db.SaveChanges();
        }
    }

Driver model:

public partial class Driver
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
    public Driver()
    {
        this.DriverQualifications = new HashSet<DriverQualification>();
        this.DriverTrainings = new HashSet<DriverTraining>();
    }

    public int Id { get; set; }
    public string Phone_Number { get; set; }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<DriverQualification> DriverQualifications { get; set; }
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<DriverTraining> DriverTrainings { get; set; }
}

DriverQualification model:

public partial class DriverQualification
{
    public int Id { get; set; }
    public Nullable<System.DateTime> Expiry_Date { get; set; }
    public int Fk_Driver_Id { get; set; }
    public int Fk_Qualifications_Id { get; set; }

    public virtual Driver Driver { get; set; }
    public virtual Qualification Qualification { get; set; }
}

pretty sure your problem is related to the way the EF context loads/tracks entities.

This code:

using (var db = new COMP1690Entities())
    {
        Driver d = db.Drivers.Where((dr) => dr.Id == val.Id).Include("DriverQualifications.Qualification").FirstOrDefault();
        d.DriverQualifications.Clear();
        foreach (DriverQualification q in val.DriverQualifications)
        {
            q.Fk_Qualifications_Id = q.Qualification.Id;
            q.Qualification = null;
            d.DriverQualifications.Add(q);
        }
        d.Phone_Number = val.Phone_Number;
        db.SaveChanges();
    }

does the following:

1) Loads the driver and all it's driver qualifications (and each of their qualifications).

2) Clears the qualifications from the current driver.

3) Loops through the incoming qualifications

4) Adds the "new" qualification to the current driver.

I believe the issue is related to #2 and #4. Even through you "Clear" the qualifications, they are still being references by the EF context. When you get to #4 and attempt to include those again, you get the multiplicity error that you are seeing.

I'm not entirely sure this will solve your issue as I've never tried this approach before, but I'm curious if you were to loop through the list of qualifications and manually set it's state in the context to deleted if that would resolve your problem.

So, instead of:

d.DriverQualifications.Clear();

Do this instead (inside a foreach loop):

db.Entry(d).State = System.Data.Entity.EntityState.Deleted;

Again...can't guarantee this will work, but I think you are going to have to so something of this nature to deal with the entities that are attached to the context during your initial get request.

When dealing with EF and references (DriverQualification -> Qualification) use the references, not the FKs. In fact, I generally advise not to even add FKs to the entities, but rather use shadow properties (EF Core) or .Map() in the entity configuration to avoid having them accessible. The issue you are facing is that EF is still tracking DriverQualification entities that reference a particular Qualification, so setting a Qualification to null and updating a FK doesn't really work.

So you're passing back a Driver, want to load that driver entity fresh, and update their qualifications based on the passed in driver.

Assuming that the driver passed in came from a client (web app, etc.) and has been modified, we cannot "trust" it, or it's reference data, so it's good that you are loading it fresh rather than re-attaching it to the context.

edit: I recommend using a ViewModel rather than passing an entity, even if you don't intend to trust it. The main risk of passing the entity is that it can be tempting to re-attach/use it, or referenced entities when updating. I had to second-check this answer because I thought I'd broken that rule when getting the updated Qualifications! :) Passing entity graphs to a client browser for instance also exposes more information about your domain than you should. Even if you don't display various columns/fks/reference data, clients can view this data using debugging tools. It's also more data across the wire than may be needed. Automapper can make transposing entities to view models a snap, and works with IQueryable as well. ( ProjectTo ). /edit

I've renamed some of the variables for clarity.. (Ie val => updatedDriver)

using (var context = new COMP1690Entities())
{
    var updatedQualificationIds = updatedDriver.DriverQualifications.Select(dq => dq.Qualification.Id).ToList();
    // Get the updated qualification entities from the DB.
    var updatedQualifications = context.Qualifications.Where(q => updatedQualificationIds.Contains(q.Id)).ToList();

    var driver = context.Drivers.Where(d => d.Id == updatedDriver.Id)
        .Include(d => d.DriverQualifications)
        .Include("DriverQualifications.Qualification").Single();

    var driverQualificationsToRemove = driver.DriverQualifications
        .Where(dq => !updatedQualificationIds.Contains(udq.Qualification.Id));

    foreach(var driverQualification in driverQualificationsToRemove)
        driver.DriverQualifications.Remove(driverQualification);

    var driverQualificationsToAdd = updatedDriverQualifications
        .Except(driver.DriverQualifications.Select(dq => dq.Qualification),
            new LamdaComparer((q1,q2) => q1.Id == q2.Id))
        .Select(q => new DriverQualification { Qualification = q })
        .ToList();

    driver.DriverQualifications.AddRange(driverQualificationsToAdd);

    driver.PhoneNumber = updatedDriver.PhoneNumber;

    context.SaveChanges();
}

This assumes that we want to remove qualification associations that are no longer associated to the driver, and add any new qualifications not already associated. (leaving any unchanged qualifications.)

The LamdaComparer you can find here

Basically, to avoid reference/key issues, stick with updating references and ignoring FKs entirely. For entities/contexts where I need to do a lot of "raw" updates I will declare just FKs and forgo adding references for performance.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM