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 powiązana z jednostką główną/nadrzędną, gdy wartości właściwości klucza obcego w jednostce zależnej/podrzędnej są zgodne z wartościami właściwości alternatywnego lub podstawowego klucza (PK) na jednostce głównej/nadrzędnej.

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 dodaje poziom "nawigacji" nad reprezentacją FK. Nawigacje w C#/.NET tworzą powiązania między instancjami jednostek, które odzwierciedlają relacje znalezione poprzez przypasowanie wartości kluczy obcych do wartości kluczy głównych lub alternatywnych.

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 cieniowanej właściwości. Zobacz Relacje , aby uzyskać więcej informacji na temat modelowania relacji.

Wskazówka

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 ).

Wskazówka

Możesz uruchomić i debugować cały kod w tym dokumencie, pobierając przykładowy kod z usługi 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 elementami nawigacyjnymi.

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 głównym podmiotem/elementem nadrzędnym.
    • Post jest zależnym/dzieckiem. 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 między kolekcją wpisów bloga a wszystkimi skojarzonymi wpisami. Blog.Posts to odwrotna nawigacja dla elementu Post.Blog.
  • Każdy blog może mieć jeden zasób (jeden do jednego):
    • Blog jest głównym podmiotem/elementem nadrzędnym.
    • BlogAssets jest zależnym/dzieckiem. 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 stanowią dodatkową warstwę nakładającą się na dwie relacje typu jeden do wielu. Relacje wiele-do-wielu zostały przedstawione w dalszej części tego dokumentu.
    • Post.Tags to nawigacja zbioru od wpisu do wszystkich powiązanych tagów. Post.Tags to odwrotna nawigacja dla elementu Tag.Posts.
    • Tag.Posts to nawigacja zbioru z tagu do wszystkich powiązanych 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 umożliwia synchronizację nawigacji z wartościami kluczy obcych i odwrotnie. 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ę. Nazywa się to "naprawa relacji".

Korekta na podstawie zapytania

Poprawka najpierw występuje, gdy jednostki są odpytywane z bazy danych. Baza danych zawiera tylko wartości kluczy obcych, więc gdy EF Core tworzy instancję encji z bazy danych, używa tych wartości do ustawiania nawigacji referencyjnych oraz dodawania encji do nawigacji kolekcji w odpowiedni sposób. 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 = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToListAsync();

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

Dla każdego bloga platforma EF Core najpierw utworzy instancję Blog. 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 tego przypadku, obie nawigacje są odwołaniami. Nawigacja Blog.Assets jest ustawiona na wskazywanie instancji zasobów, a nawigacja BlogAsserts.Blog na instancji 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 wpisy, które są śledzone:

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 powyższym wyniku Posts: [{Id: 1}, {Id: 2}] wskazuje, ż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 Blog: {Id: 1} wskazuje, że Post.Blog nawigacja odwołuje się do bloga z kluczem podstawowym 1.

Poprawka dotycząca lokalnie śledzonych jednostek

Ustalanie relacji występuje również między jednostkami zwracanymi z zapytania śledzenia a jednostkami już śledzonymi przez 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 = await context.Blogs.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

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

var posts = await context.Posts.ToListAsync();
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 poprawione, aby wskazywać na nowo śledzone BlogAsset wystąpienia. 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ż EF Core poprawił nawigacje podczas śledzenia jednostek, nawet gdy pochodzą z wielu różnych zapytań.

Uwaga / Notatka

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. Można to zrobić za pomocą:

  • Dodawanie lub usuwanie jednostki z nawigacji kolekcji.
  • Zmiana odniesienia nawigacji tak, aby wskazywało na inną jednostkę lub zostało ustawione na brak wartości.

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 = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");
var vsBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(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);

await context.SaveChangesAsync();

Wskazówka

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 bardziej interesujące, mimo że kod nie dokonał jawnych zmian w Post.Blog nawigacji, został dostosowany tak, aby wskazywał na blog Visual Studio (Blog: {Id: 1}). Ponadto wartość klucza obcego została zaktualizowana, aby dopasować się do wartości klucza podstawowego blogu .NET. Zmiana wartości klucza FK zostaje utrwalona 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.

Wskazówka

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 elementu 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, to jednak zostaje śledzona. (Jednostka będzie zwykle w stanie Added. Jeśli jednak typ jednostki jest skonfigurowany do używania wygenerowanych kluczy, a wartość klucza podstawowego jest ustawiona, jednostka jest śledzona w stanie Unchanged.)
  • Jeśli jednostka jest skojarzona z innym podmiotem nadrzędnym, ta relacja zostaje zerwana.
  • Jednostka organizacyjna staje się powiązana z elementem głównym lub nadrzędnym, który jest właścicielem nawigacji w ramach kolekcji.
  • Nawigacje i wartości kluczy obcych są ustalone dla wszystkich 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 zależny/podrzędny nie jest już skojarzony z żadnym podmiotem głównym/rodzicielskim. Przykładowo, załadujmy blog oraz wpisy, a następnie usuńmy jeden z wpisów z nawigacji w kolekcji Blog.Posts.

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

Analiza widoku debugowania śledzenia zmian po wprowadzeniu tej zmiany 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 nawigacji kolekcji Blog.Posts(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 zostać przypisana do nowej jednostki nadrzędnej lub usunięta z bazy danych, gdy wywoływana jest 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ę pomiędzy blogiem a wpisami, aby była wymagana, a następnie uruchommy 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 nawigacji kolekcji Blog.Posts (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 osieroconego 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 oznaczenie osieroconych następuje, gdy tylko zostanie wykryta zmiana relacji. Ten proces można jednak opóźnić, dopóki funkcja SaveChanges nie zostanie wywołana. Może to być użyteczne, aby uniknąć utworzenia osieroconych jednostek, które zostały usunięte z jednego podmiotu zabezpieczeń/elementu nadrzędnego, ale staną się podległe nowemu podmiotowi zabezpieczeń/elementowi nadrzędnemu, zanim zostanie wywołana funkcja 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);

await context.SaveChangesAsync();

Po usunięciu wpisu z pierwszej kolekcji obiekt nie jest oznaczony jako Deleted, tak jak było to 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 osieroconego posta. Jednakże, jeśli tak jak w powyższym przykładzie, wpis zostanie skojarzony z nowym blogiem przed wywołaniem funkcji SaveChanges, to zostanie odpowiednio poprawiony do nowego bloga i nie będzie już uważany za "sierotę".

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 = await context.Blogs.Include(e => e.Posts).SingleAsync(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);

await context.SaveChangesAsync(); // 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 zezwala na wartość 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ęcie kaskadowe, można w dowolnym momencie wymusić za pomocą wywołania ChangeTracker.CascadeChanges(). Połączenie tego z ustawieniem czasu usunięcia osieroconych elementów na Never zapewni, że osierocone elementy nigdy nie zostaną usunięte, chyba że program EF Core zostanie wyraźnie poinstruowany, by to zrobić.

Zmienianie nawigacji referencyjnej

Zmiana nawigacji referencji relacji jeden do wielu ma taki sam wpływ, jak zmiana nawigacji kolekcji po drugiej stronie 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 modyfikacje i zmiany bazy danych są wykonywane zgodnie z opisane w poprzedniej sekcji, w tym wprowadzanie encji jako osieroconej, 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 = await context.Blogs.Include(e => e.Assets).SingleAsync(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

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

await context.SaveChangesAsync();

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 wstawienie podczas 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ę osierocony, gdy nowy BlogAssets zajmuje jego 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>

Skutkuje to usunięciem i wstawieniem, kiedy zostanie wywołana funkcja 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 elementów jako usunięte można dostosować w ten sam sposób, jak pokazano dla nawigacji kolekcji i ma to takie same efekty.

Usuwanie jednostki

Relacje opcjonalne

Gdy jednostka jest oznaczona jako Deleted, na przykład przez wywołanie metody DbContext.Remove, 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 = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .SingleAsync(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

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

await context.SaveChangesAsync();

Kiedy patrzymy na widok debugowania rejestratora zmian przed wywołaniem SaveChanges, widzimy:

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 nullowe odwołanie nawigacyjne (Blog: <null>)

Wymagane relacje

Zachowanie poprawki dla wymaganych relacji jest takie samo jak w przypadku relacji opcjonalnych, z tą różnicą, że encje zależne/dzieci są oznaczone jako Deleted, ponieważ nie mogą istnieć bez encji głównej/nadrzędnej i muszą zostać usunięte z bazy danych, gdy wywołana zostanie metoda SaveChanges, aby uniknąć wyjątku ograniczenia referencyjnego. 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 osoby zależne/dzieci 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/głównego 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 elementów, w tym do ponownego przypisania dzieci/zależnych do nowego elementu nadrzędnego po usunięciu głównego podmiotu/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 czasu usuwania kaskadowego na Never zapewni, że usunięcia kaskadowe nigdy nie nastąpią, chyba że program EF Core zostanie jawnie poinstruowany, aby to zrobić.

Wskazówka

Usuwanie kaskadowe i usuwanie osieroconych są ściśle powiązane. Oba te elementy powodują usunięcie jednostek zależnych/podrzędnych, gdy relacja z wymaganym podmiotem głównym/rodzicielskim jest zerwana. W przypadku usunięcia kaskadowego rozłączenie następuje, ponieważ element główny/nadrzędny jest usuwany. W przypadku sierot 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 w relacji wiele-do-wielu jest powiązana z tą jednostką łączącą poprzez 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 podstawowemu zachowaniu, aby zrozumieć, jak funkcjonuje ś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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

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

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

Spoglądając na widok debugowania śledzenia zmian po uruchomieniu tego kodu, widać, że wpis i tag są powiązane przez nowy PostTag byt łączący:

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 za pomocą specjalnych nawigacji kolekcji, które "pomijają" encję łączącą. Na przykład do powyższego modelu można dodać dwie nawigacje pomijające; jedna między postem a tagami, a druga między tagiem a postami.

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.

Nawigacje pomijania wyglądają i zachowują się jak normalne nawigacje kolekcji. Jednak sposób, w jaki pracują 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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(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 element do kolekcji nawigacji w taki sam sposób, jak w 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 omijania, Tag.Posts odwrotna nawigacja omijania po drugiej stronie tej relacji została również naprawiona, aby zawierała skojarzony wpis.

Warto zauważyć, że bazowe relacje wiele-do-wielu mogą być nadal manipulowane bezpośrednio, nawet gdy pomijane opcje nawigacji zostały nałożone na wierzch. Na przykład tag i post mogą być skojarzone tak jak wcześniej przed wprowadzeniem pomijania nawigacji:

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

To nadal spowoduje poprawne naprawienie nawigacji przeskakiwania, dając 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 sekcji pomijania nawigacji (lub, alternatywnie, dodając tag do Post.Tags sekcji pomijania nawigacji):

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(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, można zauważyć, że 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 polegaj na typie sprzężenia Dictionary<string, object>, o ile nie zostało to jawnie skonfigurowane.

Połącz jednostki z ładunkami

Do tej pory wszystkie przykłady wykorzystywały typ encji łączącej (jawnej lub niejawnej), który zawiera tylko dwie właściwości klucza obcego potrzebne do realizacji relacji wiele do wielu. Żadnej z wartości kluczy FK nie trzeba jawnie ustawiać 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 łączenia.

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ść TaggedOn można skonfigurować tak, aby używała znacznika czasu wygenerowanego przez sklep przy wstawianiu 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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

await context.SaveChangesAsync();

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ć w taki sam sposób jak wcześniej, a encja połączenia 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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

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

joinEntity.TaggedBy = "ajcvickers";

await context.SaveChangesAsync();

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 / Notatka

Należy pamiętać, że w tym miejscu wymagane jest wywołanie ChangeTracker.DetectChanges(), aby EF Core miało szansę wykryć zmianę właściwości nawigacji i utworzyć wystąpienie jednostki sprzężenia, zanim użyje się Find. 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.SingleAsync(e => e.Id == 3);
var tag = context.Tags.SingleAsync(e => e.Id == 1);

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

await context.SaveChangesAsync();

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

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

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return await base.SaveChangesAsync(cancellationToken);
}