Codice EF Prima chiave esterna senza proprietà di navigazione


93

Diciamo che ho le seguenti entità:

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

Qual è la sintassi dell'API fluente prima del codice per imporre che ParentId viene creato nel database con un vincolo di chiave esterna alla tabella Parents, senza la necessità di avere una proprietà di navigazione ?

So che se aggiungo una proprietà di navigazione Parent a Child, posso farlo:

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

Ma non voglio la proprietà di navigazione in questo caso particolare.


1
Non penso che ciò sia effettivamente possibile con solo EF, probabilmente è necessario utilizzare un SQL grezzo in una migrazione manuale per configurarlo
Non amato il

@LukeMcGregor questo è ciò che temevo. Se dai una risposta sarò felice di accettarla, ammesso che sia corretta. :-)
RationalGeek

C'è qualche motivo specifico per non avere la proprietà di navigazione? Rendere privata la proprietà di navigazione per te - non sarebbe visibile all'esterno dell'entità, ma farebbe piacere a EF. (Nota che non l'ho provato ma penso che dovrebbe funzionare - dai un'occhiata a questo post sulla mappatura delle proprietà private romiller.com/2012/10/01/… )
Pawel

6
Beh, non lo voglio perché non ne ho bisogno. Non mi piace dover inserire cose extra non necessarie in un progetto solo per soddisfare i requisiti di un framework. Mi ucciderebbe mettere l'elica di navigazione? No. In effetti è quello che ho fatto per il momento.
RationalGeek

Hai sempre bisogno della proprietà di navigazione su almeno un lato per costruire una relazione. Per ulteriori informazioni, stackoverflow.com/a/7105288/105445
Wahid Bitar

Risposte:


63

Con EF Code First Fluent API è impossibile. È sempre necessaria almeno una proprietà di navigazione per creare un vincolo di chiave esterna nel database.

Se stai utilizzando le migrazioni Code First, hai la possibilità di aggiungere una nuova migrazione basata sul codice sulla console del gestore pacchetti ( add-migration SomeNewSchemaName). Se hai cambiato qualcosa con il tuo modello o mappato, verrà aggiunta una nuova migrazione. Se non hai modificato nulla, forza una nuova migrazione utilizzando add-migration -IgnoreChanges SomeNewSchemaName. La migrazione conterrà solo file vuoti UpeDown questo caso, la metodi .

Quindi puoi modificare il Upmetodo aggiungendovi il seguente:

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

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

L'esecuzione di questa migrazione ( update-databasesulla console di gestione dei pacchetti) eseguirà un'istruzione SQL simile a questa (per SQL Server):

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

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

In alternativa, senza migrazioni, potresti semplicemente eseguire un comando SQL puro usando

context.Database.ExecuteSqlCommand(sql);

dove contextè un'istanza della classe di contesto derivata ed sqlè solo il comando SQL precedente come stringa.

Tieni presente che con tutto questo EF non ha idea che ParentIdsia una chiave esterna che descrive una relazione. EF lo considererà solo come una proprietà scalare ordinaria. In qualche modo tutto quanto sopra è solo un modo più complicato e più lento rispetto alla semplice apertura di uno strumento di gestione SQL e all'aggiunta manuale del vincolo.


2
La semplificazione viene dall'automazione: non ho accesso ad altri ambienti in cui è distribuito il mio codice. Essere in grado di eseguire queste modifiche nel codice è bello per me. Ma mi piace lo snark :)
pomeroy

Penso che tu abbia anche impostato un attributo sull'entità, quindi sull'id genitore, aggiungi [ForeignKey("ParentTableName")]. Ciò collegherà la proprietà a qualunque sia la chiave sulla tabella padre. Ora hai un nome di tabella hardcoded però.
Triynko,

2
Chiaramente non è impossibile, vedi l'altro commento qui sotto Perché anche questo è contrassegnato come risposta corretta
Igor Be

114

Sebbene questo post Entity Frameworknon Entity Framework Coresia valido, potrebbe essere utile per qualcuno che vuole ottenere lo stesso risultato usando Entity Framework Core (sto usando V1.1.2).

Non ho bisogno di proprietà di navigazione (anche se sono bello) perché sto praticando DDD e voglio Parente Childdi essere due radici di aggregazione separati. Voglio che siano in grado di parlare tra loro tramite chiave esterna non tramite specifiche infrastruttureEntity Framework proprietà di navigazione .

Tutto quello che devi fare è configurare la relazione su un lato usando HasOnee WithManysenza specificare le proprietà di navigazione (dopotutto non ci sono).

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

        ......
    }
}

Sto fornendo esempi su come configurare anche le proprietà dell'entità, ma il più importante qui è HasOne<>, WithMany()e HasForeignKey().

Spero che sia d'aiuto.


9
Questa è la risposta corretta per EF Core e per coloro che praticano DDD, questo è un MUST.
Thiago Silva

1
Non mi è chiaro cosa sia cambiato per rimuovere la proprietà di navigazione. Puoi chiarire per favore?
andrew.rockwell

3
@ andrew.rockwell: vedi la configurazione del figlio HasOne<Parent>()e .WithMany()su quella. Non fanno affatto riferimento alle proprietà di navigazione, poiché non sono comunque definite proprietà di navigazione. Cercherò di renderlo più chiaro con i miei aggiornamenti.
David Liang

Eccezionale. Grazie @DavidLiang
andrew.rockwell

2
@ mirind4 non correlato all'esempio di codice specifico nell'OP, se stai mappando diverse entità root aggregate alle rispettive tabelle DB, secondo DDD, tali entità root dovrebbero fare riferimento l'una all'altra solo tramite la loro identità e non dovrebbero contenere un riferimento completo all'altra entità AR. In effetti, in DDD è anche comune rendere l'identità delle entità un oggetto valore invece di utilizzare oggetti di scena con tipi primitivi come int / log / guid (in particolare entità AR), evitando l'ossessione primitiva e consentendo anche a diversi AR di fare riferimento a entità tramite il valore tipo di ID oggetto. HTH
Thiago Silva

21

Piccolo suggerimento per coloro che desiderano utilizzare DataAnotations e non vogliono esporre la proprietà di navigazione: utilizzare 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; }
}

Questo è tutto: verrà creata la chiave esterna con cascade:truedopo Add-Migration.


1
Può anche da privato
Marc Wittke

2
Quando crei Child dovresti assegnare Parento semplicemente ParentId?
George Mauer

5
Le virtualproprietà di @MarcWittke non possono essere private.
LINQ

@GeorgeMauer: è una bella domanda! Tecnicamente uno dei due funzionerebbe, ma diventa anche un problema quando si hanno codici incoerenti come questo, perché gli sviluppatori (specialmente i nuovi arrivati) non sono sicuri di cosa passare.
David Liang

15

In caso di EF Core non è necessario fornire una proprietà di navigazione. Puoi semplicemente fornire una chiave esterna su un lato della relazione. Un semplice esempio 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; }
    }
}

2

Sto usando .Net Core 3.1, EntityFramework 3.1.3. Ho cercato in giro e la soluzione che ho trovato è stata quella di utilizzare la versione generica di HasForeginKey<DependantEntityType>(e => e.ForeginKeyProperty). puoi creare una relazione uno a uno in questo modo:

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

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

Spero che questo aiuti o almeno fornisca altre idee su come utilizzare il HasForeginKeymetodo.


0

La mia ragione per no utilizzo le proprietà di navigazione sono le dipendenze di classe. Ho separato i miei modelli in pochi assemblaggi, che possono essere utilizzati o meno in progetti diversi in qualsiasi combinazione. Quindi, se ho un'entità che ha la proprietà di nagivazione in classe da un altro assembly, devo fare riferimento a quell'assembly, che voglio evitare (o qualsiasi progetto che utilizza parte di quel modello di dati completo porterà tutto con esso).

E ho un'app di migrazione separata, che viene utilizzata per le migrazioni (io uso le migrazioni automatiche) e la creazione iniziale di DB. Questo progetto fa riferimento a tutto per ovvie ragioni.

La soluzione è in stile C:

  • "copia" file con classe di destinazione nel progetto di migrazione tramite link (drag-n-drop con altchiave in VS)
  • disabilitare la proprietà di nagivazione (e l'attributo FK) tramite #if _MIGRATION
  • imposta la definizione del preprocessore nell'app di migrazione e non impostare nel progetto del modello, quindi non farà riferimento a nulla (non fare riferimento all'assembly con la Contactclasse nell'esempio).

Campione:

    public int? ContactId { get; set; }

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

Ovviamente dovresti allo stesso modo disabilitare la usingdirettiva e cambiare lo spazio dei nomi.

Dopodiché tutti i consumatori possono utilizzare quella proprietà come un normale campo DB (e non fare riferimento ad assembly aggiuntivi se non sono necessari), ma il server DB saprà che è FK e può utilizzare la cascata. Soluzione molto sporca. Ma funziona.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.