2011-06-08 90 views
7

试图保存我的实体时,我收到以下异常:EF 4.1代码第一次 - 复制的对象图形实体导致异常

“的AcceptChanges不能继续,因为该对象的键值与在ObjectStateManager另一个对象发生冲突。在调用AcceptChanges之前确保键值是唯一的。“

我创建了一个3层应用程序,其中数据访问层使用EF Code First,以及客户端使用WCF调用中间层。因此,我无法让上下文在客户端上建立实体时跟踪实体状态。

在某些情况下,我发现相同的实体在对象图中包含两次。在这种情况下,当我尝试设置副本的实体状态时,它会失败。

例如,我有以下实体: 客户 国家 Curreny

  1. 从我创建了一个客户的新 实例的客户。然后,我拨打 服务电话获取国家/地区 实例并将其分配给客户。 Country实例具有 关联的货币。
  2. 然后,用户可以将货币与客户关联起来。他们 可能会选择与国家相关的同一货币 。
  3. 我拨打另一个服务电话获得 这个。因此在这个阶段,我们可能有 有两个不同的 相同货币的实例。

所以我最终得到的是对象图中同一个实体的两个实例。

然后保存实体(在我的服务)我需要告诉EF两个货币实体没有修改(如果我不这样做,我得到重复)。问题是我得到上面的例外。

保存如果我将Country实例上的Currency实例设置为null,它会解决问题,但我觉得代码变得越来越混乱(由于这个和其他WCF相关的EF解决方法,我不得不放在地点)。

有没有关于如何以更好的方式解决这个问题的建议?

非常感谢任何帮助提前。代码如下:

using System; 
using System.Collections.Generic; 
using System.Data.Entity.ModelConfiguration; 
using System.ComponentModel.DataAnnotations; 
using System.Data.Entity; 
using System.Linq; 

namespace OneToManyWithDefault 
{ 

    public class Customer 
    { 
     public int Id { get; set; } 
     public string Name { get; set; } 
     public Country Country { get; set; } 
     public Currency Currency { get; set; } 
     public byte[] TimeStamp { get; set; } 
    } 

    public class Country 
    { 
     public int Id { get; set; } 
     public string Name { get; set; } 
     public Currency Currency { get; set; } 
     public byte[] TimeStamp { get; set; } 
    } 

    public class Currency 
    { 
     public int Id { get; set; } 
     public string Symbol { get; set; } 
     public byte[] TimeStamp { get; set; } 
    } 


    public class MyContext 
     : DbContext 
    { 
     public DbSet<Customer> Customers { get; set; } 
     public DbSet<Currency> Currency { get; set; } 
     public DbSet<Country> Country { get; set; } 

     public MyContext(string connectionString) 
      : base(connectionString) 
     { 
      Configuration.LazyLoadingEnabled = false; 
      Configuration.ProxyCreationEnabled = false; 
     } 

     protected override void OnModelCreating(DbModelBuilder modelBuilder) 
     { 
      modelBuilder.Configurations.Add(new CustomerConfiguration()); 
      modelBuilder.Configurations.Add(new CountryConfiguration()); 
      modelBuilder.Configurations.Add(new CurrencyConfiguration()); 
      base.OnModelCreating(modelBuilder); 
     } 
    } 

    public class CustomerConfiguration 
     : EntityTypeConfiguration<Customer> 
    { 
     public CustomerConfiguration() 
      : base() 
     { 
      HasKey(p => p.Id); 
      Property(p => p.Id) 
       .HasColumnName("Id") 
       .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) 
       .IsRequired(); 
      Property(p => p.TimeStamp) 
       .HasColumnName("TimeStamp") 
       .IsRowVersion(); 

      ToTable("Customers"); 
     } 
    } 

    public class CountryConfiguration 
     : EntityTypeConfiguration<Country> 
    { 
     public CountryConfiguration() 
      : base() 
     { 
      HasKey(p => p.Id); 
      Property(p => p.Id) 
       .HasColumnName("Id") 
       .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) 
       .IsRequired(); 
      Property(p => p.TimeStamp) 
       .HasColumnName("TimeStamp") 
       .IsRowVersion(); 

      ToTable("Countries"); 
     } 
    } 

    public class CurrencyConfiguration 
     : EntityTypeConfiguration<Currency> 
    { 
     public CurrencyConfiguration() 
      : base() 
     { 
      HasKey(p => p.Id); 
      Property(p => p.Id) 
       .HasColumnName("Id") 
       .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) 
       .IsRequired(); 
      Property(p => p.TimeStamp) 
       .HasColumnName("TimeStamp") 
       .IsRowVersion(); 

      ToTable("Currencies"); 
     } 
    } 

    class Program 
    { 
     private const string ConnectionString = 
      @"Server=.\sql2005;Database=DuplicateEntities;integrated security=SSPI;"; 

     static void Main(string[] args) 
     { 
      // Seed the database 
      MyContext context1 = new MyContext(ConnectionString); 

      Currency currency = new Currency(); 
      currency.Symbol = "GBP"; 
      context1.Currency.Add(currency); 

      Currency currency2 = new Currency(); 
      currency2.Symbol = "USD"; 
      context1.Currency.Add(currency2); 

      Country country = new Country(); 
      country.Name = "UK"; 
      country.Currency = currency; 
      context1.Country.Add(country); 

      context1.SaveChanges(); 

      // Now add a new customer 
      Customer customer = new Customer(); 
      customer.Name = "Customer1"; 

      // Assign a country to the customer 
      // Create a new context (to simulate making service calls over WCF) 
      MyContext context2 = new MyContext(ConnectionString); 
      var countries = from c in context2.Country.Include(c => c.Currency) where c.Name == "UK" select c; 
      customer.Country = countries.First(); 

      // Assign a currency to the customer 
      // Again create a new context (to simulate making service calls over WCF) 
      MyContext context3 = new MyContext(ConnectionString); 
      customer.Currency = context3.Currency.First(e => e.Symbol == "GBP"); 

      // Again create a new context (to simulate making service calls over WCF) 
      MyContext context4 = new MyContext(ConnectionString); 
      context4.Customers.Add(customer); 

      // Uncommenting the following line prevents the exception raised below 
      //customer.Country.Currency = null; 

      context4.Entry(customer.Country).State = System.Data.EntityState.Unchanged; 
      context4.Entry(customer.Currency).State = System.Data.EntityState.Unchanged; 

      // The following line will result in this exception: 
      // AcceptChanges cannot continue because the object's key values conflict with another  
      // object in the ObjectStateManager. Make sure that the key values are unique before 
      // calling AcceptChanges. 
      context4.Entry(customer.Country.Currency).State = System.Data.EntityState.Unchanged; 
      context4.SaveChanges(); 

      Console.WriteLine("Done."); 
      Console.ReadLine(); 
     } 
    } 



} 
+2

+1提供了一个完整的例子来重现你的问题,准备好复制 - 粘贴 - 编译 - 运行!你显然花费了大量的工作来问这个问题。 – Slauma 2011-06-08 20:30:20

回答

5

我猜你得到的异常只有customer.Currencycustomer.Country.Currency指的是同一种货币,即具有相同的身份密钥。问题是这两个货币对象来自不同的对象上下文,因此它们是不同的对象(ReferenceEquals(customer.Currency, customer.Country.Currency)false)。当您将两个都附加到最后一个上下文时(通过设置State),会发生异常,因为它们是具有相同键的两个不同对象。在你的代码

来看,也许是最简单的办法是检查,如果你想分配给客户的货币是一样的国家的货币之前,你甚至装载的货币,是这样的:

if (customer.Country.Currency.Symbol == "GBP") 
    customer.Currency = customer.Country.Currency; 
    // currencies refer now to same object, avoiding the exception 
else 
{ 
    MyContext context3 = new MyContext(ConnectionString); 
    customer.Currency = context3.Currency.First(e => e.Symbol == "GBP"); 
} 

(我在这里假设Symbol是货币的关键,或者在数据库中是最不重要的。)如果货币相同,您还可以避免一次服务/数据库调用。

其他选项应该是:如果可以,请不要在国家/地区查询中包含货币。您的解决方案将customer.Country.Currency设置为null(一点也不差)。在添加客户(if (customer.Country.Currency.Symbol == customer.Currency.Symbol) customer.Currency = customer.Country.Currency;)之前,使上述两种货币的参考值相等。重新载入上次上下文中的货币并将其分配给客户。

但是,在我看来,这不是一个真正的“更好的方式”来解决问题,而只是另一种方式。

+1

谢谢,这些都是好的建议。令人遗憾的是,实体框架不能在对象图中可能存在重复对象的前提下工作,我认为这是相当常见的情况。我的首选解决方法是在中间层/数据访问代码中进行检查(将其设置为null,或将其设置为其他实体,如您所描述的那样)。原因在于许多人可能正在使用该服务,我不希望他们必须担心这一点。感谢您的帮助和投票! – P2l 2011-06-09 09:52:00

+1

我同意这是EF设计中的一个可怕的疏忽,在把大量的精力投入到EF之后,我们几乎成了一个交易杀手,然后运行到像这样的非常棘手的问题。我希望看到能够告诉EF不添加任何未明确告知添加的实体。这是很好的行为,人们喜欢它,但我很想*关掉它*。 – sliderhouserules 2011-06-22 20:33:43

+0

@sliderhouserules - 是的,我同意,关掉这种行为肯定会有所帮助。我们有一个相对较小的表,主要由外键组成,并且试图让EF进行更新,这是非常痛苦的,因为我们可以非常容易地手动完成。 – P2l 2011-07-04 20:32:47

0

我认为这个问题是因为您将EntityState设置为Unchanged。只有当实体键总是存在且实体状态不是添加时,才会看到异常。

参见http://msdn.microsoft.com/en-us/library/bb896271.aspx

考虑的用于附接对象的最后一段是: 时被附接的物体具有相同的EntityKey作为一个不同的对象已经存在于对象中,“发生一个InvalidOperationException不会发生此错误如果上下文中的对象具有相同的密钥但处于已添加状态。“

所以现在的问题是,你为什么要强迫状态不变,而不是把它当作增加的

编辑:? 看着再次您的文章和您的评论后,编辑最终的问题是你告诉EF“嘿,添加这些货币和国家与该客户对象”,但其中的两个物体的存在。

您可以使用附加,而不是Add方法,但客户并不存在。

我建议将这些调用包装在一个事务范围中,在c之后立即调用SaveChanges重复客户,而不是使用“附加”,而是“添加”。如果您遇到错误,您可以根据需要回滚事务。我没有一个方便的代码示例,但我所说的话是否有意义?

喜欢的东西:

     using (TransactionScope scope = new TransactionScope()) 
      { 
       // Now add a new customer 
       Customer customer = new Customer(); 
       customer.Name = "Customer1"; 

       context1.SaveChange(); 

       // Assign a country to the customer 
       // Create a new context (to simulate making service calls over WCF) 
       MyContext context2 = new MyContext(ConnectionString); 
       var countries = from c in context2.Country.Include(c => c.Currency) where c.Name == "UK" select c; 
       customer.Country = countries.First(); 

       // Assign a currency to the customer 
       // Again create a new context (to simulate making service calls over WCF) 
       MyContext context3 = new MyContext(ConnectionString); 
       customer.Currency = context3.Currency.First(e => e.Symbol == "GBP"); 

       // Again create a new context (to simulate making service calls over WCF) 
       MyContext context4 = new MyContext(ConnectionString); 
       context4.Customers.Attach(customer); 


       // The following line will result in this exception: 
       // AcceptChanges cannot continue because the object's key values conflict with another  
       // object in the ObjectStateManager. Make sure that the key values are unique before 
       // calling AcceptChanges. 
       context4.SaveChanges(); 
       scope.Complete(); 
      } 
+0

嗨,这是因为如果我将它设置为已添加,它将为Currency实体添加另一行到货币表(我不希望发生这种情况,因为表中已经有货币实体的记录)。 – P2l 2011-06-08 19:55:07

+0

为什么不尝试从context4中检索现有的Currency和Country对象,并将它们附加到已存在的地方?使用GetObjectByKey从context4获取匹配的实体,然后将其添加到客户对象并保存。 – Amasuriel 2011-06-08 21:01:34

+0

感谢您在编辑答案中的建议,但我认为它不会真正适合我们的架构。例如,在客户端上,我们希望能够建立客户和关联实体,然后将其传递给保存服务方法。我不想强迫客户不得不多次拨打电话来保存更改。 – P2l 2011-06-09 09:29:59

0

我在Windows服务中遇到了同样的问题,并通过在每次insert/update/get调用中创建和部署DBContext来解决此问题。我之前将dbContext作为私有变量保存在我的回购站中并重新使用它。

到目前为止这么好。因人而异。我不能说我明白为什么它的工作原理 - 我还没有深入到Code First。神奇的独角兽功能很不错,但我很快就会抛出它,并手动编写TSQL代码,因为魔法很难真正理解正在发生的事情。