尝试保存我的实体时,我收到以下异常:
“AcceptChanges无法继续,因为对象的键值与ObjectStateManager中的另一个对象冲突。在调用AcceptChanges之前,请确保键值是唯一的。”
我正在创建一个3层应用程序,其中数据访问层使用EF Code First,并且客户端使用WCF调用中间层。因此,当在客户端上构建实体时,我无法让上下文跟踪实体状态。
在某些情况下,我发现对象图中包含两次相同的实体。在这种情况下,当我尝试设置副本的实体状态时,它会失败。
例如,我有以下实体: 顾客 国家 Curreny
所以我最终得到的是对象图中同一个实体的两个实例。
当保存实体(在我的服务中)时,我需要告诉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();
}
}
}
答案 0 :(得分:5)
我猜你只有在customer.Currency
和customer.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 :(得分:0)
我认为问题是因为您将EntityState设置为Unchanged。如果实体密钥始终存在且实体状态未添加,则只会发生异常。
请参阅http://msdn.microsoft.com/en-us/library/bb896271.aspx
附加对象的注意事项的最后一段是: “当附加的对象具有与对象上下文中已存在的不同对象相同的EntityKey时,会发生InvalidOperationException。如果上下文中的对象具有相同的键但处于已添加状态,则不会发生此错误。”
所以问题是,你为什么要强制状态为Unchanged而不是将其添加为添加状态?
编辑: 在再次查看您的帖子和您的评论后编辑。最终问题是你告诉EF“嘿,用这个客户添加这些货币和国家对象”,但其中两个对象已经存在。
您可以使用Attach而不是Add方法,但客户尚不存在。
我建议在事务管理器中包装这些调用,在创建Customer之后立即调用SaveChanges,而不是使用Attach而不是Add。如果出现错误,可以根据需要回滚事务。我没有方便的代码示例,但我所说的有意义吗?
类似的东西:
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();
}
答案 2 :(得分:0)
我在Windows服务中遇到了同样的问题,并通过在每次插入/更新/获取调用中创建和处理DBContext来解决它。我之前将dbContext作为私有变量保存在我的repos中并重用它。
到目前为止一切顺利。因人而异。我不能说我理解为什么它有效 - 我还没有深入到Code First中。神奇的独角兽功能很不错,但是我很想把它扔掉并手动编写TSQL代码,因为魔法使得很难真正理解正在发生的事情。