【问题标题】:How can I use EF to add multiple child entities to an object when the child has an identity key?当子对象具有身份密钥时,如何使用 EF 将多个子实体添加到对象?
【发布时间】:2013-07-30 12:08:05
【问题描述】:

我们正在使用EF5和SQL Server 2012以下两个类:

public class Question
{
    public Question()
    {
        this.Answers = new List<Answer>();
    }
    public int QuestionId { get; set; }
    public string Title { get; set; }
    public virtual ICollection<Answer> Answers { get; set; }

}
public class Answer
{
    public int AnswerId { get; set; }
    public string Text { get; set; }
    public int QuestionId { get; set; }
    public virtual Question Question { get; set; }
}

映射如下:

public class AnswerMap : EntityTypeConfiguration<Answer>
{
    public AnswerMap()
    {
        // Primary Key
        this.HasKey(t => t.AnswerId);

        // Identity
        this.Property(t => t.AnswerId)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

数据库 DDL

CREATE TABLE Answer (
    [AnswerId] INT IDENTITY (1, 1) NOT NULL,
    [QuestionId] INT NOT NULL,
    [Text] NVARCHAR(1000),
    CONSTRAINT [PK_Answer] PRIMARY KEY CLUSTERED ([AnswerId] ASC)
)";

这是我尝试过的结果:

这适用于一个孩子:

var a = new Answer{
    Text = "AA",
    QuestionId = 14
};
question.Answers.Add(a);
_uow.Questions.Update(question);
_uow.Commit();

这不适用于多个孩子:

错误:ObjectStateManager 中已存在具有相同键的对象。 ObjectStateManager 无法跟踪具有相同键的多个对象。

var a = new Answer{
    AnswerId = 0,
    Text = "AAA",
    QuestionId = 14
};
var b = new Answer {
    AnswerId = 0,
    Text = "BBB",
    QuestionId = 14
};
question.Answers.Add(a);
question.Answers.Add(b);
_uow.Questions.Update(question);
_uow.Commit();

这不适用于多个孩子:

它会创建 AnswerID 的 1000 和 1001,但我希望数据库创建新的 ID。

var a = new Answer{
    AnswerId = 1000,
    Text = "AAA",
    QuestionId = 14
};
var b = new Answer {
    AnswerId = 1001,
    Text = "BBB",
    QuestionId = 14
};
question.Answers.Add(a);
question.Answers.Add(b);
_uow.Questions.Update(question);
_uow.Commit();

不起作用:

编译器错误。无法将 null 转换为 int

var a = new Answer{
    AnswerId = null,
    Text = "AAA",
    QuestionId = 14    
};
var b = new Answer
{
    AnswerId = null,
    Text = "BBB",
    QuestionId = 14
};
question.Answers.Add(a);
question.Answers.Add(b);
_uow.Questions.Update(question);
_uow.Commit();

不起作用:

ObjectStateManager 无法使用相同的键跟踪多个对象。

var a = new Answer{
    Text = "AAA",
    QuestionId = 14
};
var b = new Answer
{
    Text = "BBB",
    QuestionId = 14
};
question.Answers.Add(a);
question.Answers.Add(b);
_uow.Questions.Update(question);
_uow.Commit();

在我的应用程序中,我在客户端生成了一个或多个新的 Answer 对象,然后将这些对象发送到服务器。以上我正在模拟将发生的事情,而没有将客户添加到问题中。请注意,向 Question 对象添加所有 Answers 是在客户端完成的,然后以 JSON 字符串的形式传递给服务器。然后将其反序列化为一个问题对象,如下所示:

public HttpResponseMessage PutQuestion(int id, Question question) {
            _uow.Questions.Update(question);
            _uow.Commit();

我希望使用 new identity ID 创建每个 Answer 对象,以便将这些对象添加到 Question 对象并以正常方式返回 Question 对象。

我不知道如何做到这一点。到目前为止,我所有的简单测试都不起作用。请注意,这是我们小组成员之前提出的问题的一个变体,这个问题不太清楚,我正试图结束。我希望这个问题更清楚。

注意事项:

这里是更新的编码方式:

public virtual void Update(T entity)
{
    DbEntityEntry dbEntityEntry = DbContext.Entry(entity);
    if (dbEntityEntry.State == EntityState.Detached)
    {
        DbSet.Attach(entity);
    }  
    dbEntityEntry.State = EntityState.Modified;
}

【问题讨论】:

  • 为什么要添加两次 Answer a?
  • 语法错误 - 现已修复。感谢您指出了这一点。实际代码尝试添加“a”,然后添加“b”变量。

标签: c# sql-server asp.net-mvc entity-framework entity-framework-5


【解决方案1】:

我也遇到了同样的身份“限制”。事实证明,如果您添加父级和任何子级,EF 可以处理父级和子级都被添加在一起的事实。当您更新父级并同时插入两个子级时,您会遇到问题。如果您附加父级,EF 将自动拾取这两个子级并附加它们,无论您是否愿意。由于我们希望它自动生成 Id,我们不会设置孩子的主键。但是,当父项是 Update 并爆炸时,EF 无法处理具有相同主键的项目,因为两个子项的 PK 相同,均为 0。

我发现的唯一方法是手动将孩子的 ID 设置为不同的数字。我通常将第一个孩子的 ID 设置为 -1,然后将第二个孩子的 ID 设置为 -2,依此类推。这将导致 EF 保存子项,并且由于在数据库上运行的 Identity 将自动更新密钥,因为 -1 和 -2 不是有效的标识值。

但是,如果您有 3 级或更高级别,这将导致巨大的痛苦。您不仅必须更新每个孩子的这个 PK,而且还必须将其任何孩子的 FK 更新为这个新的 -1 或 -2 值。否则会再次保存失败!

我看到的唯一其他选择实际上只是一次插入一个孩子并调用保存,因此上下文不会处理具有相同 PK 的多个对象,但这违背了 ORM 的目的。 ..

【讨论】:

  • 按照@AminSaghi 的建议添加 [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] 对我不起作用,但确实如此。谢谢!
  • 从 EF 6.1.2(可能更早)开始,这不再是问题。它现在似乎按预期工作! :)
  • 实际上,如果您在附加更新之前遍历实体树并添加所有新项目,它也可以正常工作。
  • 我知道这个线程已经快三年了,但我只想说我在使用 EF 6.1.3 时遇到了与@DanielLorenz 描述的相同的问题。我解决它的唯一方法是一次插入一个孩子
  • 另一个解决方案是先添加所有新实体。完成所有添加后,您可以返回并附加所有其他项目。这就是我最终所做的,以避免一直分配负 PK。
【解决方案2】:

你有没有提到你要添加两次a...?!

question.Answers.Add(a);
question.Answers.Add(a);

通常,要添加 id 为标识的项目,您必须跳过设置 id。您还应该将 [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] 属性添加到这些 ID:

public class Answer
{
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int AnswerId { get; set; }
    public string Text { get; set; }
    public int QuestionId { get; set; }
    public virtual Question Question { get; set; }
}

并像这样添加数据:

var a = new Answer{
    Text = "AAA",
    QuestionId = 14
};

var b = new Answer
{
    Text = "BBB",
    QuestionId = 14
};

dbContext.Answers.Add(a);
dbContext.Answers.Add(b);

dbContext.SaveChanges();

// ...

【讨论】:

  • 抱歉,我更正了输入错误。我已经按照您的建议使用流畅的 API 映射,请参阅上面的代码。 : this.Property(t => t.AnswerId) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
  • Amin - 我的问题是所有工作都在客户端完成。答案对象被添加到问题对象中。当用户单击保存时,这将作为带有新答案的 Question 对象传递给服务器。我原以为 EF 能够使用新的 Answer 对象来管理问题的更新。也许这在 EF 中是不可能的,但这似乎是一种基本需求。
  • @Alan 如果您想像这样更新它们_uow.Questions.Update(question);,您必须添加问题的所有新旧答案,然后更新它...
  • @Amin - 我们如何从问题对象中挑选出答案对象。更新问题,然后将它们添加回来?我们是否必须将它们一一添加以解决为答案生成身份键值的问题。
  • @Melina 如果您想要一个完整、可靠且无错误的方法,您有几项工作要做。请参阅this 向您展示如何操作。我也有一些关于这个领域的答案,比如this
【解决方案3】:

试试这些:

  • 使用DbSetCreate()方法
  • 将新实例添加到您的ContextAnswers 集合中

您已适当设置QuestionId 以便EF 实现关系。此外,请勿将 AnswerId 显式设置为零。

var a = new _uow.Answers.Create();
a.Text = "AAA";
a.QuestionId = 14;
_uow.Answers.Add(a);

var b = new _uow.Answers.Create();
b.Text = "BBB";
b.QuestionId = 14;
_uow.Answers.Add(a);

如果您打算查询Answers 集合Question 14,您可能需要致电_uow.ChangeTracker.DetectChanges()

【讨论】:

  • 这个问题是我在这里模拟客户端上发生的事情,其中​​两个 Answer 对象已经被添加并作为 Question 对象的一部分出现。完整的 Question 对象作为 JSON 传递给服务器。因此,我不能先创建答案,然后以这种方式添加它们。此外,我认为 EF 的全部意义在于它应该能够应对这种情况,因为这似乎是一件相当普遍的事情。感谢您的帮助/建议。
【解决方案4】:

如果您已正确地将 Id 声明为 Key 并作为 DBGenerated 标识。 然后 EF 将允许您在保存之前将其中的许多添加到上下文中。 您不能使用相同的密钥附加项目。 Attach 用于离线数据、放入上下文、设置其状态和保存类型场景。

您要么使用了同一个实例两次,并且默认情况下使用 EF 跟踪导致一团糟。 或者以某种方式使用了 ATTACH 两次。确保您干净地处理您的实例。*

例如

public class BaseEntityLongConfiguration<T> : EntityTypeConfiguration<T> where T : BaseObjectLong {
    public BaseEntityLongConfiguration(DatabaseGeneratedOption DGO = DatabaseGeneratedOption.Identity) {

        // Primary Key
        this.HasKey(t => t.Id);

        // Properties
        //Id is an indent allocated by DB
        this.Property(t => t.Id).HasDatabaseGeneratedOption(DGO); // default to db generated

        this.Property(t => t.RowVersion)   // for concurrency
            .IsRequired()
            .IsFixedLength()
            .HasMaxLength(8)
            .IsRowVersion();
    }
}

A 刚刚尝试了一个简单的测试来检查它是否有效(在 ef5 中)

public class ExampleLog  {
    public virtual long Id   { get; set; }
    public virtual string MessageText { get; set; }
}

[TestMethod]
    public void ExampleLogTest() {
        var e1 = new ExampleLog();
        e1.MessageText = "example1";
        var e2 = new ExampleLog();
        e2.MessageText = "example2";
        _context.Set<ExampleLog>().Add(e1);
        _context.Set<ExampleLog>().Add(e2);
     var res =   _context.SaveChanges();
      Debug.WriteLine("result expected 2->" + res.ToString());
    }

编辑:应要求,添加保存存储库模式、基本示例、删除错误处理

public class RepositoryBase<TPoco> : where TPoco :    BaseObject {
    public RepositoryBase(BosBaseDbContext context) { Context = context; }

....

   /// <summary>
    /// Add new POCO 
    /// </summary>
    public virtual OperationResult Add(TPoco poco) {
        var opResult = new OperationResult();
        try {
          Context.Set<TPoco>().Add(poco);
        }
        catch (Exception ex) {
         .... custom error tool
            return opResult;
        }
        return opResult;
    }
     /// <summary>
    /// Poco must already be attached,, detect chnages is triggered
    /// </summary>
    public virtual OperationResult Change(TPoco poco) {
        var opResult = new OperationResult();
        try {    // ONLY required if NOT using chnage tracking enabled
            Context.ChangeTracker.DetectChanges();
        }
        catch (Exception ex) {
         .... custom error tool
            return opResult;
        }
        return opResult;
    }

【讨论】:

  • Phil - 我在上面的代码中有 AnswerId 的声明,但您的示例与我的非常不同。我有一个父对象,我也添加了子对象,然后执行 SaveChanges。如果我按照您的示例进行操作,那么它也可以正常工作。
  • 同意 sn-p 在新实例中看起来不错。如果这真的是代码。 _uow.Questions.Update(question); 做什么?做 ?我还假设提交只是调用 SAveChanges。那么唯一明显的区别是我总是在 POCO 道具上使用虚拟
  • 更新执行以下操作: public virtual void Update(T entity) { DbEntityEntry dbEntityEntry = DbContext.Entry(entity); if (dbEntityEntry.State == EntityState.Detached) { DbSet.Attach(entity); } dbEntityEntry.State = EntityState.Modified;抱歉,它在此评论区的显示方式。是的,提交确实调用了 savechanges。您指的是哪个 POCO 属性。我认为所有需要它的都在我的代码中声明为虚拟的。
  • 有问题...附上你不能附上同一个钥匙两次。生病更新答案以强调点
  • 谢谢菲尔。我在问题中添加了我的更新代码。是否有一些其他通用更新可以添加到存储库并使用?如果您给出答案,您能否就我如何重新编码提出建议谢谢。
【解决方案5】:

因为我正在经历同样的问题。所以想提交我的解决方案。 这将对其他将要搜索的人有所帮助-

using (var db = new EFService())
{
    var question= db.Question.Single(p => p.QuestionID == QuestionID);
    question.Answers.Add(a);
    question.Answers.Add(b);
    db.SaveChanges();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-16
    • 1970-01-01
    • 2010-10-16
    • 2015-10-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多