如何停止实体框架试图保存/插入子对象?

当我使用实体框架保存一个实体时,我自然而然地假定它只会尝试保存指定的实体。但是,它也试图保存该实体的子实体。这导致了各种各样的诚信问题。如何强制 EF 只保存要保存的实体,从而忽略所有子对象?

如果我手动将属性设置为 null,就会得到一个错误“操作失败: 关系无法更改,因为一个或多个外键属性不能为空。”这是非常适得其反的,因为我特别将子对象设置为 null,这样 EF 就不会去管它了。

为什么我不想保存/插入子对象?

由于在评论中反复讨论这个问题,我将给出一些理由,说明为什么我希望我的子对象独自存在。

在我正在构建的应用程序中,EF 对象模型不是从数据库中加载的,而是用作数据对象,我在解析平面文件时填充这些对象。对于子对象,其中许多引用了定义父表的各种属性的查找表。例如,主实体的地理位置。

由于我自己已经填充了这些对象,因此 EF 假定这些对象是新对象,需要与父对象一起插入。但是,这些定义已经存在,我不想在数据库中创建副本。我只使用 EF 对象进行查找并填充主表实体中的外键。

即使子对象是真实的数据,我也需要首先保存父对象并获得一个主键,否则 EF 似乎会把事情搞得一团糟。希望这能给你一些解释。

118262 次浏览

据我所知,你有两个选择。

选项1)

取消所有子对象,这将确保 EF 不添加任何内容。它也不会从数据库中删除任何内容。

选项2)

使用以下代码将子对象设置为与上下文分离

 context.Entry(yourObject).State = EntityState.Detached

注意,不能分离 List/Collection。您将不得不循环遍历您的列表,并分离您的列表中的每个项目,如下所示

foreach (var item in properties)
{
db.Entry(item).State = EntityState.Detached;
}

首先,您需要知道在 EF 中有两种更新实体的方法。

  • 附着的物体

当更改附加到对象的对象的关系时 通过使用上面描述的方法之一,实体 框架需要将外键、引用和集合保存在 同步。

  • 断开连接的物体

如果使用的是断开连接的对象,则必须手动管理 同步。

在我正在构建的应用程序中,EF 对象模型不是从数据库中加载的,而是用作数据对象,我在解析平面文件时填充这些对象。

这意味着您正在处理断开连接的对象,但是不清楚您是否正在使用 独立协会或外键协会

  • 当添加具有现有子对象(数据库中存在的对象)的新实体时,如果子对象未被 EF 跟踪,则将重新插入子对象。除非您首先手动附加子对象。

      db.Entity(entity.ChildObject).State = EntityState.Modified;
    db.Entity(entity).State = EntityState.Added;
    
  • 更新

    您只需将实体标记为已修改,然后所有标量属性都将被更新,导航属性将被忽略。

      db.Entity(entity).State = EntityState.Modified;
    

Graph Diff

如果希望在处理断开连接的对象时简化代码,可以尝试使用 图形差分库图形差分库

这是简介,首次引入实体框架代码的 GraphDiff ——允许自动更新分离实体的图形

样本代码

  • 如果实体不存在,则插入该实体,否则更新。

      db.UpdateGraph(entity);
    
  • 插入实体如果不存在,则更新 还有插入子对象如果不存在,则更新。

      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();
}

我们所做的是在将父集合添加到 dbset 之前,断开子集合与父集合的连接,确保将现有集合推送到其他变量,以便以后使用它们,然后用新的空集合替换当前的子集合。对于我们来说,将子集合设置为 null/nothing 似乎是失败的。 完成此操作后,将父节点添加到 dbset。 这样,直到您希望添加子元素时,才会添加它们。

这对我很有效:

// 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;

我知道这是旧的职位,但是,如果你正在使用代码优先的方法,你可以通过使用下面的代码在您的映射文件达到预期的结果。

Ignore(parentObject => parentObject.ChildObjectOrCollection);

这将基本上告诉 EF 从模型中排除“ ChildObjectOrCollection”属性,以便它不映射到数据库。

建议的解决方案之一是从相同的数据库上下文分配导航属性。在此解决方案中,将替换从数据库上下文外部分配的导航属性。请参阅下面的例子。

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 # ,这对开发人员来说很麻烦。

我有同样的问题,当我试图保存配置文件,我已经表致敬和新建配置文件。当我插入轮廓它也插入到称呼。所以我在 savechange ()之前尝试了这种方法。

条目(Profile.Salaling) . State = EntityState. 未更改;

如果您只想存储对 家长对象的更改,并避免存储对其任何 孩子对象的更改,那么为什么不只是执行以下操作:

using (var ctx = new MyContext())
{
ctx.Parents.Attach(parent);
ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
ctx.SaveChanges();
}

第一行将父对象及其依赖子对象的整个图附加到处于 Unchanged状态的上下文。

第二行只更改父对象的状态,使其子对象处于 Unchanged状态。

注意,我使用了一个新创建的上下文,因此可以避免将任何其他更改保存到数据库中。

长话短说: 使用外键,它将拯救你的一天

假设您有一个 学校实体和一个 城市实体,这是一个多对一的关系,其中一个城市有许多学校,一个学校属于一个城市。假设城市已经存在于查找表中,因此您不希望在插入新学校时再次插入它们。

最初,您可以像下面这样定义您的实体:

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; }
}

您可以像这样插入 学校(假设您已经将 城市属性分配给了 新项目) :

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; }
}

通过这种方式,您可以明确地定义 学校具有一个外键 城市身份证,并且它引用 城市实体。因此,当涉及到 学校的插入时,你可以这样做:

    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;
}
}

在这种情况下,您显式地指定新记录的 城市身份证,并从图中删除 城市,这样 EF 就不必费心将它与 学校一起添加到上下文中。

虽然在第一印象中外键的方法似乎更复杂,但相信我,这种心态将节省你很多时间,当涉及到插入多对多的关系(想象你有一个学校和学生的关系,学生有一个城市属性)等等。

希望这对你有帮助。

我在使用实体框架核心3.1.0时遇到过类似的挑战,我的回购逻辑非常通用。

这对我很有效:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
l.ChildEntity).HasForeignKey("ParentEntityId");

请注意,“ ParentEntityId”是子实体上的外键列名。 我在这个方法上添加了上面提到的代码行:

protected override void OnModelCreating(ModelBuilder builder)...

这是 实体框架核心.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: 我将子组件设置为 null。

yourObjectList.ForEach(req => { req.ChildObject = null; });

步骤2: 将上面的代码更改为:

working code:


_context.yourObject.UpdateRange(yourObjectList);
await _context.SaveChangesAsync().ConfigureAwait(false);

如果你想设置特定的对象没有它们的关系做跟踪。 使用此选项。

public void Insert(School newItem)
{


using (var context = new DatabaseContext())
{
context.Entry(newItem).State = EntityState.Added;
context.SaveChanges();
}
}

阅读更多关于更好的知识 关于跟踪的核心文档