更新 EF6 中的实体 child collection
Updating entity child collection in EF6
我有一个名为 Driver 的模型,其中包含 'DriverQualifications' 的列表,并且在更新时我想要 add/remove/update 当前 DriverQualifications 的值。
我目前尝试通过首先清除列表并读取所有元素来更新:
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();
}
}
这导致“违反多重性约束。关系 'COMP1690Model.DriverQualifications_ibfk_1' 的角色 'Drivers' 具有多重性 1 或 0..1。'
我如何向数据库添加值:
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 型号:
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; }
}
Driver资质型号:
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; }
}
很确定您的问题与 EF 上下文 loads/tracks 实体的方式有关。
此代码:
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();
}
执行以下操作:
1) 加载驱动程序及其所有驱动程序资格(以及他们的每个资格)。
2) 清除当前驱动程序的资格。
3) 循环传入的资格
4) 为当前驱动程序添加 "new" 资格。
我认为这个问题与#2 和#4 有关。即使通过你 "Clear" 的资格,它们仍然被 EF 上下文引用。当您到达 #4 并尝试再次包含它们时,您会看到所看到的多重性错误。
我不完全确定这会解决您的问题,因为我以前从未尝试过这种方法,但我很好奇您是否要遍历资格列表并在上下文中手动将其状态设置为如果可以解决您的问题,请删除。
所以,而不是:
d.DriverQualifications.Clear();
改为执行此操作(在 foreach 循环内):
db.Entry(d).State = System.Data.Entity.EntityState.Deleted;
再次......不能保证这会起作用,但我认为你将不得不做一些这种性质的事情来处理在你的初始获取请求期间附加到上下文的实体。
处理 EF 和引用时(DriverQualification -> Qualification)使用引用,而不是 FK。事实上,我通常建议甚至不要向实体添加 FK,而是在实体配置中使用影子属性 (EF Core) 或 .Map()
以避免它们可访问。您面临的问题是 EF 仍在跟踪引用特定 Qualification 的 DriverQualification 实体,因此将 Qualification 设置为 null 并更新 FK 并没有真正起作用。
所以您要传回一个驱动程序,想要重新加载该驱动程序实体,并根据传入的驱动程序更新其资格。
假设传入的驱动程序来自客户端(Web 应用程序等)并且已经被修改,我们不能 "trust" 它,或者它是参考数据,所以你最好重新加载它而不是将其重新附加到上下文中。
编辑: 我建议使用 ViewModel 而不是传递实体,即使您不打算信任它。传递实体的主要风险是它可能会 re-attach/use 它或更新时引用的实体。我不得不再次检查这个答案,因为我认为在获得更新的资格时我违反了那个规则! :) 例如,将实体图传递给客户端浏览器也会暴露比您应有的更多关于您的域的信息。即使您不显示各种 columns/fks/reference 数据,客户也可以使用调试工具查看此数据。传输的数据也比可能需要的多。 Automapper 可以使转置实体以快速查看模型,并且也可以与 IQueryable
一起使用。 (ProjectTo
)。 /编辑
为了清楚起见,我重命名了一些变量。(即 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();
}
这假设我们要删除不再与驱动程序关联的资格关联,并添加任何尚未关联的新资格。 (保留任何不变的资格。)
你可以找到的 LamdaComparer here
基本上,要避免 reference/key 问题,请坚持更新引用并完全忽略 FK。对于 entities/contexts 我需要进行大量 "raw" 更新的地方,我将只声明 FK 并放弃添加性能引用。
我有一个名为 Driver 的模型,其中包含 'DriverQualifications' 的列表,并且在更新时我想要 add/remove/update 当前 DriverQualifications 的值。
我目前尝试通过首先清除列表并读取所有元素来更新:
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();
}
}
这导致“违反多重性约束。关系 'COMP1690Model.DriverQualifications_ibfk_1' 的角色 'Drivers' 具有多重性 1 或 0..1。'
我如何向数据库添加值:
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 型号:
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; }
}
Driver资质型号:
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; }
}
很确定您的问题与 EF 上下文 loads/tracks 实体的方式有关。
此代码:
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();
}
执行以下操作:
1) 加载驱动程序及其所有驱动程序资格(以及他们的每个资格)。
2) 清除当前驱动程序的资格。
3) 循环传入的资格
4) 为当前驱动程序添加 "new" 资格。
我认为这个问题与#2 和#4 有关。即使通过你 "Clear" 的资格,它们仍然被 EF 上下文引用。当您到达 #4 并尝试再次包含它们时,您会看到所看到的多重性错误。
我不完全确定这会解决您的问题,因为我以前从未尝试过这种方法,但我很好奇您是否要遍历资格列表并在上下文中手动将其状态设置为如果可以解决您的问题,请删除。
所以,而不是:
d.DriverQualifications.Clear();
改为执行此操作(在 foreach 循环内):
db.Entry(d).State = System.Data.Entity.EntityState.Deleted;
再次......不能保证这会起作用,但我认为你将不得不做一些这种性质的事情来处理在你的初始获取请求期间附加到上下文的实体。
处理 EF 和引用时(DriverQualification -> Qualification)使用引用,而不是 FK。事实上,我通常建议甚至不要向实体添加 FK,而是在实体配置中使用影子属性 (EF Core) 或 .Map()
以避免它们可访问。您面临的问题是 EF 仍在跟踪引用特定 Qualification 的 DriverQualification 实体,因此将 Qualification 设置为 null 并更新 FK 并没有真正起作用。
所以您要传回一个驱动程序,想要重新加载该驱动程序实体,并根据传入的驱动程序更新其资格。
假设传入的驱动程序来自客户端(Web 应用程序等)并且已经被修改,我们不能 "trust" 它,或者它是参考数据,所以你最好重新加载它而不是将其重新附加到上下文中。
编辑: 我建议使用 ViewModel 而不是传递实体,即使您不打算信任它。传递实体的主要风险是它可能会 re-attach/use 它或更新时引用的实体。我不得不再次检查这个答案,因为我认为在获得更新的资格时我违反了那个规则! :) 例如,将实体图传递给客户端浏览器也会暴露比您应有的更多关于您的域的信息。即使您不显示各种 columns/fks/reference 数据,客户也可以使用调试工具查看此数据。传输的数据也比可能需要的多。 Automapper 可以使转置实体以快速查看模型,并且也可以与 IQueryable
一起使用。 (ProjectTo
)。 /编辑
为了清楚起见,我重命名了一些变量。(即 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();
}
这假设我们要删除不再与驱动程序关联的资格关联,并添加任何尚未关联的新资格。 (保留任何不变的资格。)
你可以找到的 LamdaComparer here
基本上,要避免 reference/key 问题,请坚持更新引用并完全忽略 FK。对于 entities/contexts 我需要进行大量 "raw" 更新的地方,我将只声明 FK 并放弃添加性能引用。