2011-05-09 25 views
6

Tengo una entidad Cliente que hace referencia a una colección de Direcciones. La complicación aquí es que quiero poder identificar una dirección particular como la dirección predeterminada.Código EF Primero 4.1 - Cómo configurar una relación de uno a muchos con el valor predeterminado

De ser posible, me gustaría mantener el FK de la dirección predeterminada en la tabla Cliente. Esto parece más elegante que tener una columna en la tabla de direcciones para identificar el valor predeterminado.

Tengo dificultades con la API fluida en términos de definición de esta relación. Cuando ejecuto el siguiente código, recibo una excepción que dice: "Se produjo un error al guardar entidades que no exponen propiedades de claves foráneas para sus relaciones. La propiedad EntityEntries devolverá nulo porque no se puede identificar una sola entidad como fuente de la excepción. El manejo de excepciones mientras se guarda puede ser más fácil al exponer propiedades de claves foráneas en sus tipos de entidades. Consulte InnerException para más detalles. " "No se puede determinar un pedido válido para las operaciones dependientes. Las dependencias pueden existir debido a restricciones de clave externa, requisitos del modelo o valores generados en la tienda".

Creé una aplicación de consola para mostrar el problema exacto. En esta aplicación de prueba tengo una entidad de Cliente, una Dirección y la configuración de API de flient.

Cualquier ayuda sería muy apreciada:

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

Muchas gracias,

Paul.

Respuesta

7

No entiendo qué EF está hablando allí acerca de "claves externas no expuestas" en la excepción. Consideraría la excepción interna como la parte importante:

No se pudo determinar un pedido válido para operaciones dependientes. Las dependencias pueden existir debido a restricciones de clave externa , requisitos de modelo o valores generados en la tienda.

Creo que el problema en su modelo es que tiene una dependencia mutua entre Customer y Address: Una dirección necesita un cliente (que haya marcado como requerido en su código de mapeo) y, por otro lado, una el cliente necesita una dirección (la dirección predeterminada es requiere tanto debido a la clave externa no anulable como debido a su código de asignación). Entonces, EF no sabe qué entidad debe guardar primero en su código de ejemplo: ¿la dirección predeterminada o el cliente? Ambas entidades necesitan la clave principal del otro para guardarse con restricciones válidas de FK.

La solución más fácil que puedo ver es hacer que la dirección por defecto opcional en el modelo y luego guardar dos veces (Omito las asignaciones que funcionan mediante la convención de todos modos):

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

Y entonces el programa se parecería esto:

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 
} 

esta doble SaveChanges es feo, pero no veo otra manera.

+0

Gracias por esto, tiene sentido lo que dices aquí.Voy a probar esto y actualizar aquí con la forma en que avanzo. – P2l

+0

He probado esto y funciona para nosotros. Como dices, es feo pero no puedo ver otro enfoque. Gracias por tu ayuda. – P2l

Cuestiones relacionadas