Código EF Primera clave externa sin propiedad de navegación

91

Digamos que tengo las siguientes entidades:

public class Parent
{
    public int Id { get; set; }
}
public class Child
{
    public int Id { get; set; }
    public int ParentId { get; set; }
}

¿Cuál es la primera sintaxis de API fluida del código para hacer cumplir que ParentId se crea en la base de datos con una restricción de clave externa a la tabla Padres, sin la necesidad de tener una propiedad de navegación ?

Sé que si agrego una propiedad de navegación de padre a hijo, puedo hacer esto:

modelBuilder.Entity<Child>()
    .HasRequired<Parent>(c => c.Parent)
    .WithMany()
    .HasForeignKey(c => c.ParentId);

Pero no quiero la propiedad de navegación en este caso particular.

RationalGeek
fuente
1
No creo que esto sea realmente posible con solo EF, probablemente necesite usar algo de SQL sin procesar en una migración manual para configurarlo
no me
@LukeMcGregor eso es lo que temía. Si lo convierte en una respuesta, estaré encantado de aceptarla, suponiendo que sea correcta. :-)
RationalGeek
¿Existe alguna razón específica para no tener la propiedad de navegación? ¿Hacer que la propiedad de navegación sea privada funcione para usted? No sería visible fuera de la entidad, pero le agradaría a EF. (Tenga en cuenta que no lo he intentado, pero creo que debería funcionar; eche un vistazo a esta publicación sobre el mapeo de propiedades privadas romiller.com/2012/10/01/… )
Pawel
6
Bueno, no lo quiero porque no lo necesito. No me gusta tener que poner cosas adicionales innecesarias en un diseño solo para satisfacer los requisitos de un marco. ¿Me mataría poner el accesorio de navegación? No. De hecho, eso es lo que he hecho por el momento.
RationalGeek
Siempre necesita la propiedad de navegación en al menos un lado para construir una relación. Para obtener más información, stackoverflow.com/a/7105288/105445
Wahid Bitar

Respuestas:

63

Con EF Code First Fluent API es imposible. Siempre necesita al menos una propiedad de navegación para crear una restricción de clave externa en la base de datos.

Si está utilizando Code First Migrations, tiene la opción de agregar una nueva migración basada en código en la consola del administrador de paquetes ( add-migration SomeNewSchemaName). Si cambió algo con su modelo o mapeo, se agregará una nueva migración. Si no cambió nada, fuerce una nueva migración utilizando add-migration -IgnoreChanges SomeNewSchemaName. La migración solo contendrá métodos vacíos Upy Downen este caso.

Luego puede modificar el Upmétodo agregando lo siguiente:

public override void Up()
{
    // other stuff...

    AddForeignKey("ChildTableName", "ParentId", "ParentTableName", "Id",
        cascadeDelete: true); // or false
    CreateIndex("ChildTableName", "ParentId"); // if you want an index
}

Al ejecutar esta migración ( update-databaseen la consola de administración de paquetes) se ejecutará una declaración SQL similar a esta (para SQL Server):

ALTER TABLE [ChildTableName] ADD CONSTRAINT [FK_SomeName]
FOREIGN KEY ([ParentId]) REFERENCES [ParentTableName] ([Id])

CREATE INDEX [IX_SomeName] ON [ChildTableName] ([ParentId])

Alternativamente, sin migraciones, puede ejecutar un comando SQL puro usando

context.Database.ExecuteSqlCommand(sql);

donde contextes una instancia de su clase de contexto derivada y sqles solo el comando SQL anterior como cadena.

Tenga en cuenta que con todo esto EF no tiene ni idea de que ParentIdes una clave externa que describe una relación. EF lo considerará solo como una propiedad escalar ordinaria. De alguna manera, todo lo anterior es solo una forma más complicada y lenta en comparación con simplemente abrir una herramienta de administración de SQL y agregar la restricción a mano.

Slauma
fuente
2
La simplificación proviene de la automatización: no tengo acceso a otros entornos en los que se implementa mi código. Poder realizar estos cambios en el código es bueno para mí. Pero me gusta el snark :)
pomeroy
Creo que también acaba de establecer un atributo en la entidad, así que en la identificación principal, simplemente agregue [ForeignKey("ParentTableName")]. Eso vinculará la propiedad a cualquier clave que esté en la tabla principal. Sin embargo, ahora tiene un nombre de tabla codificado.
Triynko
2
Claramente no es imposible, vea el otro comentario a continuación ¿Por qué esto está marcado como respuesta correcta?
Igor Be
111

Aunque esta publicación es para Entity Frameworkno Entity Framework Core, podría ser útil para alguien que quiera lograr lo mismo usando Entity Framework Core (estoy usando V1.1.2).

No necesito propiedades de navegación (a pesar de que es Niza), porque estoy practicando DDD y quiero Parenty Childque hay dos raíces agregados separados. Quiero que puedan comunicarse entre sí a través de una clave externa, no a través de Entity Frameworkpropiedades de navegación específicas de la infraestructura .

Todo lo que tiene que hacer es configurar la relación en un lado usando HasOney WithManysin especificar las propiedades de navegación (después de todo, no están ahí).

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

    protected override void OnModelCreating(ModelBuilder builder)
    {
        ......

        builder.Entity<Parent>(b => {
            b.HasKey(p => p.Id);
            b.ToTable("Parent");
        });

        builder.Entity<Child>(b => {
            b.HasKey(c => c.Id);
            b.Property(c => c.ParentId).IsRequired();

            // Without referencing navigation properties (they're not there anyway)
            b.HasOne<Parent>()    // <---
                .WithMany()       // <---
                .HasForeignKey(c => c.ParentId);

            // Just for comparison, with navigation properties defined,
            // (let's say you call it Parent in the Child class and Children
            // collection in Parent class), you might have to configure them 
            // like:
            // b.HasOne(c => c.Parent)
            //     .WithMany(p => p.Children)
            //     .HasForeignKey(c => c.ParentId);

            b.ToTable("Child");
        });

        ......
    }
}

También estoy dando ejemplos sobre cómo configurar las propiedades de la entidad, pero el más importante aquí es HasOne<>, WithMany()y HasForeignKey().

Espero eso ayude.

David Liang
fuente
8
Esta es la respuesta correcta para EF Core, y para aquellos que practican DDD, esta es una OBLIGACIÓN.
Thiago Silva
1
No tengo claro qué ha cambiado para eliminar la propiedad de navegación. ¿Puedes por favor aclarar?
andrew.rockwell
3
@ andrew.rockwell: Vea HasOne<Parent>()y .WithMany()en la configuración secundaria. No hacen referencia a las propiedades de navegación en absoluto, ya que de todos modos no hay propiedades de navegación definidas. Intentaré aclararlo con mis actualizaciones.
David Liang
Increíble. Gracias @DavidLiang
andrew.rockwell
2
@ mirind4 no está relacionado con la muestra de código específico en el OP, si está mapeando diferentes entidades raíz agregadas a sus respectivas tablas de base de datos, según DDD, esas entidades raíz solo deben hacer referencia entre sí a través de su identidad y no deben contener una referencia completa a la otra entidad AR. De hecho, en DDD también es común hacer que la identidad de las entidades sea un objeto de valor en lugar de usar accesorios con tipos primitivos como int / log / guid (especialmente entidades AR), evitando la obsesión primitiva y también permitiendo que diferentes AR hagan referencia a entidades a través del valor. tipo de ID de objeto. HTH
Thiago Silva
21

Pequeña sugerencia para aquellos que desean usar DataAnotations y no desean exponer la propiedad de navegación: use protected

public class Parent
{
    public int Id { get; set; }
}
public class Child
{
    public int Id { get; set; }
    public int ParentId { get; set; }

    protected virtual Parent Parent { get; set; }
}

Eso es todo: se creará la clave externa con cascade:trueafter Add-Migration.

diezbits
fuente
1
Puede incluso en privado
Marc Wittke
2
Al crear Child, ¿tendrías que asignar Parento simplemente ParentId?
George Mauer
5
Las virtualpropiedades de @MarcWittke no pueden ser private.
LINQ
@GeorgeMauer: ¡es una buena pregunta! Técnicamente, cualquiera de los dos funcionaría, pero también se convierte en un problema cuando tiene códigos inconsistentes como este, porque los desarrolladores (especialmente los recién llegados) no están seguros de qué pasar.
David Liang
14

En el caso de EF Core , no es necesario proporcionar una propiedad de navegación. Simplemente puede proporcionar una clave externa en un lado de la relación. Un ejemplo simple con Fluent API:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;

namespace EFModeling.Configuring.FluentAPI.Samples.Relationships.NoNavigation
{
    class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
             modelBuilder.Entity<Post>()
                .HasOne<Blog>()
                .WithMany()
                .HasForeignKey(p => p.BlogId);
        }
    }

    public class Blog
    {
         public int BlogId { get; set; }
         public string Url { get; set; }
    }

    public class Post
    {
         public int PostId { get; set; }
         public string Title { get; set; }
         public string Content { get; set; }

        public int BlogId { get; set; }
    }
}
Dragón de Jonatán
fuente
2

Estoy usando .Net Core 3.1, EntityFramework 3.1.3. He estado buscando y la solución que se me ocurrió fue usar la versión genérica de HasForeginKey<DependantEntityType>(e => e.ForeginKeyProperty). puedes crear una relación uno a uno así:

builder.entity<Parent>()
.HasOne<Child>()
.WithOne<>()
.HasForeginKey<Child>(c => c.ParentId);

builder.entity<Child>()
    .Property(c => c.ParentId).IsRequired();

Espero que esto ayude o al menos proporcione algunas otras ideas sobre cómo usar el HasForeginKeymétodo.

Tejedor Radded
fuente
0

Mi razón para no usar las propiedades de navegación son las dependencias de clase. Separé mis modelos en pocos ensamblajes, que se pueden usar o no en diferentes proyectos en cualquier combinación. Entonces, si tengo una entidad que tiene propiedad de nagivación para clasificar de otro ensamblado, necesito hacer referencia a ese ensamblado, que quiero evitar (o cualquier proyecto que use parte de ese modelo de datos completo lo llevará todo).

Y tengo una aplicación de migración separada, que se usa para migraciones (yo uso automigraciones) y creación de base de datos inicial. Este proyecto hace referencia a todo por razones obvias.

La solución es de estilo C:

  • "copiar" el archivo con la clase de destino al proyecto de migración a través del enlace (arrastrar y soltar con altclave en VS)
  • deshabilitar la propiedad nagivation (y el atributo FK) a través de #if _MIGRATION
  • configure esa definición de preprocesador en la aplicación de migración y no la configure en el proyecto modelo, por lo que no hará referencia a nada (no haga referencia al ensamblado con la Contactclase en el ejemplo).

Muestra:

    public int? ContactId { get; set; }

#if _MIGRATION
    [ForeignKey(nameof(ContactId))]
    public Contact Contact { get; set; }
#endif

Por supuesto, debería deshabilitar de la misma manera using directiva de la y cambiar el espacio de nombres.

Después de eso, todos los consumidores pueden usar esa propiedad como un campo de base de datos habitual (y no hacer referencia a ensamblajes adicionales si no son necesarios), pero el servidor de base de datos sabrá que es FK y puede usar la conexión en cascada. Solución muy sucia. Pero funciona.

serpiente de cascabel
fuente