Udostępnij przez


Dodatkowe funkcje śledzenia zmian

W tym dokumencie opisano różne funkcje i scenariusze dotyczące śledzenia zmian.

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.

Add kontra 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 zapewniają alternatywne wersje Add, Update, Attach i Remove, 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 typu zakres ma tę samą funkcjonalność co wielokrotne wywołania równoważnej metody bez zakresu. Nie ma znaczącej różnicy w wydajności między dwoma podejściami.

Uwaga / Notatka

Różni się to od EF6, gdzie zarówno AddRange, jak i Add automatycznie wywoływały DetectChanges, ale wielokrotne wywoływanie Add powodowało, że funkcja DetectChanges była uruchamiana wielokrotnie zamiast raz. Sprawiło to, że AddRange stało się bardziej wydajne w EF6. W programie EF Core żadna z tych metod nie wywołuje automatycznie DetectChanges.

DbContext a metody DbSet

Wiele metod, w tym Add, Update, Attach i Remove, ma implementacje zarówno na DbSet<TEntity>, jak i na 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 jednostki w modelu, a więc DbSet do użycia można określić implicitnie.

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 na DbSet bez żadnych niejednoznaczności co do używanego typu modelu EF Core.

Typy encji współdzielonych są domyślnie używane dla encji łączących 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> jako typ jednostki połączeniowej.

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 realizuje to dla Dictionary<string, int> współdzielonego typu jednostki 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, jak to zostało zrobione dla 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ąć wywoływania skutków ubocznych związanych z użyciem akcesorów i mutatorów właściwości. Na przykład, lazy-loading może w ten sposób unikać wyzwalania nieskończonych pętli. Zobacz Pola Pomocnicze po więcej informacji na temat konfigurowania pól pomocniczych w modelu.

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ązujesz dane z jednostkami, ustawienie właściwości może generować powiadomienia do interfejsu użytkownika, które nie występują podczas bezpośredniego ustawiania pola. Można to osiągnąć przez zmianę PropertyAccessMode na:

Tryby dostępu do właściwości Field i PreferField spowodują, że EF Core będzie uzyskiwać dostęp do wartości właściwości poprzez pole pomocnicze. Podobnie, Property i PreferProperty spowodują, że EF Core uzyska dostęp do wartości właściwości za pośrednictwem jej gettera i settera.

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. Zapewnia to, że program EF Core zawsze korzysta z dostępu do pola/właściwości w momencie, gdy się tego spodziewasz.

Z drugiej strony, tryby PreferField i PreferProperty będą się opierać na używaniu odpowiednio właściwości lub pola zapasowego, jeśli nie jest możliwe użycie preferowanego dostępu. PreferField jest wartością domyślną. 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 związanych z używaniem getterów i setterów, podczas gdy późniejsze zmiany właściwości przez EF Core spowodują te efekty uboczne.

Różne tryby dostępu do właściwości zostały podsumowane w poniższej tabeli:

TrybDostępuDoWłaściwości Preferencja Preferencje tworzenia jednostek Rezerwowej Awaryjne tworzenie podmiotów
Field (No changes needed) (No changes needed) Zgłasza Zgłasza
Property Majątek Majątek Zgłasza Zgłasza
PreferField (No changes needed) (No changes needed) Majątek Majątek
PreferProperty Majątek Majątek (No changes needed) (No changes needed)
FieldDuringConstruction Majątek (No changes needed) (No changes needed) Zgłasza
PreferFieldDuringConstruction Majątek (No changes needed) (No changes needed) Majątek

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 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 tymczasowe wartości kluczy; nie jest to wymagane, ale jest to powszechna konwencja zapobiegająca kolizjom kluczy.
  • Właściwość Post.BlogId FK ma przypisaną tę samą ujemną wartość co właściwość PK skojarzonego bloga.
  • Wartości PK są oznaczone jako tymczasowe przez ustawienie IsTemporary po tym, jak każda jednostka zostanie śledzona. 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, łącznie z poprawką 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

EF Core pozwala właściwości pobrać wartość domyślną z bazy danych, gdy SaveChanges jest wywoływane. 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 widać, że pierwszy token został wygenerowany przez bazę danych, podczas gdy drugi token użył wartości ustawionej jawnie.

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

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 EF Core podczas korzystania z programu HasDefaultValueSql lub HasDefaultValue. Upewnij się, że utworzysz domyślne ograniczenie w kolumnie w inny sposób, jeśli nie używasz migracji EF Core.

Używanie właściwości z możliwością przypisania 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) dla EF Core jest nie do odróżnienia od sytuacji, w której nie ustawiono żadnej wartości, co oznacza, ż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 uczynieniem właściwości dopuszczającą zmienną do wartości null, co może nie być koncepcyjnie zasadnym w modelu domeny. Wymuszanie, aby cecha była podatna na wartość null, w konsekwencji narusza model.

Właściwość może być pozostawiona jako nieprzyjmująca wartości null, a tylko pole pomocnicze może przyjmować 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 0, jednocześnie nie wymagając ujawnienia 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 wspierające dopuszczające wartość null dla właściwości typu bool

Ten schemat jest szczególnie przydatny w przypadku używania właściwości logicznych z wartościami domyślnymi wygenerowanymi przez magazyn. Ponieważ domyślną wartością CLR dla bool jest "false", oznacza to, że "false" nie można wstawić jawnie za pomocą 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 posiadać wartości domyślne w schemacie bazy danych utworzonym przez migracje EF Core, bez używania tych wartości przez EF Core do wstawień. 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();