EF Code First 4.1 - Как настроить отношения «один ко многим» по умолчанию - PullRequest
6 голосов
/ 09 мая 2011

У меня есть объект Customer, который ссылается на коллекцию адресов.Сложность здесь в том, что я хочу иметь возможность идентифицировать конкретный адрес в качестве адреса по умолчанию.

Если возможно, я бы хотел сохранить FK адреса по умолчанию в таблице Customer.Это кажется более элегантным, чем наличие столбца в таблице адресов для определения значения по умолчанию.

У меня возникли проблемы с плавным API в плане определения этой взаимосвязи.Когда я запускаю следующий код, я получаю исключение, которое говорит: "Произошла ошибка при сохранении сущностей, которые не предоставляют свойства внешнего ключа для их отношений. Свойство EntityEntries будет возвращать ноль, поскольку одна сущность не может быть идентифицирована как источникисключения. Обработка исключений при сохранении может быть упрощена путем предоставления свойств внешнего ключа в типах объектов. Подробности см. в InnerException. "«Невозможно определить действительный порядок для зависимых операций. Зависимости могут существовать из-за ограничений внешнего ключа, требований модели или сгенерированных в хранилище значений.»

Я создал консольное приложение, чтобы показать точную проблему.В этом тестовом приложении у меня есть объект Customer, адрес и конфигурация API-интерфейса flient.

Любая помощь будет принята с благодарностью:

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

namespace OneToManyWithDefault
{

    public class Customer
    {
        private ICollection<Address> m_Addresses;

        public Customer()
        {
            Addresses = new List<Address>();
        }

        public int Id { get; set; }
        public string CompanyName { get; set; }
        public virtual ICollection<Address> Addresses
        {
            get
            {
                if (m_Addresses == null)
                {
                    m_Addresses = new List<Address>();
                }
                return m_Addresses;
            }
            set
            {
                m_Addresses = value;
            }
        }
        public Address DefaultAddress { get; set; }
        public int DefaultAddressId { get; set; }

    }

    public class Address
    {
        public int Id { get; set; }
        public string Town { get; set; }
        public Customer Customer { get; set; }
    }

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

        public MyContext(string connectionString)
            : base(connectionString)
        {

        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new CustomerConfiguration());
            modelBuilder.Configurations.Add(new AddressConfiguration());
            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.CompanyName)
                .HasColumnName("Name")
                .IsRequired();

            // Configure the mapping for the Default Address (this is likely to be wrong!):
            HasRequired(p => p.DefaultAddress).WithMany()
                .Map(x => x.MapKey("DefaultAddressId"))
                .WillCascadeOnDelete(false);
            HasRequired(p => p.DefaultAddress)
                .WithMany()
                .HasForeignKey(x => x.DefaultAddressId);

            ToTable("Customers");
        }
    }

    public class AddressConfiguration
        : EntityTypeConfiguration<Address>
    {
        public AddressConfiguration()
            : base()
        {
            HasKey(p => p.Id);
            Property(p => p.Id)
                .HasColumnName("Id")
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
                .IsRequired();
            Property(p => p.Town)
                .HasColumnName("Town")
                .IsRequired();

            HasRequired(p => p.Customer)
                .WithMany(c => c.Addresses)
                .Map(x => x.MapKey("CustomerId"));

            ToTable("Addresses");
        }
    }

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

        static void Main(string[] args)
        {
            Customer headOffice = new Customer();
            headOffice.CompanyName = "C1";

            Address address = new Address();
            address.Town = "Colchester";
            headOffice.Addresses.Add(address);

            address = new Address();
            address.Town = "Norwich";
            headOffice.Addresses.Add(address);
            headOffice.DefaultAddress = address;

            MyContext context = new MyContext(ConnectionString);
            context.Customers.Add(headOffice);
            context.SaveChanges();

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

Большое спасибо,

Пол.

1 Ответ

7 голосов
/ 09 мая 2011

Я не понимаю, что EF говорит там об "незащищенных внешних ключах" в исключении.Я бы рассмотрел внутреннее исключение как важную часть:

Невозможно определить действительный порядок для зависимых операций.Зависимости могут существовать из-за ограничений внешнего ключа, требований модели или сгенерированных в магазине значений.

Я думаю, что проблема в вашей модели заключается в том, что у вас есть взаимозависимость между Customer и Address:Для адреса нужен клиент (вы пометили его как обязательный в коде сопоставления), а с другой стороны, клиенту нужен адрес (адрес по умолчанию: обязательный , так какобнуляемый внешний ключ и из-за вашего кода сопоставления).Итак, EF не знает, какой объект сохранить первым в вашем примере кода - адрес по умолчанию или клиент?Обе сущности нуждаются в первичном ключе другого, который должен быть сохранен с действительными ограничениями FK.

Самое простое решение, которое я вижу, это сделать адрес по умолчанию необязательным в вашей модели и затем сохранить дважды (я опускаю сопоставления, которые работаютв любом случае по соглашению):

public class Customer
{
    private ICollection<Address> m_Addresses;

    public Customer() { Addresses = new List<Address>(); }

    public int Id { get; set; }
    public string CompanyName { get; set; }
    public virtual ICollection<Address> Addresses { get { ... } set { ... } }
    public Address DefaultAddress { get; set; }
    public int? DefaultAddressId { get; set; } // FK for optional relationship
}

public class Address
{
    public int Id { get; set; }
    public string Town { get; set; }
    public Customer Customer { get; set; }
}

// ...

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration() : base()
    {
        Property(p => p.CompanyName)
            .HasColumnName("Name")
            .IsRequired();

        HasMany(c => c.Addresses)
            .WithRequired(a => a.Customer)
            .Map(x => x.MapKey("CustomerId"));
    }
}

public class AddressConfiguration : EntityTypeConfiguration<Address>
{
    public AddressConfiguration() : base()
    {
        Property(p => p.Town)
            .HasColumnName("Town")
            .IsRequired();
    }
}

И тогда ваша программа будет выглядеть так:

static void Main(string[] args)
{
    Customer headOffice = new Customer();
    headOffice.CompanyName = "C1";

    Address address = new Address();
    address.Town = "Colchester";
    headOffice.Addresses.Add(address);

    address = new Address();
    address.Town = "Norwich";
    headOffice.Addresses.Add(address);

    //headOffice.DefaultAddress = address;
    //We don't set the default address here as SaveChanges would throw an
    //exception. But because it is optional now we are allowed to leave it null.

    MyContext context = new MyContext(ConnectionString);
    context.Customers.Add(headOffice);
    context.SaveChanges();

    headOffice.DefaultAddress = address; // headoffice and address have now PKs
    context.SaveChanges(); // Updates headoffice in the DB with default address
}

Этот двойной SaveChanges ужасен, но я не вижу другого пути.

...