EF Code First 4.1 - 如何使用默认配置一对多关系

时间:2011-05-09 12:28:37

标签: entity-framework entity ef-code-first fluent-interface

我有一个Customer实体,它引用了一组地址。这里的复杂性是我希望能够将特定地址识别为默认地址。

如果可能,我想在Customer表中保留默认地址的FK。这似乎比在地址表中有一个列来识别默认值更优雅。

我在定义这种关系方面遇到了流畅的API问题。当我运行以下代码时,我得到一个异常,其中说: “保存未公开其关系的外键属性的实体时发生错误.EntityEntries属性将返回null,因为无法将单个实体标识为异常的来源。保存时可以更轻松地处理异常通过在实体类型中公开外键属性。有关详细信息,请参阅InnerException。“ “无法确定依赖操作的有效排序。由于外键约束,模型要求或存储生成的值,可能存在依赖关系。”

我创建了一个控制台应用程序来显示确切的问题。在这个测试应用程序中,我有一个Customer实体,一个Address和flient api配置。

非常感谢任何帮助:

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 个答案:

答案 0 :(得分:7)

我不明白EF在那里谈论的是关于异常中“未暴露的外键”的内容。我认为内部异常是重要的部分:

  

无法确定有效的订购   对于从属操作。依赖   由于外键可能存在   约束,模型要求或   存储生成的值。

我认为您的模型中的问题是您在CustomerAddress之间存在相互依赖关系:地址需要客户(您已将其标记为必需您的映射代码)另一方面,客户需要一个地址(默认地址必需,因为不可为空的外键和您的映射代码)。那么,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很难看,但我看不到另一种方式。