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

Mar*_*lef 82 c# entity-framework

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

如果我手动将属性设置为null,则会收到错误"操作失败:由于一个或多个外键属性不可为空,因此无法更改关系." 这非常适得其反,因为我将child对象设置为null,因此EF会将其单独留下.

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

由于这是在评论中来回讨论,我将给出一些理由说明为什么我希望我的孩子对象保持独立.

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

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

即使子对象是真实数据,我需要首先保存父对象并获得主键或EF似乎只是搞乱了.希望这能给出一些解释.

Joh*_*han 51

据我所知,你有两种选择.

选项1)

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

选项2)

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

 context.Entry(yourObject).State = EntityState.Detached
Run Code Online (Sandbox Code Playgroud)

请注意,您无法分离List/ Collection.您将不得不循环遍历列表并分离列表中的每个项目,如此

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}
Run Code Online (Sandbox Code Playgroud)

  • 不幸的是,即使有一个循环列表来解析所有东西,EF似乎仍然试图插入一些相关的表.此时,我已经准备好将EF翻出来并切换回SQL,这至少表现得很明智.太痛苦了. (10认同)
  • 我可以在添加它之前使用context.Entry(yourObject).State吗? (2认同)

小智 28

长话短说:使用外键,它将节省您的一天.

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

最初你可能会定义像这样的实体:

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; }
}
Run Code Online (Sandbox Code Playgroud)

您可以像这样插入学校(假设您已经将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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,上述方法可能完美地工作,但是,我更喜欢外键方法对我来说更清晰,更灵活.请参阅下面的更新解决方案

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; }
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您明确定义学校有一个外键City_Id,它指的是City实体.因此,在插入学校时,您可以:

    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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您明确指定新记录的City_Id并从图表中删除City,以便EF不会费心将其与School一起添加到上下文中.

虽然第一印象是外键方法看起来更复杂,但相信我这种心态会在插入多对多关系(成像你有学校和学生关系,以及学生)方面为你节省很多时间有城市财产)等.

希望这对你有所帮助.


小智 16

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

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

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

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

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

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

Bik*_*rma 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;}
}
Run Code Online (Sandbox Code Playgroud)

保存到数据库:

 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();
  }
Run Code Online (Sandbox Code Playgroud)

微软将此作为一项功能,但我觉得这很烦人.如果与公司对象关联的部门对象具有已存在于数据库中的Id,那么为什么EF不将公司对象与数据库对象相关联?我们为什么要自己照顾这种关联?在添加新对象期间处理导航属性就像将数据库操作从SQL移动到C#,这对开发人员来说很麻烦.

  • 我 100% 同意,EF 尝试在提供 ID 的情况下创建新记录是荒谬的。如果有一种方法可以用实际对象填充选择列表选项,我们就可以开展业务了。 (3认同)

Yul*_*dra 9

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

  • 附加物体

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

  • 断开的对象

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

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

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

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

    db.Entity(entity.ChildObject).State = EntityState.Modified;
    db.Entity(entity).State = EntityState.Added;
    
    Run Code Online (Sandbox Code Playgroud)
  • 更新

    您只需将实体标记为已修改,然后将更新所有标量属性,并且将简单地忽略导航属性.

    db.Entity(entity).State = EntityState.Modified;
    
    Run Code Online (Sandbox Code Playgroud)

图形差异

如果要在使用断开连接的对象时简化代码,可以尝试图形差异库.

以下是介绍,首先介绍GraphDiff for Entity Framework Code - 允许自动更新分离实体的图形.

示例代码