如何阻止Entity Framework尝试保存/插入子对象?


100

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

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

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

由于在注释中来回讨论了这一点,因此我将给出一些理由说明为什么我希望让我的子对象保持不动。

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

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

即使使用真实数据的子对象,我也需要先保存父对象并获取主键,否则EF似乎一团糟。希望这能提供一些解释。


据我所知,您将必须使子对象为空。
2014年

嗨,约翰。不起作用 如果我使集合为空,它将引发错误。根据我的操作方式,它抱怨键为空或我的收藏集已被修改。显然,那些事情是对的,但我是故意这样做的,因此它将不应该触摸的对象留在原地。
马克米卡列夫

欣快,那是完全没有帮助的。
马克米卡列夫

@Euphoric即使不更改子对象,EF仍默认尝试插入它们,而不忽略或更新它们。
2014年

真正让我烦恼的是,如果我竭尽全力使这些对象无效,那么它就会抱怨而不是意识到我不希望它让它们独自呆着。由于这些子对象都是可选的(在数据库中为空),是否有某种方法可以迫使EF忘记我拥有那些对象?即清除其上下文或以某种方式缓存?
Mark Micallef 2014年

Answers:


55

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

选项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不是当前上下文模型的一部分。
Mark Micallef

@MarkyMark不要分离集合。您将不得不遍历集合并将对象逐个对象分离(我现在将更新我的答案)。
2014年

11
不幸的是,即使有一个循环列表可以分离所有内容,EF似乎仍在尝试插入一些相关表中。在这一点上,我准备将EF移出并切换回至少表现得合理的SQL。真痛苦
Mark Micallef 2014年

2
我可以在添加它之前使用context.Entry(yourObject).State吗?
Thomas Klammer '18

1
@ mirind4我没有使用状态。如果插入对象,请确保所有子代均为空。更新时,我首先得到的对象没有孩子。
托马斯·克拉默

41

长话短说:使用外键可以节省您的时间。

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

最初,您可以这样定义实体:

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的新记录,并删除了从曲线图,使EF不会打扰到它一起添加到上下文学校

尽管乍一看外键方法似乎更复杂,但是请相信我,这种心态将在插入多对多关系时为您节省很多时间(假设您有学校和学生的关系,而学生拥有City属性)等等。

希望这对您有帮助。


很好的答案,对我有很大帮助!但是使用[Range(1, int.MaxValue)]属性声明City_Id的最小值1会更好吗?
丹·雷森

像做梦一样!!太感谢了!
CJH

这将从调用者的School对象中删除该值。SaveChanges是否重新加载空导航属性的值?否则,如果调用者需要该信息,则应在调用Insert()方法之后重新加载City对象。这是我经常使用的模式,但是如果有人拥有好的模式,我仍然愿意接受更好的模式。
凌晨

1
这对我真的很有帮助。EntityState.Unchaged正是我需要分配的对象,该对象代表我保存为单个事务的大型对象图中的查找表外键。EF Core抛出了一个不太直观的错误消息IMO。我缓存了由于性能原因很少更改的查询表。您的假设是,因为查找表对象的PK与数据库中的PK已经相同,因为它知道其不变,只需将FK分配给现有项即可。而不是尝试将查找表对象插入为新对象。
tnk479

空组合或将状态设置为不变对我来说没有任何组合。当我只想更新父级中的标量字段时,EF坚持需要插入一个新的子级记录。
BobRz

21

如果你只是想保存修改到对象,避免存储更改任何其对象,那么为什么不只是做到以下几点:

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

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

第二行仅更改父对象的状态,而其子对象保持Unchanged状态。

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


2
这个答案的优点是,如果以后有人出现并添加一个子对象,则不会破坏现有代码。这是一个“选择加入”解决方案,其中其他解决方案要求您明确排除子对象。
吉姆(Jim)

这不再适用于核心。“附加:附加每个可访问实体,除非可访问实体具有商店生成的键并且未分配键值;这些将被标记为已添加。” 如果孩子也是新孩子,他们将被添加。
mmix

14

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

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

Microsoft将此功能列为功能,但是我觉得很烦。如果与公司对象关联的部门对象具有已经存在于数据库中的ID,那么EF为什么不仅仅将公司对象与数据库对象相关联?为什么我们需要自己照顾协会?在添加新对象的过程中要注意导航属性,就像将数据库操作从SQL移到C#一样,这对开发人员来说很麻烦。


我同意100%提供ID时EF尝试创建新记录是荒谬的。如果有一种方法可以用实际的对象填充选择列表选项,那么我们就可以开展业务。
T3.0

10

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

  • 附加物件

当您使用上述方法之一更改附加到对象上下文的对象的关系时,实体框架需要使外键,引用和集合保持同步。

  • 断开对象

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

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

这意味着您正在使用断开连接的对象,但是不清楚您是使用独立关联还是外键关联

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

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

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

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

图差异

如果要在处理断开连接的对象时简化代码,可以尝试绘制diff库图

这里是引进,对于实体框架代码首先介绍GraphDiff -允许分离的实体的图形自动更新

样例代码

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

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

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

3

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

2

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

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


1

这对我有用:

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

0

我们要做的是在将父级添加到dbset之前,断开子级收集与父级的连接,确保将现有的收集推到其他变量以允许以后使用它们,然后用新的空集合替换当前的子级收集。对于我们来说,将子集合设置为空/什么都不是失败的。之后,将父级添加到dbset中。这样,直到您要添加子项为止。


0

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

Ignore(parentObject => parentObject.ChildObjectOrCollection);

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


在什么上下文中使用“忽略”?它似乎在假定的上下文中不存在。
T3.0

0

我在使用Entity Framework Core 3.1.0时遇到了类似的挑战,我的回购逻辑非常通用。

这对我有用:

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

请注意,“ ParentEntityId”是子实体上的外键列名称。我在此方法上添加了上述代码行:

protected override void OnModelCreating(ModelBuilder builder)...
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.