EF 6 SaveChanges具有对同一(更改)对象的多个引用

时间:2015-04-24 19:32:02

标签: c# entity-framework

我有一个类,有两个对同一个类的引用。更新主类时,我也可以更新引用的类。当我有两个对同一(修改)对象的引用时,我得到一个InvalidOperationException:

  

附加“ns.entity”类型的实体失败,因为同一类型的另一个实体已具有相同的主键值。如果图中的任何实体具有冲突的键值,则在使用“附加”方法或将实体的状态设置为“未更改”或“已修改”时,可能会发生这种情况。这可能是因为某些实体是新的并且尚未收到数据库生成的键值。在这种情况下,使用“添加”方法或“已添加”实体状态来跟踪图表,然后根据需要将非新实体的状态设置为“未更改”或“已修改”。

简单示例:

public class Example {
    public int OldestFriendId {get; set;}
    public int BestFriendId {get; set;}
    public virtual Friend Oldest {get; set; }
    public virtual Friend Best {get; set; }

 }

如果在更新示例时,我想更新我的Oldest / Best好友的中间名,只要它们不相同,它就会起作用。但如果它们是相同的,那么我得到上述例外。

我无法弄清楚如何让它发挥作用。我已经尝试将引用设置为null,独立于父类保存它们,将它们中的所有引用设置为null(EF会自动在Friend中创建两个示例列表)。

如果有多个引用对象时,如何保存已更改的对象?

更新:尚未按照我想要的方式工作,但在从Friend中删除示例列表后我取得了一些进展。此外,更新是POST的结果。还在调查......

由于示例代码被要求...这是来自网络应用上的帖子,实际上没有进行任何更改

public ActionResult SaveEdit(int id, [Bind(Include = "OldestFriendId, BestFrinedId, Oldest, Best")] Example example)
{
    if (ModelState.IsValid)
        {
            using (((WindowsIdentity)ControllerContext.HttpContext.User.Identity).Impersonate())
            {
                using (var _db = new exampleEntities())
                {
                  //example.Best= example.Oldest; // this line would allow the update to work.

                  //next line is where the exception occurs
                   _db.Entry(example).State = EntityState.Modified;
                   _db.SaveChanges();
                 }
            }
        }
}

EditorFor模板:

@model Testing.Friend

<div class="col-md-10">
    @Html.HiddenFor(model => model.FriendId)
    @Html.EditorFor(model => model.FirstName)
    @Html.EditorFor(model => model.LastName)
</div>

示例的编辑视图

@model Testing.Example

@using (Html.BeginForm())
{
@Html.AntiForgeryToken()

<div class="form-horizontal">
    <h4>Example</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.ExampleId)

    <div class="form-group">
        @Html.LabelFor(model => model.OldestFriendId, "OldestFriendId", htmlAttributes: new { @class = "control-label col-md-2" })
        @Html.HiddenFor(model => model.OldestFriendId)
        @Html.EditorFor(model => model.Oldest)
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.BestFriendId, "BestFriendId", htmlAttributes: new { @class = "control-label col-md-2" })
        @Html.HiddenFor(model => model.BestFriendId)
        @Html.EditorFor(model=> model.Best)
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Save" class="btn btn-default" />
        </div>
    </div>
</div>

}

3 个答案:

答案 0 :(得分:2)

修改

最可能的原因是因为当您回溯对象时,它会将2个朋友反序列化为2个完全不同的对象(即使它们是相同的)。与下面的问题相同,但不是EF反序列化为2个对象,ASP.NET MVC正在这样做。

您需要做的事情如下:

  1. 检查2个朋友ID是否相同(因为ID是PK)。如果不能正常继续
  2. 如果它们具有相同的ID,请检查2个朋友对象是否相同。
  3. 如果相同则转到第5步。
  4. 将所有更改合并在一起,但是您想要处理冲突。
  5. 将其中一个Freinds设置为与其他朋友参考相同,例如Best = Oldest
  6. SaveChanges()
  7. 原始答案

    我的猜测是,当您检索数据时,这是Include的典型问题。

    当你这样做时

     Context.Examples.Include(x => x.Oldest).Include(x => x.Best).ToList()
    

    即使他们指向相同的记录,EF也会创建两个朋友的对象(最旧和最好)。这是包含的已知问题。

    因此,当您在更新后进行保存时,EF将它们视为具有相同密钥(和数据)并抱怨的2个独立实体。

    如果是这种情况,您有几个选择:

    • 检索当前示例的所有Friends的列表,然后检索不包含
    • Example
    • 让EF使用LazyLoading并在需要时加载好友。

答案 1 :(得分:1)

我解决这个问题的方法是停止绑定整个对象,然后绑定到各个对象。

public ActionResult SaveEdit(int id, [Bind(Include = "OldestFriendId, BestFrinedId")] Example example,
     Bind(Prefix="Oldest", Include = "FriendId, FirstName, MiddleName, LastName")] Friend oldest, 
     Bind(Prefix="Best", Include = "FriendId, FirstName, MiddleName, LastName")] Friend best) {
if (ModelState.IsValid)
    {
        using (((WindowsIdentity)ControllerContext.HttpContext.User.Identity).Impersonate())
        {
            using (var _db = new exampleEntities())
            {
               // do whatever processing you want on best and/or oldest
               example.BestFriendId = best.FriendId;
               example.OldestFriendId = oldest.FriendId;

               _db.Entry(example).State = EntityState.Modified;
               _db.SaveChanges();
             }
        }
    }
}

答案 2 :(得分:0)

编辑:替换为完整的示例代码 这个例子适合我。 我认为你正在尝试的是什么。

using System;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;

namespace Ef6Test {
public class Program {
    public static void Main(string[] args) {
        ExecDb1();

    }

    private static void ExecDb1() {
        Database.SetInitializer(new MigrateDatabaseToLatestVersion<Ef6Ctx, Ef6MigConf>());
        WhichDb.DbName = "MSAMPLEDB";
        WhichDb.ConnType = ConnType.CtxViaDbConn;
        var sqlConn = GetSqlConn4DBName(WhichDb.DbName);
        var context = new Ef6Ctx(sqlConn);
        context.Database.Initialize(true);
        Console.WriteLine(WhichDb.DbName, context.Database.Exists() );
        AddJunk(context);

    }

    public static class WhichDb {
        public static string DbName { get; set; }
        public static string ConnectionName { get; set; }
        public static ConnType ConnType { get; set; }
    }

    public enum ConnType {
        CtxViaDbConn,
        CtxViaConnectionName
    }

    private static void AddJunk(DbContext context) {
        var friend = new Friend();
        friend.Name = "Fred";
        friend.Phone = "555-1232424";
        context.Set<Friend>().Add(friend);
        context.SaveChanges();

        // break here and check db content.

        var eg = new Example();
        eg.Best = friend;  // set them equal
        eg.Oldest = friend;
        friend.Name = "Fredie"; // change the name of the fly
        friend.Phone = "555-99999"; // and phone is also different
        context.Set<Example>().Add(eg); Add the new example
        context.SaveChanges();

        // result... 2 records.
        // The original friend record should be chnaged
    }

    public static DbConnection GetSqlConn4DBName(string dbName) {
        var sqlConnFact = new SqlConnectionFactory(
            "Data Source=localhost; Integrated Security=True; MultipleActiveResultSets=True");
        var sqlConn = sqlConnFact.CreateConnection(dbName);
        return sqlConn;
    }
}

public class MigrationsContextFactory : IDbContextFactory<Ef6Ctx> {
    public Ef6Ctx Create() {
        switch (Program.WhichDb.ConnType) {
            case Program.ConnType.CtxViaDbConn:
                var sqlConn = Program.GetSqlConn4DBName(Program.WhichDb.DbName); //
                return new Ef6Ctx(sqlConn);

            case Program.ConnType.CtxViaConnectionName:
                return new Ef6Ctx(Program.WhichDb.ConnectionName);
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}

public class Ef6MigConf : DbMigrationsConfiguration<Ef6Ctx> {
    public Ef6MigConf() {
        AutomaticMigrationsEnabled = true;
        AutomaticMigrationDataLossAllowed = true;
    }
}



public class Friend {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Phone { get; set; }

}

public class Example
{
    public int Id { get; set; }
    public int? BestFriendId { get; set; }
    public int? OldestFriendId { get; set; }
    public virtual Friend Best { get; set; }
    public virtual Friend Oldest { get; set; }


 }

   public class Ef6Ctx : DbContext {
    public Ef6Ctx(DbConnection dbConn) : base(dbConn, true) { }

    public Ef6Ctx(string connectionName) : base(connectionName) { }

    public DbSet<Friend> Friends { get; set; }


    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {


        modelBuilder.Entity<Example>()
                    .HasOptional(t=>t.Best)
                    .WithMany()
                    .HasForeignKey(x=>x.BestFriendId);

        modelBuilder.Entity<Example>()
                    .HasOptional(t => t.Oldest)
                    .WithMany()
                    .HasForeignKey(x => x.OldestFriendId);

        }
 }

}