【问题标题】:How do I stop Entity Framework from trying to save/insert child objects?如何阻止实体框架尝试保存/插入子对象?
【发布时间】:2019-05-22 07:29:56
【问题描述】:

当我使用实体框架保存实体时,我自然认为它只会尝试保存指定的实体。但是,它也在尝试保存该实体的子实体。这会导致各种完整性问题。如何强制 EF 仅保存我要保存的实体,从而忽略所有子对象?

如果我手动将属性设置为 null,则会收到错误消息“操作失败:无法更改关系,因为一个或多个外键属性不可为空。”这是非常适得其反的,因为我专门将子对象设置为 null 以便 EF 不理会它。

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

由于这在 cmets 中反复讨论,我将给出一些理由说明为什么我希望我的子对象不被打扰。

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

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

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

【问题讨论】:

  • 据我所知,您必须将子对象设为空。
  • 嗨,约翰。不工作。如果我将集合设为空,它会引发错误。根据我的操作方式,它会抱怨键为空或我的集合已被修改。显然,那些事情是真实的,但我是故意这样做的,所以它不会留下它不应该接触的物体。
  • 欣快,这完全没有帮助。
  • @Euphoric 即使不更改子对象,EF 仍会默认尝试插入它们,而不是忽略或更新它们。
  • 真正让我烦恼的是,如果我特意将这些对象归零,它会抱怨而不是意识到我希望它不理会它们。由于这些子对象都是可选的(在数据库中可以为空),是否有某种方法可以强制 EF 忘记我拥有这些对象?即以某种方式清除其上下文或缓存?

标签: c# entity-framework


【解决方案1】:

据我所知,您有两种选择。

选项 1)

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

选项 2)

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

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

请注意,您不能分离 List/Collection。您必须像这样遍历列表并分离列表中的每个项目

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

【讨论】:

  • 嗨,Johan,我尝试分离其中一个集合并抛出以下错误:实体类型 HashSet`1 不是当前上下文模型的一部分。
  • @MarkyMark 不要分离集合。您将不得不遍历集合并将其分离为对象(我现在将更新我的答案)。
  • 不幸的是,即使有一个循环列表来分离所有内容,EF 似乎仍在尝试插入到一些相关的表中。在这一点上,我已经准备好撕掉 EF 并切换回至少表现得合理的 SQL。多么痛苦。
  • 我可以在添加之前使用 context.Entry(yourObject).State 吗?
  • @mirind4 我没有使用状态。如果我插入一个对象,我确保所有的孩子都是空的。在更新时,我首先得到没有孩子的对象。
【解决方案2】:

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

假设你有一个School实体和一个City实体,这是一个多对一的关系,一个City有很多Schools,一个School属于一个城市。并假设 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 就不会费心将其添加到上下文中以及学校

虽然在第一印象中外键方法似乎更复杂,但相信我,在插入多对多关系时,这种心态会为你节省很多时间(想象你有学校和学生的关系,并且学生拥有 City 财产)等等。

希望对你有帮助。

【讨论】:

  • 很好的答案,对我帮助很大!但是使用[Range(1, int.MaxValue)] 属性说明 City_Id 的最小值 1 不是更好吗?
  • 像梦一样!!感谢一百万!
  • 这将从调用者的 School 对象中删除值。 SaveChanges 是否重新加载空导航属性的值?否则,如果调用者需要该信息,则调用者应在调用 Insert() 方法后重新加载 City 对象。这是我经常使用的模式,但如果有人有好的模式,我仍然愿意接受更好的模式。
  • 这对我很有帮助。 EntityState.Unchaged 正是我需要分配一个对象,该对象表示我保存为单个事务的大型对象图中的查找表外键。 EF Core 会引发不太直观的错误消息 IMO。我缓存了由于性能原因很少更改的查找表。您的假设是,因为查找表对象的 PK 与数据库中的内容相同,因此它知道其未更改并且仅将 FK 分配给现有项目。而不是尝试将查找表对象插入为新对象。
  • 没有任何组合为空或将状态设置为不变对我有用。当我只想更新父级中的标量字段时,EF 坚持认为它需要插入一个新的子记录。
【解决方案3】:

如果您只想将更改存储到 对象并避免存储对其任何 对象的更改,那么为什么不直接执行以下操作:

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

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

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

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

【讨论】:

  • 这个答案的好处是,如果有人后来出现并添加了一个子对象,它不会破坏现有的代码。这是一个“选择加入”解决方案,其他解决方案要求您明确排除子对象。
  • 这不再适用于核心。 “附加:附加每个可达实体,除非可达实体具有存储生成的键且未分配键值;这些将被标记为已添加。”如果孩子也是新的,他们将被添加。
  • 简单干净。不头痛!谢谢。
  • 太棒了!在处理从我的数据库上下文的不同实例加载的现有员工实体时,我需要做什么。将主键保存到我的主表中无需大惊小怪。
  • 谢谢。工作,它节省了很多时间。简单的解决方案。
【解决方案4】:

建议的解决方案之一是从同一数据库上下文中分配导航属性。在此解决方案中,将从数据库上下文外部分配的导航属性将被替换。请参阅以下示例进行说明。

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

【讨论】:

  • 我同意 100% EF 在提供 ID 时尝试创建新记录是荒谬的。如果有办法用我们正在做的实际对象填充选择列表选项。
【解决方案5】:

首先你需要知道在 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

这里是介绍,Introducing GraphDiff for Entity Framework Code First - Allowing automated updates of a graph of detached entities

示例代码

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

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

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
    

【讨论】:

    【解决方案6】:

    最好的方法是覆盖数据上下文中的 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();
        }
    

    【讨论】:

      【解决方案7】:

      这对我有用:

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

      【讨论】:

        【解决方案8】:

        我在尝试保存个人资料时遇到了同样的问题,我已经表了称呼并新创建个人资料。当我插入配置文件时,它也会插入称呼。所以我在 savechanges() 之前试过这样。

        db.Entry(Profile.Salutation).State = EntityState.Unchanged;

        【讨论】:

          【解决方案9】:

          我们所做的是在将父集合添加到 dbset 之前,断开子集合与父集合的连接,确保将现有集合推送到其他变量以允许稍后使用它们,然后将当前子集合替换为新的空集合。将子集合设置为 null/nothing 对我们来说似乎失败了。 之后,将父级添加到 dbset。 这样,在您希望他们添加之前,不会添加孩子。

          【讨论】:

            【解决方案10】:

            我知道这是旧帖子,但是如果您使用代码优先方法,则可以通过在映射文件中使用以下代码来实现所需的结果。

            Ignore(parentObject => parentObject.ChildObjectOrCollection);
            

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

            【讨论】:

            • 在什么上下文中使用“忽略”?它似乎不存在于假定的上下文中。
            【解决方案11】:

            我在使用 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)...
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-11-28
              相关资源
              最近更新 更多