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

时间:2014-08-22 06:42:00

标签: c# entity-framework

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

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

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

由于在评论中来回讨论这个问题,我会给出一些理由说明我为什么要让我的子对象独自留下。

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

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

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

11 个答案:

答案 0 :(得分:46)

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

选项1)

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

选项2)

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

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

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

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

答案 1 :(得分:21)

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

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

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

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

您可以像这样插入学校(假设您已经 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; }
}

通过这种方式,您明确定义学校具有外键 City_Id ,并且它指的是城市实体。因此,当插入学校时,您可以这样做:

    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不会费心将其添加到上下文中以及学校

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

希望这对你有所帮助。

答案 2 :(得分:15)

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

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

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

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

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

答案 3 :(得分:11)

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

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

答案 4 :(得分:8)

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

  • 附加物品

      

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

  • 已断开连接的对象

      

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

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

这意味着您正在使用断开连接的对象,但不清楚您是否使用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));
    

答案 5 :(得分:2)

最好的方法是覆盖datacontext中的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();
    }

答案 6 :(得分: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;

答案 7 :(得分:1)

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

05-02 23:48:23.533 17050-17050/com.example.timomichel.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
  Process: com.example.timomichel.myapplication, PID: 17050
  java.lang.IllegalStateException: Could not execute method for android:onClick
      at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:289)
      at android.view.View.performClick(View.java:5198)
      at android.view.View$PerformClick.run(View.java:21147)
      at android.os.Handler.handleCallback(Handler.java:739)
      at android.os.Handler.dispatchMessage(Handler.java:95)
      at android.os.Looper.loop(Looper.java:148)
      at android.app.ActivityThread.main(ActivityThread.java:5417)
      at java.lang.reflect.Method.invoke(Native Method)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
   Caused by: java.lang.reflect.InvocationTargetException
      at java.lang.reflect.Method.invoke(Native Method)
      at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:284)
      at android.view.View.performClick(View.java:5198) 
      at android.view.View$PerformClick.run(View.java:21147) 
      at android.os.Handler.handleCallback(Handler.java:739) 
      at android.os.Handler.dispatchMessage(Handler.java:95) 
      at android.os.Looper.loop(Looper.java:148) 
      at android.app.ActivityThread.main(ActivityThread.java:5417) 
      at java.lang.reflect.Method.invoke(Native Method) 
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) 
   Caused by: android.content.ActivityNotFoundException: Unable to find explicit activity class {com.example.timomichel.myapplication/com.example.timomichel.myapplication.SecondActivity}; have you declared this activity in your AndroidManifest.xml?
      at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:1794)
      at android.app.Instrumentation.execStartActivity(Instrumentation.java:1512)
      at android.app.Activity.startActivityForResult(Activity.java:3917)
      at android.app.Activity.startActivityForResult(Activity.java:3877)
      at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:843)
      at android.app.Activity.startActivity(Activity.java:4200)
      at android.app.Activity.startActivity(Activity.java:4168)
      at com.example.timomichel.myapplication.MainActivity.start(MainActivity.java:18)
      at java.lang.reflect.Method.invoke(Native Method) 
      at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:284) 
      at android.view.View.performClick(View.java:5198) 
      at android.view.View$PerformClick.run(View.java:21147) 
      at android.os.Handler.handleCallback(Handler.java:739) 
      at android.os.Handler.dispatchMessage(Handler.java:95) 
      at android.os.Looper.loop(Looper.java:148) 
      at android.app.ActivityThread.main(ActivityThread.java:5417) 
      at java.lang.reflect.Method.invoke(Native Method) 
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

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

答案 8 :(得分:1)

当我尝试保存配置文件时,我遇到了同样的问题,我已经提出了表示敬意和新创建配置文件的问题。当我插入配置文件时,它也插入称呼。所以我在savechanges()之前就这样试过了。

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

答案 9 :(得分:0)

我们所做的是在将父集合添加到dbset之前,将子集合与父集合断开连接,确保将现有集合推送到其他变量以允许以后使用它们,然后用新的替换当前子集合空集合。将子集合设置为null / no似乎对我们失败了。 执行此操作后,将父项添加到dbset。 这样孩子就不会被添加,直到你想要它们为止。

答案 10 :(得分: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)...