Klucz obcy EF Code First bez właściwości nawigacji

91

Powiedzmy, że mam następujące jednostki:

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

Jaka jest pierwsza płynna składnia interfejsu API w kodzie, która wymusza utworzenie w bazie danych ParentId z ograniczeniem klucza obcego do tabeli Parents, bez konieczności posiadania właściwości nawigacji ?

Wiem, że jeśli dodam właściwość nawigacji Parent to Child, mogę to zrobić:

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

Ale nie chcę, aby w tym konkretnym przypadku właściwość nawigacji.

RationalGeek
źródło
1
Nie sądzę, żeby to było rzeczywiście możliwe z samym EF, prawdopodobnie musisz użyć surowego SQL w ręcznej migracji, aby go skonfigurować
Nie podobało mi się
@LukeMcGregor tego się obawiałem. Jeśli uczynisz taką odpowiedź, z przyjemnością ją przyjmuję, zakładając, że jest poprawna. :-)
RationalGeek
Czy jest jakiś konkretny powód braku właściwości nawigacji? Sprawiłby, że własność nawigacji działałaby dla ciebie jako prywatna - nie byłaby widoczna poza jednostką, ale spodobałaby się EF. (Uwaga, nie próbowałem tego, ale myślę, że to powinno działać - spójrz na ten post o mapowaniu prywatnych nieruchomości romiller.com/2012/10/01/... )
Paweł
6
Cóż, nie chcę tego, ponieważ tego nie potrzebuję. Nie lubię umieszczać dodatkowych, niepotrzebnych rzeczy w projekcie, aby spełnić wymagania frameworka. Czy zabiłoby mnie umieszczenie rekwizytu nawigacyjnego? Nie. Właściwie to właśnie zrobiłem na razie.
RationalGeek
Aby zbudować relację, zawsze potrzebujesz właściwości nawigacji po co najmniej jednej stronie. Aby uzyskać więcej informacji, stackoverflow.com/a/7105288/105445
Wahid Bitar

Odpowiedzi:

63

Z EF Code First Fluent API jest to niemożliwe. Aby utworzyć ograniczenie klucza obcego w bazie danych, zawsze potrzebujesz co najmniej jednej właściwości nawigacji.

Jeśli korzystasz z migracji Code First, możesz dodać nową migrację opartą na kodzie w konsoli menedżera pakietów ( add-migration SomeNewSchemaName). Jeśli coś zmieniłeś w swoim modelu lub mapowaniu, zostanie dodana nowa migracja. Jeśli nic nie zmieniłeś, wymuś nową migrację za pomocą add-migration -IgnoreChanges SomeNewSchemaName. W tym przypadku migracja będzie zawierać tylko puste Upi Downmetody.

Następnie możesz zmodyfikować Upmetodę, dodając do niej następującą:

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

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

Uruchomienie tej migracji ( update-databasew konsoli zarządzania pakietami) spowoduje uruchomienie instrukcji SQL podobnej do poniższej (dla SQL Server):

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

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

Alternatywnie, bez migracji, możesz po prostu uruchomić czyste polecenie SQL za pomocą

context.Database.ExecuteSqlCommand(sql);

gdzie contextjest instancją Twojej pochodnej klasy kontekstu i sqljest po prostu powyższym poleceniem SQL jako ciągiem znaków.

Należy pamiętać, że z tym wszystkim EF nie ma pojęcia, że ParentIdjest to klucz obcy opisujący relację. EF uzna to tylko za zwykłą właściwość skalarną. W jakiś sposób wszystko powyższe jest tylko bardziej skomplikowanym i wolniejszym sposobem w porównaniu do zwykłego otwarcia narzędzia do zarządzania SQL i ręcznego dodania ograniczenia.

Slauma
źródło
2
Uproszczenie wynika z automatyzacji: nie mam dostępu do innych środowisk, w których jest wdrażany mój kod. Możliwość dokonywania tych zmian w kodzie jest dla mnie przyjemna. Ale ja lubię snarka :)
pomeroy
Myślę, że po prostu ustawiasz atrybut na encji, więc na id rodzica, po prostu dodaj [ForeignKey("ParentTableName")]. To połączy właściwość z dowolnym kluczem w tabeli nadrzędnej. Teraz masz jednak zakodowaną na stałe nazwę tabeli.
Triynko,
2
To oczywiście nie jest niemożliwe, zobacz inny komentarz poniżej. Dlaczego jest to nawet oznaczone jako poprawna odpowiedź
Igor Be
111

Chociaż ten post jest dla Entity Frameworknie Entity Framework Core, może być przydatny dla kogoś, kto chce osiągnąć to samo za pomocą Entity Framework Core (używam wersji 1.1.2).

Nie muszę właściwości nawigacyjnych (chociaż są one ładne), ponieważ ćwiczę DDD i chcę Parenti Childbyć dwa odrębne korzenie kruszywa. Chcę, aby mogli rozmawiać ze sobą za pomocą klucza obcego, a nie przez Entity Frameworkwłaściwości nawigacji specyficzne dla infrastruktury .

Wszystko, co musisz zrobić, to skonfigurować relację po jednej stronie, używając HasOnei WithManybez określania właściwości nawigacji (w końcu ich tam nie ma).

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

        ......
    }
}

Daję z przykładów, w jaki sposób skonfigurować właściwości podmiotu, jak również, ale najważniejszy jest tutaj HasOne<>, WithMany()i HasForeignKey().

Mam nadzieję, że to pomoże.

David Liang
źródło
8
To jest poprawna odpowiedź dla EF Core, a dla osób ćwiczących DDD jest to MUSI.
Thiago Silva
1
Nie mam pewności, co się zmieniło, aby usunąć właściwość nawigacyjną. Czy możesz wyjaśnić?
andrew.rockwell
3
@ andrew.rockwell: Zobacz konfigurację podrzędną HasOne<Parent>()i .WithMany()inną. W ogóle nie odwołują się do właściwości nawigacji, ponieważ i tak nie są zdefiniowane żadne właściwości nawigacji. Postaram się, aby było to jaśniejsze dzięki moim aktualizacjom.
David Liang
Niesamowite. Dzięki @DavidLiang
andrew.rockwell
2
@ mirind4 niezwiązane z konkretnym przykładem kodu w OP, jeśli mapujesz różne zagregowane jednostki główne do ich odpowiednich tabel DB, zgodnie z DDD te główne jednostki powinny odwoływać się do siebie tylko poprzez swoją tożsamość i nie powinny zawierać pełnego odniesienia do innego podmiotu AR. W rzeczywistości w DDD jest również powszechne, że tożsamość encji staje się obiektem wartości, zamiast używać rekwizytów z typami prymitywnymi, takimi jak int / log / guid (zwłaszcza encje AR), unikając prymitywnej obsesji, a także pozwalając różnym AR na odwoływanie się do jednostek za pośrednictwem wartości typ identyfikatora obiektu. HTH
Thiago Silva
21

Mała wskazówka dla tych, którzy chcą korzystać z DataAnotations i nie chcą ujawniać właściwości Navigation Property - użyj 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; }
}

To wszystko - zostanie utworzony klucz obcy z cascade:trueafter Add-Migration.

tenbitów
źródło
1
Może nawet prywatnie
Marc Wittke
2
Tworząc Dziecko musiałbyś przypisać, Parentczy po prostu ParentId?
George Mauer
5
virtualWłaściwości @MarcWittke nie mogą być private.
LINQ
@GeorgeMauer: to dobre pytanie! Technicznie rzecz biorąc, którykolwiek z nich by działał, ale staje się to również problemem, gdy masz niespójne kody, takie jak ten, ponieważ programiści (zwłaszcza nowicjusze) nie są pewni, co przekazać.
David Liang
14

W przypadku EF Core nie musisz koniecznie podawać właściwości nawigacji. Możesz po prostu podać klucz obcy po jednej stronie relacji. Prosty przykład z 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 Dragon
źródło
2

Używam .Net Core 3.1, EntityFramework 3.1.3. Szukałem wokół, a rozwiązanie, które wymyśliłem, polegało na użyciu ogólnej wersji HasForeginKey<DependantEntityType>(e => e.ForeginKeyProperty). możesz stworzyć relację jeden do jednego w następujący sposób:

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

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

Mam nadzieję, że to pomoże lub przynajmniej zapewni kilka innych pomysłów, jak używać tej HasForeginKeymetody.

Radded Weaver
źródło
0

Powodem, dla którego nie używam właściwości nawigacji, są zależności klas. Rozdzieliłem moje modele na kilka złożeń, które mogą być używane lub nie używane w różnych projektach w dowolnych kombinacjach. Więc jeśli mam jednostkę, która ma właściwość nagivation do klasy z innego zestawu, muszę odwołać się do tego zestawu, którego chcę uniknąć (lub jakikolwiek projekt, który używa części tego kompletnego modelu danych, będzie nosił wszystko ze sobą).

Mam oddzielną aplikację do migracji, która jest używana do migracji (używam automigracji) i początkowego tworzenia bazy danych. Ten projekt odnosi się do wszystkiego z oczywistych powodów.

Rozwiązanie jest w stylu C:

  • „skopiuj” plik z klasą docelową do projektu migracji za pomocą linku (przeciągnij i upuść z altkluczem w VS)
  • wyłącz właściwość nagivation (i atrybut FK) za pośrednictwem #if _MIGRATION
  • ustawić tę definicję preprocesora w aplikacji do migracji i nie ustawiać w projekcie modelowym, więc nie będzie odwoływać się do niczego (nie odwołuj się do zestawu z Contactklasą w przykładzie).

Próba:

    public int? ContactId { get; set; }

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

Oczywiście w ten sam sposób należy wyłączyć usingdyrektywy i zmienić przestrzeń nazw.

Następnie wszyscy konsumenci mogą używać tej właściwości jako zwykłego pola bazy danych (i nie odwoływać się do dodatkowych zestawów, jeśli nie są potrzebne), ale serwer DB będzie wiedział, że jest to FK i może używać kaskadowania. Bardzo brudny roztwór. Ale działa.

grzechotnik
źródło