Zmienianie kluczy obcych i nawigacji
Omówienie kluczy obcych i nawigacji
Relacje w modelu platformy Entity Framework Core (EF Core) są reprezentowane przy użyciu kluczy obcych (FKs). Klucz FK składa się z co najmniej jednej właściwości jednostki zależnej lub podrzędnej w relacji. Ta jednostka zależna/podrzędna jest skojarzona z daną jednostką główną/nadrzędną, gdy wartości właściwości klucza obcego w zależności/podrzędnej są zgodne z wartościami właściwości alternatywnego lub podstawowego klucza (PK) dla podmiotu zabezpieczeń/elementu nadrzędnego.
Klucze obce to dobry sposób przechowywania relacji w bazie danych i manipulowania nimi, ale nie są bardzo przyjazne podczas pracy z wieloma powiązanymi jednostkami w kodzie aplikacji. W związku z tym większość modeli EF Core również warstwę "nawigacji" za pośrednictwem reprezentacji FK. Nawigacje tworzą odwołania C#/.NET między wystąpieniami jednostek, które odzwierciedlają skojarzenia znalezione przez dopasowanie wartości klucza obcego do wartości klucza podstawowego lub alternatywnego.
Nawigacje mogą być używane po obu stronach relacji, tylko po jednej stronie lub w ogóle, pozostawiając tylko właściwość FK. Właściwość FK może być ukryta przez utworzenie jej właściwości w tle. Zobacz Relacje , aby uzyskać więcej informacji na temat modelowania relacji.
Napiwek
W tym dokumencie przyjęto założenie, że stany jednostki i podstawy śledzenia zmian platformy EF Core są zrozumiałe. Aby uzyskać więcej informacji na temat tych tematów, zobacz Change Tracking in EF Core (Śledzenie zmian w programie EF Core ).
Napiwek
Możesz uruchomić i debugować cały kod podany w tym dokumencie, pobierając przykładowy kod z serwisu GitHub.
Przykładowy model
Poniższy model zawiera cztery typy jednostek z relacjami między nimi. Komentarze w kodzie wskazują, które właściwości są kluczami obcymi, kluczami podstawowymi i nawigacjami.
public class Blog
{
public int Id { get; set; } // Primary key
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>(); // Collection navigation
public BlogAssets Assets { get; set; } // Reference navigation
}
public class BlogAssets
{
public int Id { get; set; } // Primary key
public byte[] Banner { get; set; }
public int? BlogId { get; set; } // Foreign key
public Blog Blog { get; set; } // Reference navigation
}
public class Post
{
public int Id { get; set; } // Primary key
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; } // Foreign key
public Blog Blog { get; set; } // Reference navigation
public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
}
public class Tag
{
public int Id { get; set; } // Primary key
public string Text { get; set; }
public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
}
Trzy relacje w tym modelu to:
- Każdy blog może zawierać wiele wpisów (jeden do wielu):
Blog
jest podmiotem zabezpieczeń/elementem nadrzędnym.Post
jest zależny/podrzędny. Zawiera właściwośćPost.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 kolekcji z bloga do wszystkich skojarzonych wpisów.Blog.Posts
to odwrotna nawigacja dla elementuPost.Blog
.
- Każdy blog może mieć jeden zasób (jeden do jednego):
Blog
jest podmiotem zabezpieczeń/elementem nadrzędnym.BlogAssets
jest zależny/podrzędny. Zawiera właściwośćBlogAssets.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 to kolejna warstwa dwóch relacji jeden do wielu. Relacje wiele-do-wielu zostały omówione w dalszej części tego dokumentu.
Post.Tags
to nawigacja kolekcji z wpisu do wszystkich skojarzonych tagów.Post.Tags
to odwrotna nawigacja dla elementuTag.Posts
.Tag.Posts
to nawigacja kolekcji z tagu do wszystkich skojarzonych 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 utrzymuje nawigację zgodnie z wartościami klucza obcego i na odwrót. Oznacza to, że jeśli wartość klucza obcego zmieni się tak, że teraz odwołuje się do innej jednostki głównej/nadrzędnej, nawigacja zostanie zaktualizowana, aby odzwierciedlić tę zmianę. Podobnie, jeśli nawigacja zostanie zmieniona, wartości klucza obcego zaangażowanych jednostek zostaną zaktualizowane, aby odzwierciedlić tę zmianę. Jest to nazywane "fixup relacji".
Poprawka według zapytania
Poprawka najpierw występuje, gdy jednostki są odpytywane z bazy danych. Baza danych ma tylko wartości klucza obcego, więc gdy program EF Core tworzy wystąpienie jednostki z bazy danych, używa wartości klucza obcego do ustawiania nawigacji odwołań i dodawania jednostek do nawigacji kolekcji zgodnie z potrzebami. Rozważmy na przykład zapytanie dotyczące blogów i skojarzonych z nim wpisów i zasobów:
using var context = new BlogsContext();
var blogs = context.Blogs
.Include(e => e.Posts)
.Include(e => e.Assets)
.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Dla każdego bloga platforma EF Core najpierw utworzy Blog
wystąpienie. Następnie, gdy każdy wpis jest ładowany z bazy danych, jego Post.Blog
nawigacja referencyjna jest ustawiona tak, aby wskazywała skojarzony blog. Podobnie wpis jest dodawany do Blog.Posts
nawigacji kolekcji. To samo dzieje się z elementem BlogAssets
, z wyjątkiem w tym przypadku oba nawigacje są odwołaniami. Nawigacja Blog.Assets
jest ustawiona tak, aby wskazywała wystąpienie zasobów, a nawigacja BlogAsserts.Blog
jest ustawiona tak, aby wskazywała wystąpienie bloga.
Patrząc na widok debugowania monitora zmian po tym zapytaniu przedstawiono dwa blogi, z których każdy ma jeden zasób i dwa śledzone wpisy:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: 1}
Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
Id: 1 PK
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
Id: 2 PK
Banner: <null>
BlogId: 2 FK
Blog: {Id: 2}
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Tags: []
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Post {Id: 4} Unchanged
Id: 4 PK
BlogId: 2 FK
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: {Id: 2}
Tags: []
Widok debugowania zawiera zarówno wartości klucza, jak i nawigację. Nawigacje są wyświetlane przy użyciu wartości klucza podstawowego powiązanych jednostek. Na przykład w danych wyjściowych powyżej wskazuje, Posts: [{Id: 1}, {Id: 2}]
że Blog.Posts
nawigacja kolekcji zawiera dwa powiązane wpisy z kluczami podstawowymi odpowiednio 1 i 2. Podobnie dla każdego wpisu skojarzonego z pierwszym blogiem wiersz wskazuje, Blog: {Id: 1}
że Post.Blog
nawigacja odwołuje się do bloga z kluczem podstawowym 1.
Poprawka dotycząca lokalnie śledzonych jednostek
Poprawka relacji występuje również między jednostkami zwracanym z zapytania śledzenia i jednostkami już śledzonym przez element DbContext. Rozważ na przykład wykonywanie trzech oddzielnych zapytań dotyczących blogów, wpisów i zasobów:
using var context = new BlogsContext();
var blogs = context.Blogs.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
var assets = context.Assets.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
var posts = context.Posts.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Patrząc ponownie na widoki debugowania, po pierwszym zapytaniu śledzone są tylko dwa blogi:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: <null>
Posts: []
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: <null>
Posts: []
Nawigacje Blog.Assets
referencyjne mają wartość null, a Blog.Posts
nawigacje kolekcji są puste, ponieważ żadne skojarzone jednostki nie są obecnie śledzone przez kontekst.
Po drugim zapytaniu Blogs.Assets
nawigacje referencyjne zostały naprawione w celu wskazania nowo śledzonych BlogAsset
wystąpień. BlogAssets.Blog
Podobnie nawigacje referencyjne są ustawione tak, aby wskazywały odpowiednie już śledzone Blog
wystąpienie.
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: 1}
Posts: []
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: []
BlogAssets {Id: 1} Unchanged
Id: 1 PK
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
Id: 2 PK
Banner: <null>
BlogId: 2 FK
Blog: {Id: 2}
Na koniec po trzecim zapytaniu Blog.Posts
nawigacja kolekcji zawiera teraz wszystkie powiązane wpisy, a Post.Blog
odwołania wskazują na odpowiednie Blog
wystąpienie:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: 1}
Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
Id: 1 PK
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
Id: 2 PK
Banner: <null>
BlogId: 2 FK
Blog: {Id: 2}
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Tags: []
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Post {Id: 4} Unchanged
Id: 4 PK
BlogId: 2 FK
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: {Id: 2}
Tags: []
Jest to taki sam stan końcowy, jak w przypadku oryginalnego pojedynczego zapytania, ponieważ program EF Core naprawił nawigacje jako jednostki, nawet w przypadku pochodzących z wielu różnych zapytań.
Uwaga
Poprawka nigdy nie powoduje zwracania większej ilości danych z bazy danych. Łączy tylko jednostki, które są już zwracane przez zapytanie lub już śledzone przez dbContext. Zobacz Rozpoznawanie tożsamości w programie EF Core , aby uzyskać informacje na temat obsługi duplikatów podczas serializacji jednostek.
Zmienianie relacji przy użyciu nawigacji
Najprostszym sposobem zmiany relacji między dwiema jednostkami jest manipulowanie nawigacją, pozostawiając program EF Core w celu odpowiedniego naprawienia odwrotnej nawigacji i wartości FK. W tym celu możesz wykonać jedną z następujących czynności:
- Dodawanie lub usuwanie jednostki z nawigacji kolekcji.
- Zmiana nawigacji odwołania w taki sposób, aby wskazywała inną jednostkę lub ustawiała ją na wartość null.
Dodawanie lub usuwanie z nawigacji kolekcji
Na przykład przenieśmy jeden z wpisów z bloga programu Visual Studio do bloga platformy .NET. Wymaga to najpierw załadowania blogów i wpisów, a następnie przeniesienia wpisu z kolekcji nawigacji w jednym blogu do kolekcji nawigacji w drugim blogu:
using var context = new BlogsContext();
var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");
var vsBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == "Visual Studio Blog");
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Napiwek
W tym miejscu jest wymagane wywołanie ChangeTracker.DetectChanges() , ponieważ uzyskiwanie dostępu do widoku debugowania nie powoduje automatycznego wykrywania zmian.
Jest to widok debugowania wydrukowany po uruchomieniu powyższego kodu:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: <null>
Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: <null>
Posts: [{Id: 4}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Tags: []
Post {Id: 3} Modified
Id: 3 PK
BlogId: 1 FK Modified Originally 2
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 1}
Tags: []
Post {Id: 4} Unchanged
Id: 4 PK
BlogId: 2 FK
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: {Id: 2}
Tags: []
Nawigacja Blog.Posts
w blogu platformy .NET zawiera teraz trzy wpisy (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
). Podobnie nawigacja Blog.Posts
w blogu programu Visual Studio zawiera tylko jeden wpis (Posts: [{Id: 4}]
). Jest to oczekiwane, ponieważ kod jawnie zmienił te kolekcje.
Co ciekawe, mimo że kod nie zmienił Post.Blog
jawnie nawigacji, został naprawiony tak, aby wskazywał blog programu Visual Studio (Blog: {Id: 1}
). Ponadto wartość klucza obcego została zaktualizowana w Post.BlogId
celu dopasowania wartości klucza podstawowego bloga platformy .NET. Ta zmiana wartości klucza FK w poleceniu utrwalonej w bazie danych po wywołaniu funkcji SaveChanges:
-- Executed DbCommand (0ms) [Parameters=[@p1='3' (DbType = String), @p0='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
Zmienianie nawigacji odwołań
W poprzednim przykładzie wpis został przeniesiony z jednego bloga do innego, manipulując nawigacją po kolekcjach wpisów w każdym blogu. Tę samą czynność można osiągnąć, zmieniając nawigację referencyjną Post.Blog
tak, aby wskazywała nowy blog. Przykład:
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.Blog = dotNetBlog;
Widok debugowania po tej zmianie jest dokładnie taki sam jak w poprzednim przykładzie. Dzieje się tak, ponieważ program EF Core wykrył zmianę nawigacji referencyjnej, a następnie naprawił nawigacje kolekcji i wartość FK w celu dopasowania.
Zmienianie relacji przy użyciu wartości klucza obcego
W poprzedniej sekcji relacje były manipulowane przez nawigacje pozostawiając wartości kluczy obcych do automatycznego aktualizowania. Jest to zalecany sposób manipulowania relacjami w programie EF Core. Można jednak bezpośrednio manipulować wartościami FK. Na przykład możemy przenieść wpis z jednego bloga na inny, zmieniając wartość klucza obcego Post.BlogId
:
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.BlogId = dotNetBlog.Id;
Zwróć uwagę, że jest to bardzo podobne do zmiany nawigacji referencyjnej, jak pokazano w poprzednim przykładzie.
Widok debugowania po tej zmianie jest dokładnie taki sam jak w przypadku poprzednich dwóch przykładów. Jest to spowodowane tym, że program EF Core wykrył zmianę wartości klucza FK, a następnie naprawił nawigację zarówno referencyjną, jak i nawigację kolekcji w taki sposób, aby była zgodna.
Napiwek
Nie zapisuj kodu, aby manipulować wszystkimi nawigacjami i wartościami FK za każdym razem, gdy zmienia się relacja. Taki kod jest bardziej skomplikowany i musi zapewnić spójne zmiany kluczy obcych i nawigacji w każdym przypadku. Jeśli to możliwe, wystarczy manipulować pojedynczą nawigacją, a może obie nawigacje. W razie potrzeby wystarczy manipulować wartościami FK. Unikaj manipulowania zarówno nawigacjami, jak i wartościami FK.
Poprawka dotycząca dodanych lub usuniętych jednostek
Dodawanie do nawigacji kolekcji
Program EF Core wykonuje następujące akcje, gdy wykryje , że nowa jednostka zależna/podrzędna została dodana do nawigacji kolekcji:
- Jeśli jednostka nie jest śledzona, jest śledzona. (Jednostka będzie zwykle w
Added
stanie . Jeśli jednak typ jednostki jest skonfigurowany do używania wygenerowanych kluczy, a wartość klucza podstawowegoUnchanged
jest ustawiona, jednostka jest śledzona w stanie. - Jeśli jednostka jest skojarzona z innym podmiotem zabezpieczeń/elementem nadrzędnym, ta relacja jest zerwana.
- Jednostka staje się skojarzona z jednostką/elementem nadrzędnym, który jest właścicielem nawigacji kolekcji.
- Nawigacje i wartości kluczy obcych są naprawione dla wszystkich zaangażowanych jednostek.
Na podstawie tego widać, że aby przenieść wpis z jednego bloga do innego, nie musimy go usuwać ze starej nawigacji kolekcji przed dodaniem go do nowego. W związku z tym kod z powyższego przykładu można zmienić z:
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);
Do:
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post);
Program EF Core widzi, że wpis został dodany do nowego bloga i automatycznie usuwa go z kolekcji w pierwszym blogu.
Usuwanie z nawigacji kolekcji
Usunięcie jednostki zależnej/podrzędnej z nawigacji kolekcji podmiotu zabezpieczeń/elementu nadrzędnego powoduje zerwanie relacji z tym podmiotem zabezpieczeń/elementem nadrzędnym. To, co się stanie dalej, zależy od tego, czy relacja jest opcjonalna, czy wymagana.
Relacje opcjonalne
Domyślnie w przypadku relacji opcjonalnych wartość klucza obcego jest ustawiona na wartość null. Oznacza to, że element zależny/podrzędny nie jest już skojarzony z żadnym podmiotem zabezpieczeń/elementem nadrzędnym. Na przykład załadujmy blog i wpisy, a następnie usuńmy jeden z wpisów z Blog.Posts
nawigacji kolekcji:
var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);
Patrząc na widok debugowania śledzenia zmian po tej zmianie pokazuje, że:
- Klucz
Post.BlogId
FK został ustawiony na wartość null (BlogId: <null> FK Modified Originally 1
) - Nawigacja referencyjna
Post.Blog
została ustawiona na wartość null (Blog: <null>
) - Wpis został usunięty z
Blog.Posts
nawigacji kolekcji (Posts: [{Id: 1}]
)
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: <null>
Posts: [{Id: 1}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Modified
Id: 2 PK
BlogId: <null> FK Modified Originally 1
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Tags: []
Zwróć uwagę, że wpis nie jest oznaczony jako Deleted
. Jest ona oznaczona jako Modified
tak, aby wartość FK w bazie danych została ustawiona na wartość null po wywołaniu funkcji SaveChanges.
Wymagane relacje
Ustawienie wartości FK na wartość null jest niedozwolone (i zwykle nie jest możliwe) dla wymaganych relacji. W związku z tym zerwanie wymaganej relacji oznacza, że jednostka zależna/podrzędna musi być ponownie nadrzędna do nowej jednostki/elementu nadrzędnego lub usunięta z bazy danych, gdy jest wywoływana funkcja SaveChanges, aby uniknąć naruszenia ograniczeń odwołań. Jest to nazywane "usuwaniem osieroconych" i jest domyślnym zachowaniem w programie EF Core w przypadku wymaganych relacji.
Na przykład zmieńmy relację między wpisami w blogu, aby były wymagane, a następnie uruchomimy ten sam kod, co w poprzednim przykładzie:
var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);
Patrząc na widok debugowania po tej zmianie, widać, że:
- Wpis został oznaczony jako
Deleted
taki, że zostanie usunięty z bazy danych po wywołaniu funkcji SaveChanges. - Nawigacja referencyjna
Post.Blog
została ustawiona na wartość null (Blog: <null>
). - Wpis został usunięty z
Blog.Posts
nawigacji kolekcji (Posts: [{Id: 1}]
).
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: <null>
Posts: [{Id: 1}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Deleted
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Tags: []
Zwróć uwagę, że pozostaje niezmieniona Post.BlogId
, ponieważ dla wymaganej relacji nie można ustawić wartości null.
Wywołanie metody SaveChanges powoduje usunięcie oddzielonego wpisu:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
Usuwanie czasu i ponownego rodzicielstwa
Domyślnie oznaczanie osieroconych tak Deleted
szybko, jak tylko zostanie wykryta zmiana relacji. Ten proces można jednak opóźnić, dopóki funkcja SaveChanges nie zostanie wywołana. Może to być przydatne, aby uniknąć tworzenia oddzielonych jednostek, które zostały usunięte z jednego podmiotu zabezpieczeń/elementu nadrzędnego, ale zostanie ponownie nadrzędne z nowym podmiotem zabezpieczeń/elementem nadrzędnym przed wywołaniem funkcji SaveChanges. ChangeTracker.DeleteOrphansTiming służy do ustawiania tego chronometrażu. Przykład:
context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
dotNetBlog.Posts.Add(post);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Po usunięciu wpisu z pierwszej kolekcji obiekt nie jest oznaczony jako Deleted
w poprzednim przykładzie. Zamiast tego program EF Core śledzi, że relacja jest zerwana , mimo że jest to wymagana relacja. (Wartość FK jest uważana za null przez program EF Core, mimo że nie może być równa null, ponieważ typ nie może mieć wartości null. Jest to nazywane "koncepcyjną wartością null".
Post {Id: 3} Modified
Id: 3 PK
BlogId: <null> FK Modified Originally 2
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
Tags: []
Wywołanie funkcji SaveChanges w tej chwili spowoduje usunięcie oddzielonego wpisu. Jeśli jednak tak jak w powyższym przykładzie wpis jest skojarzony z nowym blogiem przed wywołaniem funkcji SaveChanges, zostanie on odpowiednio naprawiony dla tego nowego bloga i nie jest już uważany za oddzielony:
Post {Id: 3} Modified
Id: 3 PK
BlogId: 1 FK Modified Originally 2
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 1}
Tags: []
Funkcja SaveChanges wywoływana w tym momencie zaktualizuje wpis w bazie danych, a nie usunie go.
Można również wyłączyć automatyczne usuwanie osieroconych. Spowoduje to wyjątek, jeśli funkcja SaveChanges jest wywoływana podczas śledzenia sieroty. Na przykład ten kod:
var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");
context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;
var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);
context.SaveChanges(); // Throws
Zgłosi ten wyjątek:
System.InvalidOperationException: skojarzenie między jednostkami "Blog" i "Post" z wartością klucza "{BlogId: 1}" zostało zerwane, ale relacja jest oznaczona jako wymagana lub jest niejawnie wymagana, ponieważ klucz obcy nie jest dopuszczalny do wartości null. Jeśli jednostka zależna/podrzędna powinna zostać usunięta po zerwaniu wymaganej relacji, skonfiguruj relację tak, aby korzystała z usuwania kaskadowego.
Usunięcie osieroconych, a także usunięcia kaskadowego, można w dowolnym momencie wymusić przez wywołanie metody ChangeTracker.CascadeChanges(). Połączenie tego z ustawieniem czasu usunięcia osieroconego w celu Never
zapewnienia, że osieroce nigdy nie zostaną usunięte, chyba że program EF Core zostanie jawnie poinstruowany, aby to zrobić.
Zmienianie nawigacji referencyjnej
Zmiana nawigacji odwołania relacji jeden do wielu ma taki sam wpływ, jak zmiana nawigacji kolekcji na drugim końcu relacji. Ustawienie nawigacji referencyjnej elementu zależnego/podrzędnego na wartość null jest równoważne usunięciu jednostki z nawigacji kolekcji podmiotu zabezpieczeń/elementu nadrzędnego. Wszystkie poprawki i zmiany bazy danych są wykonywane zgodnie z opisem w poprzedniej sekcji, w tym wprowadzanie jednostki jako oddzielonej, jeśli relacja jest wymagana.
Opcjonalne relacje jeden do jednego
W przypadku relacji jeden do jednego zmiana nawigacji referencyjnej powoduje zerwanie poprzedniej relacji. W przypadku relacji opcjonalnych oznacza to, że wartość klucza FK dla wcześniej powiązanego elementu zależnego/podrzędnego jest ustawiona na wartość null. Przykład:
using var context = new BlogsContext();
var dotNetBlog = context.Blogs.Include(e => e.Assets).Single(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Widok debugowania przed wywołaniem polecenia SaveChanges pokazuje, że nowe zasoby zastąpiły istniejące zasoby, które są teraz oznaczone jako Modified
z wartością null BlogAssets.BlogId
FK:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: -2147482629}
Posts: []
BlogAssets {Id: -2147482629} Added
Id: -2147482629 PK Temporary
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 1} Modified
Id: 1 PK
Banner: <null>
BlogId: <null> FK Modified Originally 1
Blog: <null>
Spowoduje to aktualizację i wstaw, gdy jest wywoływana funkcja SaveChanges:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Assets" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p2=NULL, @p3='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p2, @p3);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Wymagane relacje jeden do jednego
Uruchomienie tego samego kodu, co w poprzednim przykładzie, ale tym razem z wymaganą relacją jeden do jednego pokazuje, że wcześniej skojarzony BlogAssets
jest teraz oznaczony jako Deleted
, ponieważ staje się oddzielony, gdy nowy BlogAssets
ma miejsce:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: -2147482639}
Posts: []
BlogAssets {Id: -2147482639} Added
Id: -2147482639 PK Temporary
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 1} Deleted
Id: 1 PK
Banner: <null>
BlogId: 1 FK
Blog: <null>
Spowoduje to usunięcie i wstawienie po wywołaniu funkcji SaveChanges:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Assets"
WHERE "Id" = @p0;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1=NULL, @p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p1, @p2);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Czas oznaczania osieroconych można zmienić w taki sam sposób, jak pokazano dla nawigacji kolekcji i ma takie same efekty.
Usuwanie jednostki
Relacje opcjonalne
Gdy jednostka jest oznaczona jako Deleted
, na przykład przez wywołanie DbContext.Removemetody , odwołania do usuniętej jednostki są usuwane z nawigacji innych jednostek. W przypadku relacji opcjonalnych wartości klucza FK w jednostkach zależnych są ustawione na wartość null.
Na przykład oznaczmy blog programu Visual Studio jako Deleted
:
using var context = new BlogsContext();
var vsBlog = context.Blogs
.Include(e => e.Posts)
.Include(e => e.Assets)
.Single(e => e.Name == "Visual Studio Blog");
context.Remove(vsBlog);
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Patrząc na widok debugowania śledzenia zmian przed wywołaniem polecenia SaveChanges pokazuje:
Blog {Id: 2} Deleted
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Modified
Id: 2 PK
Banner: <null>
BlogId: <null> FK Modified Originally 2
Blog: <null>
Post {Id: 3} Modified
Id: 3 PK
BlogId: <null> FK Modified Originally 2
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
Tags: []
Post {Id: 4} Modified
Id: 4 PK
BlogId: <null> FK Modified Originally 2
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: <null>
Tags: []
Zwróć uwagę, że:
- Blog jest oznaczony jako
Deleted
. - Zasoby powiązane z usuniętym blogiem mają wartość null FK (
BlogId: <null> FK Modified Originally 2
) i nawigację po odwołaniu o wartości null (Blog: <null>
) - Każdy wpis powiązany z usuniętym blogiem ma wartość null FK (
BlogId: <null> FK Modified Originally 2
) i nawigację po odwołaniu o wartości null (Blog: <null>
)
Wymagane relacje
Zachowanie poprawki dla wymaganych relacji jest takie samo jak w przypadku relacji opcjonalnych, z tą różnicą, że jednostki zależne/podrzędne są oznaczone jako Deleted
, ponieważ nie mogą istnieć bez podmiotu zabezpieczeń/elementu nadrzędnego i muszą zostać usunięte z bazy danych, gdy jest wywoływana funkcja SaveChanges, aby uniknąć wyjątku ograniczenia odwołania. Jest to nazywane "usuwaniem kaskadowym" i jest zachowaniem domyślnym w programie EF Core w przypadku wymaganych relacji. Na przykład uruchomienie tego samego kodu co w poprzednim przykładzie, ale z wymaganą relacją powoduje wyświetlenie następującego widoku debugowania przed wywołaniem funkcji SaveChanges:
Blog {Id: 2} Deleted
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Deleted
Id: 2 PK
Banner: <null>
BlogId: 2 FK
Blog: {Id: 2}
Post {Id: 3} Deleted
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Post {Id: 4} Deleted
Id: 4 PK
BlogId: 2 FK
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: {Id: 2}
Tags: []
Zgodnie z oczekiwaniami zależne/podrzędne są teraz oznaczone jako Deleted
. Należy jednak zauważyć, że nawigacje w usuniętych jednostkach nie uległy zmianie. Może to wydawać się dziwne, ale pozwala uniknąć całkowitego rozdrobnienia usuniętego grafu jednostek przez wyczyszczenie wszystkich nawigacji. Oznacza to, że blog, zasób i wpisy nadal tworzą graf jednostek nawet po usunięciu. Znacznie ułatwia to usunięcie grafu jednostek niż w przypadku ef6, w którym wykres został rozdrobniony.
Kaskadowe usuwanie chronometrażu i ponownego elementu nadrzędnego
Domyślnie usuwanie kaskadowe odbywa się natychmiast po oznaczeniu elementu nadrzędnego/podmiotu zabezpieczeń jako Deleted
. Jest to takie samo, jak w przypadku usuwania osieroconych, jak opisano wcześniej. Podobnie jak w przypadku usuwania osieroconych, ten proces można opóźnić, dopóki funkcja SaveChanges nie zostanie wywołana, a nawet całkowicie wyłączona, przez odpowiednie ustawienie ChangeTracker.CascadeDeleteTiming . Jest to przydatne w taki sam sposób, jak w przypadku usuwania osieroconych, w tym dla elementów podrzędnych/zależnych od ponownego rodzica po usunięciu podmiotu zabezpieczeń/elementu nadrzędnego.
Usunięcie kaskadowe, a także usunięcie osieroconych, można w dowolnym momencie wymusić przez wywołanie metody ChangeTracker.CascadeChanges(). Połączenie tego z ustawieniem chronometrażu usuwania kaskadowego w celu Never
zapewnienia, że kaskadowe usunięcie nigdy nie nastąpi, chyba że program EF Core zostanie jawnie poinstruowany, aby to zrobić.
Napiwek
Usuwanie kaskadowe i usuwanie osieroconych jest ściśle powiązane. Oba te elementy powodują usunięcie jednostek zależnych/podrzędnych, gdy relacja z wymaganym podmiotem zabezpieczeń/elementem nadrzędnym jest zerwana. W przypadku usunięcia kaskadowego następuje to zerwanie, ponieważ jednostka/element nadrzędny jest usuwany. W przypadku oddzielonych jednostka główna/nadrzędna nadal istnieje, ale nie jest już powiązana z jednostkami zależnymi/podrzędnymi.
Relacje „wiele do wielu”
Relacje wiele-do-wielu w programie EF Core są implementowane przy użyciu jednostki sprzężenia. Każda strona relacji wiele do wielu jest powiązana z tą jednostką sprzężenia z relacją jeden do wielu. Tę jednostkę sprzężenia można jawnie zdefiniować i zamapować lub utworzyć niejawnie i ukryć. W obu przypadkach zachowanie bazowe jest takie samo. Najpierw przyjrzymy się temu bazowemu zachowaniu, aby zrozumieć, jak działa śledzenie relacji wiele-do-wielu.
Jak działają relacje wiele-do-wielu
Rozważmy ten model platformy EF Core, który tworzy relację wiele-do-wielu między wpisami i tagami przy użyciu jawnie zdefiniowanego typu jednostki sprzężenia:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}
public class Tag
{
public int Id { get; set; }
public string Text { get; set; }
public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}
public class PostTag
{
public int PostId { get; set; } // First part of composite PK; FK to Post
public int TagId { get; set; } // Second part of composite PK; FK to Tag
public Post Post { get; set; } // Reference navigation
public Tag Tag { get; set; } // Reference navigation
}
Zwróć uwagę, że PostTag
typ jednostki sprzężenia zawiera dwie właściwości klucza obcego. W tym modelu, aby wpis był powiązany z tagiem, musi istnieć jednostka sprzężenia PostTag, w której PostTag.PostId
wartość klucza obcego jest zgodna Post.Id
z wartością klucza podstawowego, a PostTag.TagId
wartość klucza obcego jest zgodna z wartością klucza podstawowego Tag.Id
. Przykład:
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Patrząc na widok debugowania śledzenia zmian po uruchomieniu tego kodu pokazuje, że wpis i tag są powiązane z nową PostTag
jednostką sprzężenia:
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
PostTags: [{PostId: 3, TagId: 1}]
PostTag {PostId: 3, TagId: 1} Added
PostId: 3 PK FK
TagId: 1 PK FK
Post: {Id: 3}
Tag: {Id: 1}
Tag {Id: 1} Unchanged
Id: 1 PK
Text: '.NET'
PostTags: [{PostId: 3, TagId: 1}]
Zwróć uwagę, że nawigacje kolekcji w systemie Post
i Tag
zostały naprawione, podobnie jak nawigacje referencyjne w systemie PostTag
. Te relacje można manipulować za pomocą nawigacji zamiast wartości FK, podobnie jak we wszystkich poprzednich przykładach. Na przykład powyższy kod można zmodyfikować, aby dodać relację, ustawiając nawigacje referencyjne w jednostce sprzężenia:
context.Add(new PostTag { Post = post, Tag = tag });
Spowoduje to dokładnie taką samą zmianę w przypadku zestawów FKs i nawigacji, jak w poprzednim przykładzie.
Pomiń nawigacje
Ręczne manipulowanie tabelą sprzężenia może być kłopotliwe. Relacje wiele-do-wielu można manipulować bezpośrednio przy użyciu specjalnych nawigacji kolekcji, które "pomijają" jednostkę sprzężenia. Na przykład do powyższego modelu można dodać dwie nawigacje pomijania; jeden od postu do tagów, a drugi z tagu do postów:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}
public class Tag
{
public int Id { get; set; }
public string Text { get; set; }
public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}
public class PostTag
{
public int PostId { get; set; } // First part of composite PK; FK to Post
public int TagId { get; set; } // Second part of composite PK; FK to Tag
public Post Post { get; set; } // Reference navigation
public Tag Tag { get; set; } // Reference navigation
}
Ta relacja wiele-do-wielu wymaga następującej konfiguracji, aby upewnić się, że nawigacje pomijane i normalne nawigacje są używane dla tej samej relacji wiele-do-wielu:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j.HasOne(t => t.Tag).WithMany(p => p.PostTags),
j => j.HasOne(t => t.Post).WithMany(p => p.PostTags));
}
Zobacz Relacje , aby uzyskać więcej informacji na temat mapowania relacji wiele-do-wielu.
Pomiń nawigacje wyglądają i zachowują się jak normalne nawigacje kolekcji. Jednak sposób ich pracy z wartościami klucza obcego jest inny. Skojarzmy post z tagiem, ale tym razem użyjemy nawigacji pomiń:
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
post.Tags.Add(tag);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Zwróć uwagę, że ten kod nie używa jednostki join. Zamiast tego po prostu dodaje jednostkę do kolekcji nawigacji w taki sam sposób, jak w przypadku relacji jeden do wielu. Wynikowy widok debugowania jest zasadniczo taki sam jak poprzednio:
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
PostTags: [{PostId: 3, TagId: 1}]
Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Added
PostId: 3 PK FK
TagId: 1 PK FK
Post: {Id: 3}
Tag: {Id: 1}
Tag {Id: 1} Unchanged
Id: 1 PK
Text: '.NET'
PostTags: [{PostId: 3, TagId: 1}]
Posts: [{Id: 3}]
Zwróć uwagę, że wystąpienie PostTag
jednostki sprzężenia zostało utworzone automatycznie z wartościami FK ustawionymi na wartości PK tagu i wpisu, które są teraz skojarzone. Wszystkie normalne nawigacje odwołań i kolekcji zostały naprawione tak, aby były zgodne z tymi wartościami FK. Ponadto, ponieważ ten model zawiera pomijanie nawigacji, zostały one również naprawione. W szczególności, mimo że dodaliśmy tag do Post.Tags
nawigacji pominięcia, Tag.Posts
odwrotna nawigacja pomiń po drugiej stronie tej relacji została również naprawiona tak, aby zawierał skojarzony wpis.
Warto zauważyć, że bazowe relacje wiele-do-wielu mogą być nadal manipulowane bezpośrednio, nawet gdy nawigacja pomiń została warstwowa na górze. Na przykład tag i post mogą być skojarzone tak jak wcześniej przed wprowadzeniem nawigacji pominięcia:
context.Add(new PostTag { Post = post, Tag = tag });
Możesz też użyć wartości FK:
context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });
Nadal spowoduje to poprawne naprawienie nawigacji pomijania, co spowoduje, że te same dane wyjściowe widoku debugowania, co w poprzednim przykładzie.
Pomiń tylko nawigację
W poprzedniej sekcji dodaliśmy pomijanie nawigacji oprócz pełnego zdefiniowania dwóch podstawowych relacji jeden do wielu. Jest to przydatne, aby zilustrować, co się dzieje z wartościami FK, ale często jest niepotrzebne. Zamiast tego można zdefiniować relację wiele do wielu przy użyciu tylko pomijania nawigacji. Jest to sposób, w jaki relacja wiele-do-wielu jest zdefiniowana w modelu w górnej części tego dokumentu. Korzystając z tego modelu, możemy ponownie skojarzyć post i tag, dodając wpis do Tag.Posts
nawigacji pomiń (lub, alternatywnie, dodając tag do Post.Tags
nawigacji pomijania):
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
post.Tags.Add(tag);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Patrząc na widok debugowania po wprowadzeniu tej zmiany, program EF Core utworzył wystąpienie Dictionary<string, object>
reprezentujące jednostkę sprzężenia. Ta jednostka sprzężenia zawiera zarówno PostsId
właściwości klucza obcego, jak i TagsId
właściwości, które zostały ustawione tak, aby były zgodne z wartościami PK skojarzonego wpisu i tagu.
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
Tags: [{Id: 1}]
Tag {Id: 1} Unchanged
Id: 1 PK
Text: '.NET'
Posts: [{Id: 3}]
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 1} Added
PostsId: 3 PK FK
TagsId: 1 PK FK
Zobacz Relacje , aby uzyskać więcej informacji na temat niejawnych jednostek sprzężenia i używania Dictionary<string, object>
typów jednostek.
Ważne
Typ CLR używany dla typów jednostek sprzężenia według konwencji może ulec zmianie w przyszłych wersjach, aby zwiększyć wydajność. Nie należy zależeć od typu Dictionary<string, object>
sprzężenia, chyba że zostało to jawnie skonfigurowane.
Dołączanie jednostek za pomocą ładunków
Do tej pory wszystkie przykłady używały typu jednostki sprzężenia (jawnego lub niejawnego), który zawiera tylko dwie właściwości klucza obcego potrzebne do relacji wiele do wielu. Żadna z tych wartości klucza FK nie musi być jawnie ustawiana przez aplikację podczas manipulowania relacjami, ponieważ ich wartości pochodzą z właściwości klucza podstawowego powiązanych jednostek. Dzięki temu program EF Core może tworzyć wystąpienia jednostki sprzężenia bez brakujących danych.
Ładunki z wygenerowanymi wartościami
Program EF Core obsługuje dodawanie dodatkowych właściwości do typu jednostki sprzężenia. Jest to nazywane nadawaniem jednostce sprzężenia "ładunkiem". Na przykład dodajmy TaggedOn
właściwość do PostTag
jednostki join:
public class PostTag
{
public int PostId { get; set; } // First part of composite PK; FK to Post
public int TagId { get; set; } // Second part of composite PK; FK to Tag
public DateTime TaggedOn { get; set; } // Payload
}
Ta właściwość ładunku nie zostanie ustawiona, gdy program EF Core utworzy wystąpienie jednostki sprzężenia. Najczęstszym sposobem radzenia sobie z tym jest użycie właściwości ładunku z automatycznie wygenerowanymi wartościami. Na przykład właściwość można skonfigurować tak, TaggedOn
aby używała znacznika czasu wygenerowanego przez magazyn po wstawieniu każdej nowej jednostki:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j.HasOne<Tag>().WithMany(),
j => j.HasOne<Post>().WithMany(),
j => j.Property(e => e.TaggedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}
Wpis można teraz otagować w taki sam sposób jak poprzednio:
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
post.Tags.Add(tag);
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Patrząc na widok debugowania śledzenia zmian po wywołaniu polecenia SaveChanges pokazuje, że właściwość ładunku została odpowiednio ustawiona:
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Unchanged
PostId: 3 PK FK
TagId: 1 PK FK
TaggedOn: '12/29/2020 8:13:21 PM'
Tag {Id: 1} Unchanged
Id: 1 PK
Text: '.NET'
Posts: [{Id: 3}]
Jawne ustawianie wartości ładunku
Po wykonaniu poprzedniego przykładu dodajmy właściwość ładunku, która nie używa automatycznie wygenerowanej wartości:
public class PostTag
{
public int PostId { get; set; } // First part of composite PK; FK to Post
public int TagId { get; set; } // Second part of composite PK; FK to Tag
public DateTime TaggedOn { get; set; } // Auto-generated payload property
public string TaggedBy { get; set; } // Not-generated payload property
}
Wpis można teraz oznaczyć tak samo jak poprzednio, a jednostka sprzężenia będzie nadal tworzona automatycznie. Dostęp do tej jednostki można uzyskać przy użyciu jednego z mechanizmów opisanych w temacie Uzyskiwanie dostępu do śledzonych jednostek. Na przykład poniższy kod używa DbSet<TEntity>.Find metody w celu uzyskania dostępu do wystąpienia jednostki join:
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
post.Tags.Add(tag);
context.ChangeTracker.DetectChanges();
var joinEntity = context.Set<PostTag>().Find(post.Id, tag.Id);
joinEntity.TaggedBy = "ajcvickers";
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Po zlokalizowaniu jednostki sprzężenia można ją manipulować w normalny sposób — w tym przykładzie można ustawić właściwość ładunku TaggedBy
przed wywołaniem funkcji SaveChanges.
Uwaga
Należy pamiętać, że w tym miejscu wymagane jest wywołanie ChangeTracker.DetectChanges() programu EF Core, aby wykryć zmianę właściwości nawigacji i utworzyć wystąpienie jednostki sprzężenia przed Find
użyciem. Aby uzyskać więcej informacji, zobacz Wykrywanie zmian i powiadomienia .
Alternatywnie jednostkę sprzężenia można utworzyć jawnie, aby skojarzyć wpis z tagiem. Przykład:
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
context.Add(
new PostTag { PostId = post.Id, TagId = tag.Id, TaggedBy = "ajcvickers" });
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Na koniec innym sposobem ustawienia danych ładunku jest zastąpienie SaveChanges lub użycie DbContext.SavingChanges zdarzenia do przetwarzania jednostek przed zaktualizowaniem bazy danych. Przykład:
public override int SaveChanges()
{
foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
{
if (entityEntry.State == EntityState.Added)
{
entityEntry.Entity.TaggedBy = "ajcvickers";
}
}
return base.SaveChanges();
}