实体框架 - EF Code First 4.1 - 如何使用默认值配置一对多关系



我有一个引用地址集合的客户实体。 这里的复杂之处在于我希望能够将特定地址标识为默认地址。

如果可能的话,我想在客户表中保存默认地址的 FK。 这似乎比在地址表中使用一列来标识默认值更优雅。

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

我创建了一个控制台应用程序来显示确切的问题。 在此测试应用程序中,我有一个客户实体、一个地址和 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();
        }
    }
}

非常感谢,

保罗。

我不明白 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很丑陋,但我看不到其他方法。

最新更新