EF-Code Erster Fremdschlüssel ohne Navigationseigenschaft

86

Angenommen, ich habe die folgenden Entitäten:

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

Wie lautet die erste fließende API-Syntax des Codes, um zu erzwingen, dass ParentId in der Datenbank mit einer Fremdschlüsseleinschränkung für die Parents-Tabelle erstellt wird, ohne dass eine Navigationseigenschaft erforderlich ist ?

Ich weiß, dass ich Folgendes tun kann, wenn ich eine Navigationseigenschaft Parent to Child hinzufüge:

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

Aber ich möchte die Navigationseigenschaft in diesem speziellen Fall nicht.

RationalGeek
quelle
1
Ich glaube nicht, dass dies tatsächlich nur mit EF möglich ist. Wahrscheinlich müssen Sie bei einer manuellen Migration Roh-SQL verwenden, um es einzurichten
nicht geliebt am
@ LukeMcGregor das habe ich befürchtet. Wenn Sie eine Antwort geben, werde ich diese gerne akzeptieren, vorausgesetzt, sie ist korrekt. :-)
RationalGeek
Gibt es einen bestimmten Grund dafür, dass die Navigationseigenschaft nicht vorhanden ist? Würde die Navigationseigenschaft für Sie privat machen - wäre außerhalb der Entität nicht sichtbar, würde aber EF gefallen. (Hinweis Ich habe dies nicht versucht, aber ich denke, dies sollte funktionieren - werfen Sie einen Blick auf diesen Beitrag über die Zuordnung von privaten Eigenschaften romiller.com/2012/10/01/… )
Pawel
5
Nun, ich will es nicht, weil ich es nicht brauche. Ich mag es nicht, zusätzliche unnötige Dinge in ein Design einfügen zu müssen, um die Anforderungen eines Frameworks zu erfüllen. Würde es mich umbringen, die Navigationsstütze einzusetzen? Tatsächlich habe ich das vorerst getan.
RationalGeek
Sie benötigen immer eine Navigationseigenschaft auf mindestens einer Seite, um eine Beziehung aufzubauen. Für weitere Informationen stackoverflow.com/a/7105288/105445
Wahid Bitar

Antworten:

60

Mit EF Code First Fluent API ist dies nicht möglich. Sie benötigen immer mindestens eine Navigationseigenschaft, um eine Fremdschlüsseleinschränkung in der Datenbank zu erstellen.

Wenn Sie Code First Migrations verwenden, können Sie der Paketmanagerkonsole ( add-migration SomeNewSchemaName) eine neue codebasierte Migration hinzufügen . Wenn Sie etwas an Ihrem Modell geändert oder zugeordnet haben, wird eine neue Migration hinzugefügt. Wenn Sie nichts geändert haben, erzwingen Sie eine neue Migration mit add-migration -IgnoreChanges SomeNewSchemaName. Die Migration enthält in diesem Fall nur leere Upund DownMethoden.

Anschließend können Sie die UpMethode ändern , indem Sie Folgendes hinzufügen:

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

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

Wenn Sie diese Migration ( update-databaseauf der Paketverwaltungskonsole) ausführen, wird eine ähnliche SQL-Anweisung ausgeführt (für SQL Server):

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

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

Alternativ können Sie ohne Migrationen einfach einen reinen SQL-Befehl mit ausführen

context.Database.ExecuteSqlCommand(sql);

Dabei contexthandelt es sich um eine Instanz Ihrer abgeleiteten Kontextklasse und sqlnur um den obigen SQL-Befehl als Zeichenfolge.

Beachten Sie, dass EF bei alledem keine Ahnung hat, dass ParentIdes sich um einen Fremdschlüssel handelt, der eine Beziehung beschreibt. EF betrachtet es nur als gewöhnliche Skalareigenschaft. Irgendwie ist all das nur komplizierter und langsamer als das Öffnen eines SQL-Verwaltungstools und das manuelle Hinzufügen der Einschränkung.

Slauma
quelle
2
Die Vereinfachung ergibt sich aus der Automatisierung: Ich habe keinen Zugriff auf andere Umgebungen, in denen mein Code bereitgestellt wird. Es ist schön für mich, diese Änderungen im Code vornehmen zu können. Aber ich mag den Snark :)
Pomeroy
Ich denke, Sie haben auch nur ein Attribut für die Entität festgelegt, also fügen Sie einfach die übergeordnete ID hinzu [ForeignKey("ParentTableName")]. Dadurch wird die Eigenschaft mit dem Schlüssel verknüpft, der sich in der übergeordneten Tabelle befindet. Jetzt haben Sie jedoch einen fest codierten Tabellennamen.
Triynko
1
Es ist eindeutig nicht unmöglich, siehe den anderen Kommentar unten Warum wird dies überhaupt als richtige Antwort markiert
Igor Be
100

Obwohl dieser Beitrag Entity Frameworknicht Entity Framework Coredazu gedacht ist, kann er für jemanden nützlich sein, der dasselbe mit Entity Framework Core erreichen möchte (ich verwende V1.1.2).

Ich habe keine Navigationseigenschaften müssen (obwohl sie sind nett) , weil ich DDD übe und ich möchte Parentund Childzwei separate Aggregate Wurzeln zu sein. Ich möchte, dass sie über Fremdschlüssel und nicht über infrastrukturspezifische Entity FrameworkNavigationseigenschaften miteinander kommunizieren können .

Alles, was Sie tun müssen, ist, die Beziehung auf einer Seite mit HasOneund WithManyohne Angabe der Navigationseigenschaften zu konfigurieren (sie sind schließlich nicht vorhanden).

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

        ......
    }
}

Ich gebe auch Beispiele zum Konfigurieren von Entitätseigenschaften, aber das wichtigste hier ist HasOne<>, WithMany()und HasForeignKey().

Ich hoffe es hilft.

David Liang
quelle
7
Dies ist die richtige Antwort für EF Core, und für diejenigen, die DDD praktizieren, ist dies ein MUSS.
Thiago Silva
1
Mir ist nicht klar, was sich geändert hat, um die Navigationseigenschaft zu entfernen. Können Sie bitte klarstellen?
Andrew.rockwell
2
@ andrew.rockwell: Siehe HasOne<Parent>()und .WithMany()unter der untergeordneten Konfiguration. Sie verweisen überhaupt nicht auf die Navigationseigenschaften, da ohnehin keine Navigationseigenschaften definiert sind. Ich werde versuchen, es mit meinen Updates klarer zu machen.
David Liang
Genial. Vielen Dank @DavidLiang
andrew.rockwell
2
@ mirind4, unabhängig von dem spezifischen Codebeispiel im OP. Wenn Sie laut DDD verschiedene aggregierte Stammentitäten ihren jeweiligen DB-Tabellen zuordnen, sollten diese Stammentitäten nur über ihre Identität aufeinander verweisen und keine vollständige Referenz enthalten an die andere AR-Entität. Tatsächlich ist es in DDD auch üblich, die Identität von Entitäten zu einem Wertobjekt zu machen, anstatt Requisiten mit primitiven Typen wie int / log / guid (insbesondere AR-Entitäten) zu verwenden, um primitive Besessenheit zu vermeiden und verschiedenen ARs zu ermöglichen, Entitäten über den Wert zu referenzieren Objekt-ID-Typ. HTH
Thiago Silva
20

Kleiner Hinweis für diejenigen, die DataAnotations verwenden und die Navigationseigenschaft nicht verfügbar machen möchten - verwenden 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; }
}

Das war's - der Fremdschlüssel mit cascade:trueAfter Add-Migrationwird erstellt.

Tenbits
quelle
1
Es kann sogar privat sein
Marc Wittke
2
Müssen Sie beim Erstellen von Child zuweisen Parentoder nur ParentId?
George Mauer
5
@ MarcWittke- virtualEigenschaften können nicht sein private.
LINQ
@ GeorgeMauer: Das ist eine gute Frage! Technisch würde beides funktionieren, aber es wird auch zu Problemen, wenn Sie inkonsistente Codes wie diesen haben, weil Entwickler (insbesondere Neuankömmlinge) nicht sicher sind, was sie weitergeben sollen.
David Liang
11

Im Fall von EF Core müssen Sie nicht unbedingt eine Navigationseigenschaft angeben. Sie können einfach einen Fremdschlüssel auf einer Seite der Beziehung angeben. Ein einfaches Beispiel mit 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; }
    }
}
Jonatan Drache
quelle
2

Ich verwende .Net Core 3.1, EntityFramework 3.1.3. Ich habe mich umgesehen und die Lösung, die ich gefunden habe, war die Verwendung der generischen Version von HasForeginKey<DependantEntityType>(e => e.ForeginKeyProperty). Sie können eine Eins-zu-Eins-Beziehung wie folgt erstellen:

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

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

Ich hoffe, dies hilft oder liefert zumindest einige andere Ideen zur Verwendung der HasForeginKeyMethode.

Radded Weaver
quelle
0

Mein Grund dafür nicht Navigationseigenschaften verwende, sind Klassenabhängigkeiten. Ich habe meine Modelle in wenige Baugruppen unterteilt, die in verschiedenen Kombinationen in verschiedenen Projekten verwendet werden können oder nicht. Wenn ich also eine Entität habe, die über eine Nagivationseigenschaft verfügt, die von einer anderen Assembly klassifiziert werden soll, muss ich auf diese Assembly verweisen, die ich vermeiden möchte (oder jedes Projekt, das einen Teil dieses vollständigen Datenmodells verwendet, enthält alles).

Und ich habe eine separate Migrations-App, die für Migrationen (ich verwende Automigrationen) und die anfängliche DB-Erstellung verwendet wird. Dieses Projekt bezieht sich aus offensichtlichen Gründen auf alles.

Lösung ist C-Stil:

  • Datei mit Zielklasse über Link in Migrationsprojekt "kopieren" (Drag & Drop mit altSchlüssel in VS)
  • Deaktivieren Sie die Nagivationseigenschaft (und das FK-Attribut) über #if _MIGRATION
  • Legen Sie diese Präprozessordefinition in der Migrations-App fest und legen Sie sie nicht im Modellprojekt fest, damit sie auf nichts verweist ( Contactim Beispiel nicht auf Assembly mit Klasse verweisen ).

Stichprobe:

    public int? ContactId { get; set; }

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

Natürlich sollten Sie die usingDirektive auf die gleiche Weise deaktivieren und den Namespace ändern.

Danach können alle Konsumenten diese Eigenschaft wie gewohnt als DB-Feld verwenden (und nicht auf zusätzliche Assemblys verweisen, wenn sie nicht benötigt werden), aber der DB-Server weiß, dass es sich um FK handelt, und kann Kaskadierung verwenden. Sehr schmutzige Lösung. Funktioniert aber.

Klapperschlange
quelle