Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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.BlogId
FK , 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 elementuBlog.Posts
. -
Blog.Posts
to nawigacja między kolekcją wpisów bloga a wszystkimi skojarzonymi wpisami.Blog.Posts
to odwrotna nawigacja dla elementuPost.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.BlogId
FK , 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 elementuBlog.Assets
. -
Blog.Assets
to nawigacja referencyjna z bloga do skojarzonych zasobów.Blog.Assets
to odwrotna nawigacja dla elementuBlogAssets.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 elementuTag.Posts
. -
Tag.Posts
to nawigacja zbioru z tagu do wszystkich powiązanych wpisów.Tag.Posts
to odwrotna nawigacja dla elementuPost.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 stanieUnchanged
.) - 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);
}