当我使用实体框架保存实体时,我自然认为它只会尝试保存指定的实体。但是,它也在尝试保存该实体的子实体。这会导致各种完整性问题。如何强制 EF 仅保存我要保存的实体,从而忽略所有子对象?
如果我手动将属性设置为 null,则会收到错误消息“操作失败:无法更改关系,因为一个或多个外键属性不可为空。”这是非常适得其反的,因为我专门将子对象设置为 null ,因此 EF 将不理会它。
为什么我不想保存/插入子对象?
由于在评论中来回讨论了这一点,我将给出一些理由来说明为什么我希望我的子对象不理会。
在我正在构建的应用程序中,EF 对象模型不是从数据库中加载的,而是用作我在解析平面文件时填充的数据对象。在子对象的情况下,其中许多是指定义父表的各种属性的查找表。例如,主要实体的地理位置。
由于我自己填充了这些对象,因此 EF 假定这些是新对象并且需要与父对象一起插入。但是,这些定义已经存在,我不想在数据库中创建重复项。我只使用 EF 对象进行查找并在我的主表实体中填充外键。
即使子对象是真实数据,我也需要先保存父对象并获取主键,否则 EF 似乎会把事情弄得一团糟。希望这能给出一些解释。
据我所知,你有两个选择。
选项1)
清空所有子对象,这将确保 EF 不添加任何内容。它也不会从您的数据库中删除任何内容。
选项 2)
使用以下代码将子对象设置为从上下文中分离
context.Entry(yourObject).State = EntityState.Detached
请注意,您不能分离 List
/Collection
。您将不得不遍历您的列表并像这样分离列表中的每个项目
foreach (var item in properties)
{
db.Entry(item).State = EntityState.Detached;
}
长话短说:使用外键,它会节省你的一天。
假设你有一个 School 实体和一个 City 实体,这是一个多对一的关系,一个 City 有很多 Schools,一个 School 属于一个 City。并假设 Cities 已经存在于查找表中,因此您不希望在插入新学校时再次插入它们。
最初,您可能会像这样定义实体:
public class City
{
public int Id { get; set; }
public string Name { get; set; }
}
public class School
{
public int Id { get; set; }
public string Name { get; set; }
[Required]
public City City { get; set; }
}
您可以像这样进行 School 插入(假设您已经将 City 属性分配给 newItem):
public School Insert(School newItem)
{
using (var context = new DatabaseContext())
{
context.Set<School>().Add(newItem);
// use the following statement so that City won't be inserted
context.Entry(newItem.City).State = EntityState.Unchanged;
context.SaveChanges();
return newItem;
}
}
在这种情况下,上述方法可能会完美运行,但是,我更喜欢外键方法,它对我来说更加清晰和灵活。请参阅下面的更新解决方案:
public class City
{
public int Id { get; set; }
public string Name { get; set; }
}
public class School
{
public int Id { get; set; }
public string Name { get; set; }
[ForeignKey("City_Id")]
public City City { get; set; }
[Required]
public int City_Id { get; set; }
}
通过这种方式,您明确定义 School 具有外键 City_Id 并且它引用 City 实体。因此,当涉及到 School 的插入时,您可以这样做:
public School Insert(School newItem, int cityId)
{
if(cityId <= 0)
{
throw new Exception("City ID no provided");
}
newItem.City = null;
newItem.City_Id = cityId;
using (var context = new DatabaseContext())
{
context.Set<School>().Add(newItem);
context.SaveChanges();
return newItem;
}
}
在这种情况下,您明确指定新记录的 City_Id 并从图中删除 City,这样 EF 就不会费心将其与 School 一起添加到上下文中。
虽然在第一印象中外键方法似乎更复杂,但相信我,在插入多对多关系时,这种心态会为你节省很多时间(想象你有学校和学生的关系,学生有一个城市属性)等等。
希望这对你有帮助。
[Range(1, int.MaxValue)]
属性说明 City_Id 的最小值 1 不是更好吗?
如果您只想将更改存储到父对象并避免存储对其任何子对象的更改,那么为什么不直接执行以下操作:
using (var ctx = new MyContext())
{
ctx.Parents.Attach(parent);
ctx.Entry(parent).State = EntityState.Added; // or EntityState.Modified
ctx.SaveChanges();
}
第一行将父对象及其依赖子对象的整个图附加到处于 Unchanged
状态的上下文中。
第二行仅更改父对象的状态,使其子对象处于 Unchanged
状态。
请注意,我使用的是新创建的上下文,因此可以避免将任何其他更改保存到数据库中。
建议的解决方案之一是从相同的数据库上下文中分配导航属性。在此解决方案中,将从数据库上下文外部分配的导航属性将被替换。请参阅以下示例进行说明。
class Company{
public int Id{get;set;}
public Virtual Department department{get; set;}
}
class Department{
public int Id{get; set;}
public String Name{get; set;}
}
保存到数据库:
Company company = new Company();
company.department = new Department(){Id = 45};
//an Department object with Id = 45 exists in database.
using(CompanyContext db = new CompanyContext()){
Department department = db.Departments.Find(company.department.Id);
company.department = department;
db.Companies.Add(company);
db.SaveChanges();
}
微软将此列为一项功能,但我觉得这很烦人。如果与公司对象关联的部门对象的 Id 已经存在于数据库中,那么为什么 EF 不只是将公司对象与数据库对象关联呢?为什么我们需要自己照顾协会?在添加新对象期间处理导航属性就像将数据库操作从 SQL 移动到 C#,对开发人员来说很麻烦。
首先,您需要知道在 EF 中更新实体有两种方法。
附加对象
当您使用上述方法之一更改附加到对象上下文的对象的关系时,实体框架需要保持外键、引用和集合同步。
断开的对象
如果您正在使用断开连接的对象,则必须手动管理同步。
在我正在构建的应用程序中,EF 对象模型不是从数据库中加载的,而是用作我在解析平面文件时填充的数据对象。
这意味着您正在使用断开连接的对象,但不清楚您是否使用 independent association or foreign key association。
添加 使用现有子对象(数据库中存在的对象)添加新实体时,如果子对象未被 EF 跟踪,则将重新插入子对象。除非您先手动附加子对象。 db.Entity(entity.ChildObject).State = EntityState.Modified; db.Entity(entity).State = EntityState.Added;
更新您可以将实体标记为已修改,然后将更新所有标量属性,而导航属性将被忽略。 db.Entity(entity).State = EntityState.Modified;
图差异
如果您想在使用断开连接的对象时简化代码,可以尝试 graph diff library。
示例代码
如果实体不存在则插入,否则更新。 db.UpdateGraph(实体);
如果实体不存在则插入实体,否则更新并插入子对象,如果不存在则更新。 db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
最好的方法是覆盖数据上下文中的 SaveChanges 函数。
public override int SaveChanges()
{
var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);
// Do your thing, like changing the state to detached
return base.SaveChanges();
}
这对我有用:
// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();
.... do other stuff
context.SaveChanges();
entity.ChildCollection = temp;
当我尝试保存个人资料时,我遇到了同样的问题,我已经表了称呼并且新创建了个人资料。当我插入配置文件时,它也会插入称呼。所以我在 savechanges() 之前试过这样。
db.Entry(Profile.Salutation).State = EntityState.Unchanged;
我们所做的是在将父集合添加到 dbset 之前,断开子集合与父集合的连接,确保将现有集合推送到其他变量以允许稍后使用它们,然后用新的空集合替换当前子集合。将子集合设置为 null/nothing 对我们来说似乎失败了。之后,将父级添加到 dbset。这样,在您希望他们添加之前,不会添加子项。
我在使用 Entity Framework Core 3.1.0 时遇到了类似的挑战,我的 repo 逻辑非常通用。
这对我有用:
builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
l.ChildEntity).HasForeignKey("ParentEntityId");
请注意“ParentEntityId”是子实体的外键列名。我在这个方法上添加了上面提到的代码行:
protected override void OnModelCreating(ModelBuilder builder)...
如果您想设置没有它们关系的特定对象,请执行跟踪。使用此选项。
public void Insert(School newItem)
{
using (var context = new DatabaseContext())
{
context.Entry(newItem).State = EntityState.Added;
context.SaveChanges();
}
}
在 EF Core Documentation about Tracking 阅读更多内容以获得更好的 Knolage
我知道这是旧帖子,但是如果您使用代码优先方法,则可以通过在映射文件中使用以下代码来实现所需的结果。
Ignore(parentObject => parentObject.ChildObjectOrCollection);
这基本上会告诉 EF 从模型中排除“ChildObjectOrCollection”属性,以便它不会映射到数据库。
这是 Entity Framework Core .net 5 的工作代码,它只保存在父表中,没有错误。我正在保存列表,您也可以保存对象。
改成这个,开始给我另一个错误:The entity type <typename> was not found. Ensure that the entity type has been added to the model
注意:YouObject 是实体
error code:
_context.yourObject.AttachRange(yourObjectList);
_context.Entry(_context.yourObject).State = EntityState.Added;
await _context.SaveChangesAsync().ConfigureAwait(false);
所以,这就是我所做的:(下面的工作代码)
第 1 步:我将子组件设为空。
yourObjectList.ForEach(req => { req.ChildObject = null; });
第2步:将上面的代码更改为:
working code:
_context.yourObject.UpdateRange(yourObjectList);
await _context.SaveChangesAsync().ConfigureAwait(false);