Udostępnij za pośrednictwem


Zmienianie kluczy obcych i nawigacji

Omówienie kluczy obcych i nawigacji

Relacje w modelu platformy Entity Framework Core (EF Core) są reprezentowane przy użyciu kluczy obcych (FKs). Klucz FK składa się z co najmniej jednej właściwości jednostki zależnej lub podrzędnej w relacji. Ta jednostka zależna/podrzędna jest skojarzona z daną jednostką główną/nadrzędną, gdy wartości właściwości klucza obcego w zależności/podrzędnej są zgodne z wartościami właściwości alternatywnego lub podstawowego klucza (PK) dla podmiotu zabezpieczeń/elementu nadrzędnego.

Klucze obce to dobry sposób przechowywania relacji w bazie danych i manipulowania nimi, ale nie są bardzo przyjazne podczas pracy z wieloma powiązanymi jednostkami w kodzie aplikacji. W związku z tym większość modeli EF Core również warstwę "nawigacji" za pośrednictwem reprezentacji FK. Nawigacje tworzą odwołania C#/.NET między wystąpieniami jednostek, które odzwierciedlają skojarzenia znalezione przez dopasowanie wartości klucza obcego do wartości klucza podstawowego lub alternatywnego.

Nawigacje mogą być używane po obu stronach relacji, tylko po jednej stronie lub w ogóle, pozostawiając tylko właściwość FK. Właściwość FK może być ukryta przez utworzenie jej właściwości w tle. Zobacz Relacje , aby uzyskać więcej informacji na temat modelowania relacji.

Napiwek

W tym dokumencie przyjęto założenie, że stany jednostki i podstawy śledzenia zmian platformy EF Core są zrozumiałe. Aby uzyskać więcej informacji na temat tych tematów, zobacz Change Tracking in EF Core (Śledzenie zmian w programie EF Core ).

Napiwek

Możesz uruchomić i debugować cały kod podany w tym dokumencie, pobierając przykładowy kod z serwisu GitHub.

Przykładowy model

Poniższy model zawiera cztery typy jednostek z relacjami między nimi. Komentarze w kodzie wskazują, które właściwości są kluczami obcymi, kluczami podstawowymi i nawigacjami.

public class Blog
{
    public int Id { get; set; } // Primary key
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Collection navigation
    public BlogAssets Assets { get; set; } // Reference navigation
}

public class BlogAssets
{
    public int Id { get; set; } // Primary key
    public byte[] Banner { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation
}

public class Post
{
    public int Id { get; set; } // Primary key
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
}

public class Tag
{
    public int Id { get; set; } // Primary key
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
}

Trzy relacje w tym modelu to:

  • Każdy blog może zawierać wiele wpisów (jeden do wielu):
    • Blog jest podmiotem zabezpieczeń/elementem nadrzędnym.
    • Post jest zależny/podrzędny. Zawiera właściwość Post.BlogIdFK , której wartość musi być zgodna z wartością Blog.Id PK powiązanego bloga.
    • Post.Blog to nawigacja referencyjna z wpisu do skojarzonego bloga. Post.Blog to odwrotna nawigacja dla elementu Blog.Posts.
    • Blog.Posts to nawigacja kolekcji z bloga do wszystkich skojarzonych wpisów. Blog.Posts to odwrotna nawigacja dla elementu Post.Blog.
  • Każdy blog może mieć jeden zasób (jeden do jednego):
    • Blog jest podmiotem zabezpieczeń/elementem nadrzędnym.
    • BlogAssets jest zależny/podrzędny. Zawiera właściwość BlogAssets.BlogIdFK , której wartość musi być zgodna z wartością Blog.Id PK powiązanego bloga.
    • BlogAssets.Blog to nawigacja referencyjna z zasobów do skojarzonego bloga. BlogAssets.Blog to odwrotna nawigacja dla elementu Blog.Assets.
    • Blog.Assets to nawigacja referencyjna z bloga do skojarzonych zasobów. Blog.Assets to odwrotna nawigacja dla elementu BlogAssets.Blog.
  • Każdy wpis może zawierać wiele tagów, a każdy tag może zawierać wiele wpisów (wiele do wielu):
    • Relacje wiele-do-wielu to kolejna warstwa dwóch relacji jeden do wielu. Relacje wiele-do-wielu zostały omówione w dalszej części tego dokumentu.
    • Post.Tags to nawigacja kolekcji z wpisu do wszystkich skojarzonych tagów. Post.Tags to odwrotna nawigacja dla elementu Tag.Posts.
    • Tag.Posts to nawigacja kolekcji z tagu do wszystkich skojarzonych wpisów. Tag.Posts to odwrotna nawigacja dla elementu Post.Tags.

Zobacz Relacje , aby uzyskać więcej informacji na temat sposobu modelowania i konfigurowania relacji.

Poprawka relacji

Program EF Core utrzymuje nawigację zgodnie z wartościami klucza obcego i na odwrót. Oznacza to, że jeśli wartość klucza obcego zmieni się tak, że teraz odwołuje się do innej jednostki głównej/nadrzędnej, nawigacja zostanie zaktualizowana, aby odzwierciedlić tę zmianę. Podobnie, jeśli nawigacja zostanie zmieniona, wartości klucza obcego zaangażowanych jednostek zostaną zaktualizowane, aby odzwierciedlić tę zmianę. Jest to nazywane "fixup relacji".

Poprawka według zapytania

Poprawka najpierw występuje, gdy jednostki są odpytywane z bazy danych. Baza danych ma tylko wartości klucza obcego, więc gdy program EF Core tworzy wystąpienie jednostki z bazy danych, używa wartości klucza obcego do ustawiania nawigacji odwołań i dodawania jednostek do nawigacji kolekcji zgodnie z potrzebami. Rozważmy na przykład zapytanie dotyczące blogów i skojarzonych z nim wpisów i zasobów:

using var context = new BlogsContext();

var blogs = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToList();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Dla każdego bloga platforma EF Core najpierw utworzy Blog wystąpienie. Następnie, gdy każdy wpis jest ładowany z bazy danych, jego Post.Blog nawigacja referencyjna jest ustawiona tak, aby wskazywała skojarzony blog. Podobnie wpis jest dodawany do Blog.Posts nawigacji kolekcji. To samo dzieje się z elementem BlogAssets, z wyjątkiem w tym przypadku oba nawigacje są odwołaniami. Nawigacja Blog.Assets jest ustawiona tak, aby wskazywała wystąpienie zasobów, a nawigacja BlogAsserts.Blog jest ustawiona tak, aby wskazywała wystąpienie bloga.

Patrząc na widok debugowania monitora zmian po tym zapytaniu przedstawiono dwa blogi, z których każdy ma jeden zasób i dwa śledzone wpisy:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Widok debugowania zawiera zarówno wartości klucza, jak i nawigację. Nawigacje są wyświetlane przy użyciu wartości klucza podstawowego powiązanych jednostek. Na przykład w danych wyjściowych powyżej wskazuje, Posts: [{Id: 1}, {Id: 2}] że Blog.Posts nawigacja kolekcji zawiera dwa powiązane wpisy z kluczami podstawowymi odpowiednio 1 i 2. Podobnie dla każdego wpisu skojarzonego z pierwszym blogiem wiersz wskazuje, Blog: {Id: 1} że Post.Blog nawigacja odwołuje się do bloga z kluczem podstawowym 1.

Poprawka dotycząca lokalnie śledzonych jednostek

Poprawka relacji występuje również między jednostkami zwracanym z zapytania śledzenia i jednostkami już śledzonym przez element DbContext. Rozważ na przykład wykonywanie trzech oddzielnych zapytań dotyczących blogów, wpisów i zasobów:

using var context = new BlogsContext();

var blogs = context.Blogs.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var assets = context.Assets.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var posts = context.Posts.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Patrząc ponownie na widoki debugowania, po pierwszym zapytaniu śledzone są tylko dwa blogi:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: []

Nawigacje Blog.Assets referencyjne mają wartość null, a Blog.Posts nawigacje kolekcji są puste, ponieważ żadne skojarzone jednostki nie są obecnie śledzone przez kontekst.

Po drugim zapytaniu Blogs.Assets nawigacje referencyjne zostały naprawione w celu wskazania nowo śledzonych BlogAsset wystąpień. BlogAssets.Blog Podobnie nawigacje referencyjne są ustawione tak, aby wskazywały odpowiednie już śledzone Blog wystąpienie.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: []
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}

Na koniec po trzecim zapytaniu Blog.Posts nawigacja kolekcji zawiera teraz wszystkie powiązane wpisy, a Post.Blog odwołania wskazują na odpowiednie Blog wystąpienie:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Jest to taki sam stan końcowy, jak w przypadku oryginalnego pojedynczego zapytania, ponieważ program EF Core naprawił nawigacje jako jednostki, nawet w przypadku pochodzących z wielu różnych zapytań.

Uwaga

Poprawka nigdy nie powoduje zwracania większej ilości danych z bazy danych. Łączy tylko jednostki, które są już zwracane przez zapytanie lub już śledzone przez dbContext. Zobacz Rozpoznawanie tożsamości w programie EF Core , aby uzyskać informacje na temat obsługi duplikatów podczas serializacji jednostek.

Zmienianie relacji przy użyciu nawigacji

Najprostszym sposobem zmiany relacji między dwiema jednostkami jest manipulowanie nawigacją, pozostawiając program EF Core w celu odpowiedniego naprawienia odwrotnej nawigacji i wartości FK. W tym celu możesz wykonać jedną z następujących czynności:

  • Dodawanie lub usuwanie jednostki z nawigacji kolekcji.
  • Zmiana nawigacji odwołania w taki sposób, aby wskazywała inną jednostkę lub ustawiała ją na wartość null.

Dodawanie lub usuwanie z nawigacji kolekcji

Na przykład przenieśmy jeden z wpisów z bloga programu Visual Studio do bloga platformy .NET. Wymaga to najpierw załadowania blogów i wpisów, a następnie przeniesienia wpisu z kolekcji nawigacji w jednym blogu do kolekcji nawigacji w drugim blogu:

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");
var vsBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == "Visual Studio Blog");

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Napiwek

W tym miejscu jest wymagane wywołanie ChangeTracker.DetectChanges() , ponieważ uzyskiwanie dostępu do widoku debugowania nie powoduje automatycznego wykrywania zmian.

Jest to widok debugowania wydrukowany po uruchomieniu powyższego kodu:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: [{Id: 4}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Nawigacja Blog.Posts w blogu platformy .NET zawiera teraz trzy wpisy (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Podobnie nawigacja Blog.Posts w blogu programu Visual Studio zawiera tylko jeden wpis (Posts: [{Id: 4}]). Jest to oczekiwane, ponieważ kod jawnie zmienił te kolekcje.

Co ciekawe, mimo że kod nie zmienił Post.Blog jawnie nawigacji, został naprawiony tak, aby wskazywał blog programu Visual Studio (Blog: {Id: 1}). Ponadto wartość klucza obcego została zaktualizowana w Post.BlogId celu dopasowania wartości klucza podstawowego bloga platformy .NET. Ta zmiana wartości klucza FK w poleceniu utrwalonej w bazie danych po wywołaniu funkcji SaveChanges:

-- Executed DbCommand (0ms) [Parameters=[@p1='3' (DbType = String), @p0='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

Zmienianie nawigacji odwołań

W poprzednim przykładzie wpis został przeniesiony z jednego bloga do innego, manipulując nawigacją po kolekcjach wpisów w każdym blogu. Tę samą czynność można osiągnąć, zmieniając nawigację referencyjną Post.Blog tak, aby wskazywała nowy blog. Przykład:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.Blog = dotNetBlog;

Widok debugowania po tej zmianie jest dokładnie taki sam jak w poprzednim przykładzie. Dzieje się tak, ponieważ program EF Core wykrył zmianę nawigacji referencyjnej, a następnie naprawił nawigacje kolekcji i wartość FK w celu dopasowania.

Zmienianie relacji przy użyciu wartości klucza obcego

W poprzedniej sekcji relacje były manipulowane przez nawigacje pozostawiając wartości kluczy obcych do automatycznego aktualizowania. Jest to zalecany sposób manipulowania relacjami w programie EF Core. Można jednak bezpośrednio manipulować wartościami FK. Na przykład możemy przenieść wpis z jednego bloga na inny, zmieniając wartość klucza obcego Post.BlogId :

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.BlogId = dotNetBlog.Id;

Zwróć uwagę, że jest to bardzo podobne do zmiany nawigacji referencyjnej, jak pokazano w poprzednim przykładzie.

Widok debugowania po tej zmianie jest dokładnie taki sam jak w przypadku poprzednich dwóch przykładów. Jest to spowodowane tym, że program EF Core wykrył zmianę wartości klucza FK, a następnie naprawił nawigację zarówno referencyjną, jak i nawigację kolekcji w taki sposób, aby była zgodna.

Napiwek

Nie zapisuj kodu, aby manipulować wszystkimi nawigacjami i wartościami FK za każdym razem, gdy zmienia się relacja. Taki kod jest bardziej skomplikowany i musi zapewnić spójne zmiany kluczy obcych i nawigacji w każdym przypadku. Jeśli to możliwe, wystarczy manipulować pojedynczą nawigacją, a może obie nawigacje. W razie potrzeby wystarczy manipulować wartościami FK. Unikaj manipulowania zarówno nawigacjami, jak i wartościami FK.

Poprawka dotycząca dodanych lub usuniętych jednostek

Dodawanie do nawigacji kolekcji

Program EF Core wykonuje następujące akcje, gdy wykryje , że nowa jednostka zależna/podrzędna została dodana do nawigacji kolekcji:

  • Jeśli jednostka nie jest śledzona, jest śledzona. (Jednostka będzie zwykle w Added stanie . Jeśli jednak typ jednostki jest skonfigurowany do używania wygenerowanych kluczy, a wartość klucza podstawowego Unchanged jest ustawiona, jednostka jest śledzona w stanie.
  • Jeśli jednostka jest skojarzona z innym podmiotem zabezpieczeń/elementem nadrzędnym, ta relacja jest zerwana.
  • Jednostka staje się skojarzona z jednostką/elementem nadrzędnym, który jest właścicielem nawigacji kolekcji.
  • Nawigacje i wartości kluczy obcych są naprawione dla wszystkich zaangażowanych jednostek.

Na podstawie tego widać, że aby przenieść wpis z jednego bloga do innego, nie musimy go usuwać ze starej nawigacji kolekcji przed dodaniem go do nowego. W związku z tym kod z powyższego przykładu można zmienić z:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

Do:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post);

Program EF Core widzi, że wpis został dodany do nowego bloga i automatycznie usuwa go z kolekcji w pierwszym blogu.

Usuwanie z nawigacji kolekcji

Usunięcie jednostki zależnej/podrzędnej z nawigacji kolekcji podmiotu zabezpieczeń/elementu nadrzędnego powoduje zerwanie relacji z tym podmiotem zabezpieczeń/elementem nadrzędnym. To, co się stanie dalej, zależy od tego, czy relacja jest opcjonalna, czy wymagana.

Relacje opcjonalne

Domyślnie w przypadku relacji opcjonalnych wartość klucza obcego jest ustawiona na wartość null. Oznacza to, że element zależny/podrzędny nie jest już skojarzony z żadnym podmiotem zabezpieczeń/elementem nadrzędnym. Na przykład załadujmy blog i wpisy, a następnie usuńmy jeden z wpisów z Blog.Posts nawigacji kolekcji:

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

Patrząc na widok debugowania śledzenia zmian po tej zmianie pokazuje, że:

  • Klucz Post.BlogId FK został ustawiony na wartość null (BlogId: <null> FK Modified Originally 1)
  • Nawigacja referencyjna Post.Blog została ustawiona na wartość null (Blog: <null>)
  • Wpis został usunięty z Blog.Posts nawigacji kolekcji (Posts: [{Id: 1}])
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

Zwróć uwagę, że wpis nie jest oznaczony jako Deleted. Jest ona oznaczona jako Modified tak, aby wartość FK w bazie danych została ustawiona na wartość null po wywołaniu funkcji SaveChanges.

Wymagane relacje

Ustawienie wartości FK na wartość null jest niedozwolone (i zwykle nie jest możliwe) dla wymaganych relacji. W związku z tym zerwanie wymaganej relacji oznacza, że jednostka zależna/podrzędna musi być ponownie nadrzędna do nowej jednostki/elementu nadrzędnego lub usunięta z bazy danych, gdy jest wywoływana funkcja SaveChanges, aby uniknąć naruszenia ograniczeń odwołań. Jest to nazywane "usuwaniem osieroconych" i jest domyślnym zachowaniem w programie EF Core w przypadku wymaganych relacji.

Na przykład zmieńmy relację między wpisami w blogu, aby były wymagane, a następnie uruchomimy ten sam kod, co w poprzednim przykładzie:

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

Patrząc na widok debugowania po tej zmianie, widać, że:

  • Wpis został oznaczony jako Deleted taki, że zostanie usunięty z bazy danych po wywołaniu funkcji SaveChanges.
  • Nawigacja referencyjna Post.Blog została ustawiona na wartość null (Blog: <null>).
  • Wpis został usunięty z Blog.Posts nawigacji kolekcji (Posts: [{Id: 1}]).
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

Zwróć uwagę, że pozostaje niezmieniona Post.BlogId , ponieważ dla wymaganej relacji nie można ustawić wartości null.

Wywołanie metody SaveChanges powoduje usunięcie oddzielonego wpisu:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Usuwanie czasu i ponownego rodzicielstwa

Domyślnie oznaczanie osieroconych tak Deleted szybko, jak tylko zostanie wykryta zmiana relacji. Ten proces można jednak opóźnić, dopóki funkcja SaveChanges nie zostanie wywołana. Może to być przydatne, aby uniknąć tworzenia oddzielonych jednostek, które zostały usunięte z jednego podmiotu zabezpieczeń/elementu nadrzędnego, ale zostanie ponownie nadrzędne z nowym podmiotem zabezpieczeń/elementem nadrzędnym przed wywołaniem funkcji SaveChanges. ChangeTracker.DeleteOrphansTiming służy do ustawiania tego chronometrażu. Przykład:

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Po usunięciu wpisu z pierwszej kolekcji obiekt nie jest oznaczony jako Deleted w poprzednim przykładzie. Zamiast tego program EF Core śledzi, że relacja jest zerwana , mimo że jest to wymagana relacja. (Wartość FK jest uważana za null przez program EF Core, mimo że nie może być równa null, ponieważ typ nie może mieć wartości null. Jest to nazywane "koncepcyjną wartością null".

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []

Wywołanie funkcji SaveChanges w tej chwili spowoduje usunięcie oddzielonego wpisu. Jeśli jednak tak jak w powyższym przykładzie wpis jest skojarzony z nowym blogiem przed wywołaniem funkcji SaveChanges, zostanie on odpowiednio naprawiony dla tego nowego bloga i nie jest już uważany za oddzielony:

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []

Funkcja SaveChanges wywoływana w tym momencie zaktualizuje wpis w bazie danych, a nie usunie go.

Można również wyłączyć automatyczne usuwanie osieroconych. Spowoduje to wyjątek, jeśli funkcja SaveChanges jest wywoływana podczas śledzenia sieroty. Na przykład ten kod:

var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

context.SaveChanges(); // Throws

Zgłosi ten wyjątek:

System.InvalidOperationException: skojarzenie między jednostkami "Blog" i "Post" z wartością klucza "{BlogId: 1}" zostało zerwane, ale relacja jest oznaczona jako wymagana lub jest niejawnie wymagana, ponieważ klucz obcy nie jest dopuszczalny do wartości null. Jeśli jednostka zależna/podrzędna powinna zostać usunięta po zerwaniu wymaganej relacji, skonfiguruj relację tak, aby korzystała z usuwania kaskadowego.

Usunięcie osieroconych, a także usunięcia kaskadowego, można w dowolnym momencie wymusić przez wywołanie metody ChangeTracker.CascadeChanges(). Połączenie tego z ustawieniem czasu usunięcia osieroconego w celu Never zapewnienia, że osieroce nigdy nie zostaną usunięte, chyba że program EF Core zostanie jawnie poinstruowany, aby to zrobić.

Zmienianie nawigacji referencyjnej

Zmiana nawigacji odwołania relacji jeden do wielu ma taki sam wpływ, jak zmiana nawigacji kolekcji na drugim końcu relacji. Ustawienie nawigacji referencyjnej elementu zależnego/podrzędnego na wartość null jest równoważne usunięciu jednostki z nawigacji kolekcji podmiotu zabezpieczeń/elementu nadrzędnego. Wszystkie poprawki i zmiany bazy danych są wykonywane zgodnie z opisem w poprzedniej sekcji, w tym wprowadzanie jednostki jako oddzielonej, jeśli relacja jest wymagana.

Opcjonalne relacje jeden do jednego

W przypadku relacji jeden do jednego zmiana nawigacji referencyjnej powoduje zerwanie poprzedniej relacji. W przypadku relacji opcjonalnych oznacza to, że wartość klucza FK dla wcześniej powiązanego elementu zależnego/podrzędnego jest ustawiona na wartość null. Przykład:

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Assets).Single(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Widok debugowania przed wywołaniem polecenia SaveChanges pokazuje, że nowe zasoby zastąpiły istniejące zasoby, które są teraz oznaczone jako Modified z wartością null BlogAssets.BlogId FK:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482629}
  Posts: []
BlogAssets {Id: -2147482629} Added
  Id: -2147482629 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Modified
  Id: 1 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 1
  Blog: <null>

Spowoduje to aktualizację i wstaw, gdy jest wywoływana funkcja SaveChanges:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Assets" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2=NULL, @p3='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p2, @p3);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Wymagane relacje jeden do jednego

Uruchomienie tego samego kodu, co w poprzednim przykładzie, ale tym razem z wymaganą relacją jeden do jednego pokazuje, że wcześniej skojarzony BlogAssets jest teraz oznaczony jako Deleted, ponieważ staje się oddzielony, gdy nowy BlogAssets ma miejsce:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482639}
  Posts: []
BlogAssets {Id: -2147482639} Added
  Id: -2147482639 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Deleted
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: <null>

Spowoduje to usunięcie i wstawienie po wywołaniu funkcji SaveChanges:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Assets"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1=NULL, @p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p1, @p2);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Czas oznaczania osieroconych można zmienić w taki sam sposób, jak pokazano dla nawigacji kolekcji i ma takie same efekty.

Usuwanie jednostki

Relacje opcjonalne

Gdy jednostka jest oznaczona jako Deleted, na przykład przez wywołanie DbContext.Removemetody , odwołania do usuniętej jednostki są usuwane z nawigacji innych jednostek. W przypadku relacji opcjonalnych wartości klucza FK w jednostkach zależnych są ustawione na wartość null.

Na przykład oznaczmy blog programu Visual Studio jako Deleted:

using var context = new BlogsContext();

var vsBlog = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .Single(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Patrząc na widok debugowania śledzenia zmian przed wywołaniem polecenia SaveChanges pokazuje:

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Modified
  Id: 2 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 2
  Blog: <null>
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []
Post {Id: 4} Modified
  Id: 4 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: <null>
  Tags: []

Zwróć uwagę, że:

  • Blog jest oznaczony jako Deleted.
  • Zasoby powiązane z usuniętym blogiem mają wartość null FK (BlogId: <null> FK Modified Originally 2) i nawigację po odwołaniu o wartości null (Blog: <null>)
  • Każdy wpis powiązany z usuniętym blogiem ma wartość null FK (BlogId: <null> FK Modified Originally 2) i nawigację po odwołaniu o wartości null (Blog: <null>)

Wymagane relacje

Zachowanie poprawki dla wymaganych relacji jest takie samo jak w przypadku relacji opcjonalnych, z tą różnicą, że jednostki zależne/podrzędne są oznaczone jako Deleted , ponieważ nie mogą istnieć bez podmiotu zabezpieczeń/elementu nadrzędnego i muszą zostać usunięte z bazy danych, gdy jest wywoływana funkcja SaveChanges, aby uniknąć wyjątku ograniczenia odwołania. Jest to nazywane "usuwaniem kaskadowym" i jest zachowaniem domyślnym w programie EF Core w przypadku wymaganych relacji. Na przykład uruchomienie tego samego kodu co w poprzednim przykładzie, ale z wymaganą relacją powoduje wyświetlenie następującego widoku debugowania przed wywołaniem funkcji SaveChanges:

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Deleted
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 3} Deleted
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Deleted
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Zgodnie z oczekiwaniami zależne/podrzędne są teraz oznaczone jako Deleted. Należy jednak zauważyć, że nawigacje w usuniętych jednostkach nie uległy zmianie. Może to wydawać się dziwne, ale pozwala uniknąć całkowitego rozdrobnienia usuniętego grafu jednostek przez wyczyszczenie wszystkich nawigacji. Oznacza to, że blog, zasób i wpisy nadal tworzą graf jednostek nawet po usunięciu. Znacznie ułatwia to usunięcie grafu jednostek niż w przypadku ef6, w którym wykres został rozdrobniony.

Kaskadowe usuwanie chronometrażu i ponownego elementu nadrzędnego

Domyślnie usuwanie kaskadowe odbywa się natychmiast po oznaczeniu elementu nadrzędnego/podmiotu zabezpieczeń jako Deleted. Jest to takie samo, jak w przypadku usuwania osieroconych, jak opisano wcześniej. Podobnie jak w przypadku usuwania osieroconych, ten proces można opóźnić, dopóki funkcja SaveChanges nie zostanie wywołana, a nawet całkowicie wyłączona, przez odpowiednie ustawienie ChangeTracker.CascadeDeleteTiming . Jest to przydatne w taki sam sposób, jak w przypadku usuwania osieroconych, w tym dla elementów podrzędnych/zależnych od ponownego rodzica po usunięciu podmiotu zabezpieczeń/elementu nadrzędnego.

Usunięcie kaskadowe, a także usunięcie osieroconych, można w dowolnym momencie wymusić przez wywołanie metody ChangeTracker.CascadeChanges(). Połączenie tego z ustawieniem chronometrażu usuwania kaskadowego w celu Never zapewnienia, że kaskadowe usunięcie nigdy nie nastąpi, chyba że program EF Core zostanie jawnie poinstruowany, aby to zrobić.

Napiwek

Usuwanie kaskadowe i usuwanie osieroconych jest ściśle powiązane. Oba te elementy powodują usunięcie jednostek zależnych/podrzędnych, gdy relacja z wymaganym podmiotem zabezpieczeń/elementem nadrzędnym jest zerwana. W przypadku usunięcia kaskadowego następuje to zerwanie, ponieważ jednostka/element nadrzędny jest usuwany. W przypadku oddzielonych jednostka główna/nadrzędna nadal istnieje, ale nie jest już powiązana z jednostkami zależnymi/podrzędnymi.

Relacje „wiele do wielu”

Relacje wiele-do-wielu w programie EF Core są implementowane przy użyciu jednostki sprzężenia. Każda strona relacji wiele do wielu jest powiązana z tą jednostką sprzężenia z relacją jeden do wielu. Tę jednostkę sprzężenia można jawnie zdefiniować i zamapować lub utworzyć niejawnie i ukryć. W obu przypadkach zachowanie bazowe jest takie samo. Najpierw przyjrzymy się temu bazowemu zachowaniu, aby zrozumieć, jak działa śledzenie relacji wiele-do-wielu.

Jak działają relacje wiele-do-wielu

Rozważmy ten model platformy EF Core, który tworzy relację wiele-do-wielu między wpisami i tagami przy użyciu jawnie zdefiniowanego typu jednostki sprzężenia:

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

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

Zwróć uwagę, że PostTag typ jednostki sprzężenia zawiera dwie właściwości klucza obcego. W tym modelu, aby wpis był powiązany z tagiem, musi istnieć jednostka sprzężenia PostTag, w której PostTag.PostId wartość klucza obcego jest zgodna Post.Id z wartością klucza podstawowego, a PostTag.TagId wartość klucza obcego jest zgodna z wartością klucza podstawowego Tag.Id . Przykład:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Patrząc na widok debugowania śledzenia zmian po uruchomieniu tego kodu pokazuje, że wpis i tag są powiązane z nową PostTag jednostką sprzężenia:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]

Zwróć uwagę, że nawigacje kolekcji w systemie Post i Tag zostały naprawione, podobnie jak nawigacje referencyjne w systemie PostTag. Te relacje można manipulować za pomocą nawigacji zamiast wartości FK, podobnie jak we wszystkich poprzednich przykładach. Na przykład powyższy kod można zmodyfikować, aby dodać relację, ustawiając nawigacje referencyjne w jednostce sprzężenia:

context.Add(new PostTag { Post = post, Tag = tag });

Spowoduje to dokładnie taką samą zmianę w przypadku zestawów FKs i nawigacji, jak w poprzednim przykładzie.

Pomiń nawigacje

Ręczne manipulowanie tabelą sprzężenia może być kłopotliwe. Relacje wiele-do-wielu można manipulować bezpośrednio przy użyciu specjalnych nawigacji kolekcji, które "pomijają" jednostkę sprzężenia. Na przykład do powyższego modelu można dodać dwie nawigacje pomijania; jeden od postu do tagów, a drugi z tagu do postów:

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

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

Ta relacja wiele-do-wielu wymaga następującej konfiguracji, aby upewnić się, że nawigacje pomijane i normalne nawigacje są używane dla tej samej relacji wiele-do-wielu:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne(t => t.Tag).WithMany(p => p.PostTags),
            j => j.HasOne(t => t.Post).WithMany(p => p.PostTags));
}

Zobacz Relacje , aby uzyskać więcej informacji na temat mapowania relacji wiele-do-wielu.

Pomiń nawigacje wyglądają i zachowują się jak normalne nawigacje kolekcji. Jednak sposób ich pracy z wartościami klucza obcego jest inny. Skojarzmy post z tagiem, ale tym razem użyjemy nawigacji pomiń:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Zwróć uwagę, że ten kod nie używa jednostki join. Zamiast tego po prostu dodaje jednostkę do kolekcji nawigacji w taki sam sposób, jak w przypadku relacji jeden do wielu. Wynikowy widok debugowania jest zasadniczo taki sam jak poprzednio:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]
  Posts: [{Id: 3}]

Zwróć uwagę, że wystąpienie PostTag jednostki sprzężenia zostało utworzone automatycznie z wartościami FK ustawionymi na wartości PK tagu i wpisu, które są teraz skojarzone. Wszystkie normalne nawigacje odwołań i kolekcji zostały naprawione tak, aby były zgodne z tymi wartościami FK. Ponadto, ponieważ ten model zawiera pomijanie nawigacji, zostały one również naprawione. W szczególności, mimo że dodaliśmy tag do Post.Tags nawigacji pominięcia, Tag.Posts odwrotna nawigacja pomiń po drugiej stronie tej relacji została również naprawiona tak, aby zawierał skojarzony wpis.

Warto zauważyć, że bazowe relacje wiele-do-wielu mogą być nadal manipulowane bezpośrednio, nawet gdy nawigacja pomiń została warstwowa na górze. Na przykład tag i post mogą być skojarzone tak jak wcześniej przed wprowadzeniem nawigacji pominięcia:

context.Add(new PostTag { Post = post, Tag = tag });

Możesz też użyć wartości FK:

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

Nadal spowoduje to poprawne naprawienie nawigacji pomijania, co spowoduje, że te same dane wyjściowe widoku debugowania, co w poprzednim przykładzie.

Pomiń tylko nawigację

W poprzedniej sekcji dodaliśmy pomijanie nawigacji oprócz pełnego zdefiniowania dwóch podstawowych relacji jeden do wielu. Jest to przydatne, aby zilustrować, co się dzieje z wartościami FK, ale często jest niepotrzebne. Zamiast tego można zdefiniować relację wiele do wielu przy użyciu tylko pomijania nawigacji. Jest to sposób, w jaki relacja wiele-do-wielu jest zdefiniowana w modelu w górnej części tego dokumentu. Korzystając z tego modelu, możemy ponownie skojarzyć post i tag, dodając wpis do Tag.Posts nawigacji pomiń (lub, alternatywnie, dodając tag do Post.Tags nawigacji pomijania):

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Patrząc na widok debugowania po wprowadzeniu tej zmiany, program EF Core utworzył wystąpienie Dictionary<string, object> reprezentujące jednostkę sprzężenia. Ta jednostka sprzężenia zawiera zarówno PostsId właściwości klucza obcego, jak i TagsId właściwości, które zostały ustawione tak, aby były zgodne z wartościami PK skojarzonego wpisu i tagu.

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 1} Added
  PostsId: 3 PK FK
  TagsId: 1 PK FK

Zobacz Relacje , aby uzyskać więcej informacji na temat niejawnych jednostek sprzężenia i używania Dictionary<string, object> typów jednostek.

Ważne

Typ CLR używany dla typów jednostek sprzężenia według konwencji może ulec zmianie w przyszłych wersjach, aby zwiększyć wydajność. Nie należy zależeć od typu Dictionary<string, object> sprzężenia, chyba że zostało to jawnie skonfigurowane.

Dołączanie jednostek za pomocą ładunków

Do tej pory wszystkie przykłady używały typu jednostki sprzężenia (jawnego lub niejawnego), który zawiera tylko dwie właściwości klucza obcego potrzebne do relacji wiele do wielu. Żadna z tych wartości klucza FK nie musi być jawnie ustawiana przez aplikację podczas manipulowania relacjami, ponieważ ich wartości pochodzą z właściwości klucza podstawowego powiązanych jednostek. Dzięki temu program EF Core może tworzyć wystąpienia jednostki sprzężenia bez brakujących danych.

Ładunki z wygenerowanymi wartościami

Program EF Core obsługuje dodawanie dodatkowych właściwości do typu jednostki sprzężenia. Jest to nazywane nadawaniem jednostce sprzężenia "ładunkiem". Na przykład dodajmy TaggedOn właściwość do PostTag jednostki join:

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Payload
}

Ta właściwość ładunku nie zostanie ustawiona, gdy program EF Core utworzy wystąpienie jednostki sprzężenia. Najczęstszym sposobem radzenia sobie z tym jest użycie właściwości ładunku z automatycznie wygenerowanymi wartościami. Na przykład właściwość można skonfigurować tak, TaggedOn aby używała znacznika czasu wygenerowanego przez magazyn po wstawieniu każdej nowej jednostki:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany(),
            j => j.Property(e => e.TaggedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

Wpis można teraz otagować w taki sam sposób jak poprzednio:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Patrząc na widok debugowania śledzenia zmian po wywołaniu polecenia SaveChanges pokazuje, że właściwość ładunku została odpowiednio ustawiona:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Unchanged
  PostId: 3 PK FK
  TagId: 1 PK FK
  TaggedOn: '12/29/2020 8:13:21 PM'
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]

Jawne ustawianie wartości ładunku

Po wykonaniu poprzedniego przykładu dodajmy właściwość ładunku, która nie używa automatycznie wygenerowanej wartości:

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Auto-generated payload property
    public string TaggedBy { get; set; } // Not-generated payload property
}

Wpis można teraz oznaczyć tak samo jak poprzednio, a jednostka sprzężenia będzie nadal tworzona automatycznie. Dostęp do tej jednostki można uzyskać przy użyciu jednego z mechanizmów opisanych w temacie Uzyskiwanie dostępu do śledzonych jednostek. Na przykład poniższy kod używa DbSet<TEntity>.Find metody w celu uzyskania dostępu do wystąpienia jednostki join:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

var joinEntity = context.Set<PostTag>().Find(post.Id, tag.Id);

joinEntity.TaggedBy = "ajcvickers";

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Po zlokalizowaniu jednostki sprzężenia można ją manipulować w normalny sposób — w tym przykładzie można ustawić właściwość ładunku TaggedBy przed wywołaniem funkcji SaveChanges.

Uwaga

Należy pamiętać, że w tym miejscu wymagane jest wywołanie ChangeTracker.DetectChanges() programu EF Core, aby wykryć zmianę właściwości nawigacji i utworzyć wystąpienie jednostki sprzężenia przed Find użyciem. Aby uzyskać więcej informacji, zobacz Wykrywanie zmian i powiadomienia .

Alternatywnie jednostkę sprzężenia można utworzyć jawnie, aby skojarzyć wpis z tagiem. Przykład:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

context.Add(
    new PostTag { PostId = post.Id, TagId = tag.Id, TaggedBy = "ajcvickers" });

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Na koniec innym sposobem ustawienia danych ładunku jest zastąpienie SaveChanges lub użycie DbContext.SavingChanges zdarzenia do przetwarzania jednostek przed zaktualizowaniem bazy danych. Przykład:

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return base.SaveChanges();
}