Dodatkowe funkcje śledzenia zmian
W tym dokumencie opisano różne funkcje i scenariusze dotyczące śledzenia zmian.
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.
Add
a AddAsync
Program Entity Framework Core (EF Core) udostępnia metody asynchroniczne przy każdym użyciu tej metody, co może spowodować interakcję z bazą danych. Dostępne są również metody synchroniczne, aby uniknąć narzutów podczas korzystania z baz danych, które nie obsługują dostępu asynchronicznego o wysokiej wydajności.
DbContext.Add i DbSet<TEntity>.Add zwykle nie uzyskują dostępu do bazy danych, ponieważ te metody z natury po prostu zaczynają śledzić jednostki. Jednak niektóre formy generowania wartości mogą uzyskiwać dostęp do bazy danych w celu wygenerowania wartości klucza. Jedynym generatorem wartości, który to robi i jest dostarczany z programem EF Core, jest HiLoValueGenerator<TValue>. Używanie tego generatora jest nietypowe; nigdy nie jest ona domyślnie skonfigurowana. Oznacza to, że zdecydowana większość aplikacji powinna używać elementów Add
, a nie AddAsync
.
Inne podobne metody, takie jak Update
, Attach
i Remove
nie mają przeciążeń asynchronicznych, ponieważ nigdy nie generują nowych wartości kluczy, a tym samym nigdy nie muszą uzyskiwać dostępu do bazy danych.
AddRange
, UpdateRange
, AttachRange
i RemoveRange
DbSet<TEntity>i DbContext podaj alternatywne wersje Add
, , Attach
Update
iRemove
, które akceptują wiele wystąpień w jednym wywołaniu. Te metody to AddRange, UpdateRange, AttachRangei RemoveRange odpowiednio.
Te metody są udostępniane jako wygoda. Użycie metody "zakres" ma tę samą funkcjonalność co wiele wywołań do równoważnej metody innej niż zakres. Nie ma znaczącej różnicy w wydajności między dwoma podejściami.
Uwaga
Różni się to od ef6, gdzie AddRange
i Add
oba automatycznie wywoływane DetectChanges
metody , ale wywołanie wiele razy powodowało wywołanie Add
funkcji DetectChanges wielokrotnie zamiast raz. Stało się AddRange
to bardziej wydajne w ef6. W programie EF Core żadna z tych metod nie wywołuje automatycznie DetectChanges
metody .
DbContext a metody DbSet
Wiele metod, w tym Add
, Update
, Attach
i Remove
, ma implementacje zarówno , jak DbSet<TEntity> i DbContext. Te metody mają dokładnie takie samo zachowanie dla normalnych typów jednostek. Dzieje się tak, ponieważ typ CLR jednostki jest mapowany na jeden i tylko jeden typ jednostki w modelu EF Core. W związku z tym typ CLR w pełni definiuje miejsce, w którym mieści się jednostka w modelu, a więc zestaw dbSet do użycia można określić niejawnie.
Wyjątkiem od tej reguły jest użycie typów jednostek typu współużytkowanego, które są używane głównie dla jednostek sprzężenia wiele-do-wielu. W przypadku używania typu jednostki typu współużytkowanego należy najpierw utworzyć zestaw DbSet dla używanego typu modelu EF Core. Metody takie jak Add
, Update
, Attach
i Remove
mogą być następnie używane w zestawie dbSet bez żadnych niejednoznaczności co do używanego typu modelu EF Core.
Typy jednostek typu współużytkowanego są domyślnie używane dla jednostek sprzężenia w relacjach wiele-do-wielu. Typ jednostki typu współużytkowanego można również jawnie skonfigurować do użycia w relacji wiele-do-wielu. Na przykład poniższy kod konfiguruje Dictionary<string, int>
się jako typ jednostki sprzężenia:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.SharedTypeEntity<Dictionary<string, int>>(
"PostTag",
b =>
{
b.IndexerProperty<int>("TagId");
b.IndexerProperty<int>("PostId");
});
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, int>>(
"PostTag",
j => j.HasOne<Tag>().WithMany(),
j => j.HasOne<Post>().WithMany());
}
Zmiana kluczy obcych i nawigacji pokazuje, jak skojarzyć dwie jednostki przez śledzenie nowego wystąpienia jednostki sprzężenia. Poniższy kod dotyczy Dictionary<string, int>
typu jednostki typu współużytkowanego używanego dla jednostki sprzężenia:
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);
var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Zwróć uwagę, że DbContext.Set<TEntity>(String) służy do tworzenia elementu DbSet dla PostTag
typu jednostki. Tego zestawu dbSet można następnie użyć do wywołania Add
z nowym wystąpieniem jednostki join.
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 określonego typu jednostki sprzężenia, chyba że został jawnie skonfigurowany tak, jak to ma miejsce Dictionary<string, int>
w powyższym kodzie.
Dostęp do właściwości a pola
Dostęp do właściwości jednostki domyślnie używa pola zapasowego właściwości. Jest to wydajne i pozwala uniknąć wyzwalania skutków ubocznych z wywoływania metody pobierających właściwości i ustawiających. Na przykład w ten sposób można uniknąć wyzwalania nieskończonych pętli. Aby uzyskać więcej informacji na temat konfigurowania pól zapasowych w modelu, zobacz Tworzenie kopii zapasowych pól.
Czasami może być pożądane, aby program EF Core wygenerował skutki uboczne podczas modyfikowania wartości właściwości. Na przykład gdy powiązanie danych z jednostkami, ustawienie właściwości może generować powiadomienia do INTERFEJSu UŻYTKOWNIKA, które nie mają miejsce podczas bezpośredniego ustawiania pola. Można to osiągnąć przez zmianę PropertyAccessMode elementu dla:
- Wszystkie typy jednostek w modelu przy użyciu ModelBuilder.UsePropertyAccessMode
- Wszystkie właściwości i nawigacje określonego typu jednostki przy użyciu EntityTypeBuilder<TEntity>.UsePropertyAccessMode
- Określona właściwość używająca PropertyBuilder.UsePropertyAccessMode
- Określona nawigacja przy użyciu NavigationBuilder.UsePropertyAccessMode
Tryby Field
dostępu do właściwości i PreferField
spowodują, że program EF Core będzie uzyskiwać dostęp do wartości właściwości za pośrednictwem pola zapasowego. Podobnie i PreferProperty
spowoduje, Property
że program EF Core będzie uzyskiwać dostęp do wartości właściwości za pośrednictwem metody pobierania i ustawiania.
Jeśli Field
lub Property
są używane, a program EF Core nie może uzyskać dostępu do wartości za pośrednictwem odpowiednio metody getter/setter właściwości lub pola, program EF Core zgłosi wyjątek. Gwarantuje to, że program EF Core zawsze korzysta z dostępu do pola/właściwości, gdy uważasz, że tak jest.
Z drugiej strony tryby i PreferProperty
powrócą do używania odpowiednio pola właściwości lub pola zapasowego, PreferField
jeśli nie jest możliwe użycie preferowanego dostępu. Wartość domyślna to PreferField
. Oznacza to, że program EF Core będzie używać pól zawsze wtedy, gdy może, ale nie zakończy się niepowodzeniem, jeśli właściwość musi być uzyskiwana za pośrednictwem metody pobierania lub ustawiania.
FieldDuringConstruction
i PreferFieldDuringConstruction
skonfiguruj program EF Core do używania pól zapasowych tylko podczas tworzenia wystąpień jednostek. Umożliwia to wykonywanie zapytań bez efektów ubocznych getter i setter, podczas gdy późniejsze zmiany właściwości przez program EF Core spowodują te skutki uboczne.
Różne tryby dostępu do właściwości zostały podsumowane w poniższej tabeli:
PropertyAccessMode | Preferencji | Preferencje tworzenia jednostek | Temat rezerwowy | Rezerwowe tworzenie jednostek |
---|---|---|---|---|
Field |
Pole | Pole | Zgłasza | Zgłasza |
Property |
Właściwości | Właściwości | Zgłasza | Zgłasza |
PreferField |
Pole | Pole | Właściwości | Właściwości |
PreferProperty |
Właściwości | Właściwości | Pole | Pole |
FieldDuringConstruction |
Właściwości | Pole | Pole | Zgłasza |
PreferFieldDuringConstruction |
Właściwości | Pole | Pole | Właściwości |
Wartości tymczasowe
Program EF Core tworzy tymczasowe wartości kluczy podczas śledzenia nowych jednostek, które będą miały rzeczywiste wartości klucza generowane przez bazę danych podczas wywoływanej funkcji SaveChanges. Zobacz Change Tracking in EF Core (Śledzenie zmian w programie EF Core ), aby zapoznać się z omówieniem sposobu użycia tych wartości tymczasowych.
Uzyskiwanie dostępu do wartości tymczasowych
Wartości tymczasowe są przechowywane w monitorze zmian i nie są ustawiane bezpośrednio na wystąpienia jednostek. Te wartości tymczasowe są jednak widoczne w przypadku używania różnych mechanizmów uzyskiwania dostępu do śledzonych jednostek. Na przykład następujący kod uzyskuje dostęp do wartości tymczasowej przy użyciu polecenia EntityEntry.CurrentValues:
using var context = new BlogsContext();
var blog = new Blog { Name = ".NET Blog" };
context.Add(blog);
Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");
Dane wyjściowe z tego kodu to:
Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643
PropertyEntry.IsTemporary można użyć do sprawdzania wartości tymczasowych.
Manipulowanie wartościami tymczasowymi
Czasami warto jawnie pracować z wartościami tymczasowymi. Na przykład kolekcja nowych jednostek może zostać utworzona na kliencie internetowym, a następnie serializowana z powrotem na serwerze. Wartości klucza obcego to jeden ze sposobów konfigurowania relacji między tymi jednostkami. Poniższy kod używa tego podejścia, aby skojarzyć graf nowych jednostek według klucza obcego, jednocześnie pozwalając na generowanie rzeczywistych wartości kluczy podczas wywoływana funkcji SaveChanges.
var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };
var posts = new List<Post>
{
new Post
{
Id = -1,
BlogId = -1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = -2,
BlogId = -2,
Title = "Disassembly improvements for optimized managed debugging",
Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
}
};
using var context = new BlogsContext();
foreach (var blog in blogs)
{
context.Add(blog).Property(e => e.Id).IsTemporary = true;
}
foreach (var post in posts)
{
context.Add(post).Property(e => e.Id).IsTemporary = true;
}
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Zwróć uwagę, że:
- Liczby ujemne są używane jako wartości klucza tymczasowego; nie jest to wymagane, ale jest wspólną konwencją zapobiegania kluczowym starciu.
- Właściwość
Post.BlogId
FK ma przypisaną tę samą wartość ujemną co klucz PK skojarzonego bloga. - Wartości PK są oznaczone jako tymczasowe przez ustawienie IsTemporary po śledzeniu każdej jednostki. Jest to konieczne, ponieważ przyjmuje się, że każda wartość klucza dostarczana przez aplikację jest rzeczywistą wartością klucza.
Patrząc na widok debugowania śledzenia zmian przed wywołaniem polecenia SaveChanges pokazuje, że wartości PK są oznaczone jako tymczasowe, a wpisy są skojarzone z poprawnymi blogami, w tym poprawkami nawigacji:
Blog {Id: -2} Added
Id: -2 PK Temporary
Name: 'Visual Studio Blog'
Posts: [{Id: -2}]
Blog {Id: -1} Added
Id: -1 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -1}]
Post {Id: -2} Added
Id: -2 PK Temporary
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: -1} Added
Id: -1 PK Temporary
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}
Po wywołaniu metody SaveChangeste wartości tymczasowe zostały zastąpione rzeczywistymi wartościami wygenerowanymi przez bazę danych:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Posts: [{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: 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: []
Praca z wartościami domyślnymi
Program EF Core umożliwia właściwości uzyskanie jej wartości domyślnej z bazy danych, gdy SaveChanges jest wywoływana. Podobnie jak w przypadku wygenerowanych wartości klucza, program EF Core będzie używać wartości domyślnej z bazy danych tylko wtedy, gdy żadna wartość nie została jawnie ustawiona. Rozważmy na przykład następujący typ jednostki:
public class Token
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime ValidFrom { get; set; }
}
Właściwość ValidFrom
jest skonfigurowana do pobierania wartości domyślnej z bazy danych:
modelBuilder
.Entity<Token>()
.Property(e => e.ValidFrom)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
Podczas wstawiania jednostki tego typu program EF Core pozwoli bazie danych wygenerować wartość, chyba że zamiast tego ustawiono jawną wartość. Przykład:
using var context = new BlogsContext();
context.AddRange(
new Token { Name = "A" },
new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Patrząc na widok debugowania śledzenia zmian pokazuje, że pierwszy token wygenerowany ValidFrom
przez bazę danych, podczas gdy drugi token użył jawnie ustawionej wartości:
Token {Id: 1} Unchanged
Id: 1 PK
Name: 'A'
ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
Id: 2 PK
Name: 'B'
ValidFrom: '11/11/1111 11:11:11 AM'
Uwaga
Użycie wartości domyślnych bazy danych wymaga, aby kolumna bazy danych ma skonfigurowane ograniczenie wartości domyślnej. Jest to wykonywane automatycznie przez migracje platformy EF Core w przypadku korzystania z programu HasDefaultValueSql lub HasDefaultValue. Pamiętaj, aby utworzyć domyślne ograniczenie w kolumnie w inny sposób, gdy nie korzystasz z migracji platformy EF Core.
Używanie właściwości dopuszczanych do wartości null
Program EF Core może określić, czy właściwość została ustawiona, porównując wartość właściwości z wartością domyślną CLR dla tego typu. Działa to dobrze w większości przypadków, ale oznacza to, że domyślne ustawienie CLR nie może być jawnie wstawione do bazy danych. Rozważmy na przykład jednostkę z właściwością całkowitą:
public class Foo1
{
public int Id { get; set; }
public int Count { get; set; }
}
Jeśli ta właściwość jest skonfigurowana tak, aby miała domyślną wartość -1 bazy danych:
modelBuilder
.Entity<Foo1>()
.Property(e => e.Count)
.HasDefaultValue(-1);
Intencją jest to, że wartość domyślna -1 będzie używana zawsze, gdy nie ustawiono jawnej wartości. Jednak ustawienie wartości na 0 (wartość domyślna CLR dla liczb całkowitych) jest nie do odróżnienia od platformy EF Core, nie ustawiając żadnej wartości, oznacza to, że nie można wstawić wartości 0 dla tej właściwości. Przykład:
using var context = new BlogsContext();
var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);
Zwróć uwagę, że wystąpienie, w którym Count
jawnie ustawiono wartość 0, nadal pobiera wartość domyślną z bazy danych, co nie jest zamierzone. Łatwym sposobem radzenia sobie z tym jest uczynienie Count
właściwością dopuszczaną do wartości null:
public class Foo2
{
public int Id { get; set; }
public int? Count { get; set; }
}
Spowoduje to, że wartość domyślna CLR ma wartość null zamiast wartości 0, co oznacza, że wartość 0 zostanie wstawiona po jawnym ustawieniu:
using var context = new BlogsContext();
var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Używanie pól kopii zapasowych dopuszczających wartość null
Problem z tworzeniem właściwości dopuszczającą wartość null, która może nie być koncepcyjnie dopuszczana do wartości null w modelu domeny. Wymuszanie wartości null właściwości w związku z tym narusza model.
Właściwość może być pozostawiona bez wartości null, a pole kopii zapasowej może mieć wartość null. Przykład:
public class Foo3
{
public int Id { get; set; }
private int? _count;
public int Count
{
get => _count ?? -1;
set => _count = value;
}
}
Umożliwia to wstawienie wartości domyślnej clR (0), jeśli właściwość jest jawnie ustawiona na wartość 0, a jednocześnie nie musi ujawniać właściwości jako dopuszczającej wartość null w modelu domeny. Przykład:
using var context = new BlogsContext();
var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Pola kopii zapasowej dopuszczające wartość null dla właściwości logicznych
Ten wzorzec jest szczególnie przydatny w przypadku używania właściwości logicznych z wartościami domyślnymi wygenerowanymi przez magazyn. Ponieważ wartość domyślna clR dla bool
parametru to "false", oznacza to, że "false" nie można wstawić jawnie przy użyciu normalnego wzorca. Rozważmy na przykład User
typ jednostki:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
private bool? _isAuthorized;
public bool IsAuthorized
{
get => _isAuthorized ?? true;
set => _isAuthorized = value;
}
}
Właściwość IsAuthorized
jest skonfigurowana z wartością domyślną bazy danych "true":
modelBuilder
.Entity<User>()
.Property(e => e.IsAuthorized)
.HasDefaultValue(true);
Właściwość IsAuthorized
można ustawić na wartość "true" lub "false" jawnie przed wstawieniem lub można pozostawić bez ustawienia, w którym przypadku będzie używana domyślna baza danych:
using var context = new BlogsContext();
var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!
context.AddRange(userA, userB, userC);
await context.SaveChangesAsync();
Dane wyjściowe z funkcji SaveChanges podczas korzystania z biblioteki SQLite pokazują, że domyślna baza danych jest używana dla komputerów Mac, podczas gdy jawne wartości są ustawiane dla Alice i Baxter:
-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Tylko wartości domyślne schematu
Czasami warto mieć wartości domyślne w schemacie bazy danych utworzonym przez migracje platformy EF Core bez programu EF Core, kiedykolwiek używając tych wartości dla wstawiania. Można to osiągnąć, konfigurując właściwość na PropertyBuilder.ValueGeneratedNever przykład:
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();