EF 4.1 Code First - повторяющиеся объекты в графе объектов вызывают исключение - PullRequest
7 голосов
/ 08 июня 2011

При попытке сохранить мою сущность я получаю следующее исключение:

"AcceptChanges не может продолжить работу, поскольку значения ключа объекта конфликтуют с другим объектом в ObjectStateManager. Убедитесь, что значения ключа уникальныперед вызовом AcceptChanges. "

Я создаю трехуровневое приложение, в котором уровень доступа к данным сначала использует код EF, а клиент вызывает средний уровень с помощью WCF.Поэтому я не могу позволить контексту отслеживать состояние объекта при создании объекта на клиенте.

В некоторых ситуациях я обнаруживаю, что один и тот же объект дважды содержится в графе объектов.В этой ситуации происходит сбой, когда я пытаюсь установить состояние объекта дубликата.

Например, у меня есть следующие объекты: Страна клиента Валюта

  1. От клиента, которого я создаюновый экземпляр клиента.Затем я звоню в службу поддержки, чтобы получить экземпляр Country и назначить его Заказчику.Экземпляр Country имеет связанную валюту.
  2. Пользователь может затем связать валюту с клиентом.Они вполне могут выбрать ту же валюту, которая связана со страной.
  3. Я звоню в другой сервис, чтобы получить это.Таким образом, на этом этапе у нас могут быть два отдельных экземпляра одной и той же валюты.

Итак, в результате я получаю два экземпляра одного и того же объекта в графе объектов.

Когда тогдаСохраняя сущность (в моем сервисе), я должен сказать EF, что обе сущности Валюты не изменены (если я этого не сделаю, я получу дубликаты).Проблема в том, что я получаю исключение выше.

При сохранении, если я устанавливаю экземпляр Currency на экземпляре Country на null, это решает проблему, но я чувствую, что код становится все более запутанным (из-за этого и другихОбходные пути EF, связанные с WCF, которые я должен применить).

Есть ли какие-либо предложения о том, как решить эту проблему лучше?

Большое спасибо за любую помощь заранее.Вот код:

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



}

Ответы [ 3 ]

5 голосов
/ 09 июня 2011

Полагаю, вы получите исключение, только если 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;). Перезагрузите валюты в вашем последнем контексте и назначьте их клиенту.

Но на самом деле это не самый «хороший» способ решения проблемы, а только другой - на мой взгляд.

0 голосов
/ 13 февраля 2012

У меня была такая же проблема в службе Windows, и я решил ее, создав и утилизировав DBContext при каждом вызове вставки / обновления / получения. Ранее я держал dbContext в качестве закрытой переменной в своих репозиториях и использовал его повторно.

Пока все хорошо. YMMV. Не могу сказать, что точно понимаю, почему это работает - я еще недостаточно углубился в Code First. Волшебные возможности единорога хороши, но я готов выкинуть его и написать код TSQL, потому что магия затрудняет понимание того, что происходит.

0 голосов
/ 08 июня 2011

Я думаю, что проблема в том, что вы устанавливаете EntityState в значение Без изменений.Исключение, которое вы видите, происходит только в том случае, если ключи сущности всегда существуют И состояние сущности не добавлено.

См. http://msdn.microsoft.com/en-us/library/bb896271.aspx

Последний абзац соображений для присоединения объектов: «InvalidOperationException возникает, когда присоединяемый объект имеет тот же EntityKey, что и другой объект, уже присутствующий в контексте объекта.Эта ошибка не возникает, если объект в контексте с тем же ключом, но находится в состоянии «Добавлен». "

Итак, вопрос в том, почему вы принудительно изменяете состояние на Не вместо того, чтобы оставлять его добавленным?

РЕДАКТИРОВАТЬ: Отредактировано после повторного просмотра вашего поста и вашего комментария.В конечном итоге проблема заключается в том, что вы говорите EF: «Эй, добавьте эти объекты Currency и Country с этим клиентом», но два из этих объектов уже существуют.

Вы можете использовать метод Attach вместо Add, но клиент еще не существует.

Я предлагаю обернуть эти вызовы в область транзакций, вызывая SaveChanges сразу после создания Customer, чем использовать 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();
            }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...