【问题标题】:EF Code First 4.1 - How to configure one to many relationship with defaultEF Code First 4.1 - 如何使用默认配置一对多关系
【发布时间】:2011-05-09 12:28:36
【问题描述】:

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

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

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

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

非常感谢,

保罗。

【问题讨论】:

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


    【解决方案1】:

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

    【讨论】:

    • 谢谢你,你在这里说的很有意义。我会试试这个并在这里更新我的进展情况。
    • 我现在已经对此进行了测试,它对我们有用。正如您所说,这很丑陋,但我看不到另一种方法。感谢您的帮助。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-18
    相关资源
    最近更新 更多