Udostępnij za pośrednictwem


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, Attachi 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, AttachRangei RemoveRange

DbSet<TEntity>i DbContext podaj alternatywne wersje Add, , AttachUpdateiRemove, 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 DetectChangesmetody , 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 DetectChangesmetody .

DbContext a metody DbSet

Wiele metod, w tym Add, Update, Attachi 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, Attachi 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:

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