Udostępnij za pośrednictwem


Jawne śledzenie jednostek

Każde wystąpienie obiektu DbContext śledzi zmiany wprowadzane w jednostkach. Te śledzone jednostki sterują z kolei zmianami w bazie danych przy wywoływaniu metody SaveChanges.

Śledzenie zmian platformy Entity Framework Core (EF Core) działa najlepiej, gdy to samo DbContext wystąpienie jest używane do wykonywania zapytań dotyczących jednostek i aktualizowania ich przez wywołanie metody SaveChanges. Jest tak dlatego, że rozwiązanie EF Core automatycznie śledzi stan jednostek określonych w zapytaniu, a następnie przy wywołaniu metody SaveChanges wykrywa wszelkie zmiany wprowadzone w tych jednostkach. Takie podejście zostało omówione w rozwiązaniu Change Tracking w programie EF Core.

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.

Napiwek

Dla uproszczenia w tym dokumencie są używane metody synchroniczne, takie jak SaveChanges, a nie ich odpowiedniki asynchroniczne, takie jak SaveChangesAsync. Alternatywnie można wywoływać metodę asynchroniczną i oczekiwać na nią, chyba że określono inaczej.

Wprowadzenie

Jednostki mogą być jawnie "dołączone" do DbContext takiego, że kontekst następnie śledzi te jednostki. Jest to szczególnie przydatne w następujących przypadkach:

  1. Tworzenie nowych jednostek, które zostaną wstawione do bazy danych.
  2. Ponowne dołączanie odłączonych jednostek, które wcześniej były odpytywane przez inne wystąpienie dbContext.

Pierwsza z nich będzie potrzebna przez większość aplikacji i jest obsługiwana DbContext.Add głównie przez metody.

Drugi jest wymagany tylko przez aplikacje, które zmieniają jednostki lub ich relacje , gdy jednostki nie są śledzone. Na przykład aplikacja internetowa może wysyłać jednostki do klienta internetowego, w którym użytkownik wprowadza zmiany i wysyła jednostki z powrotem. Te jednostki są określane jako "rozłączone", ponieważ były one pierwotnie wysyłane z obiektu DbContext, ale następnie zostały odłączone od tego kontekstu podczas wysyłania do klienta.

Aplikacja internetowa musi teraz ponownie dołączyć te jednostki, aby były ponownie śledzone i wskazywać wprowadzone zmiany, które SaveChanges mogą wprowadzać odpowiednie aktualizacje bazy danych. Jest to obsługiwane głównie przez DbContext.Attach metody i DbContext.Update .

Napiwek

Dołączanie jednostek do tego samego wystąpienia dbContext, z którego zostały one zapytane, nie powinno być zwykle potrzebne. Nie wykonuj rutynowo zapytania bez śledzenia, a następnie dołączaj zwrócone jednostki do tego samego kontekstu. Będzie to wolniejsze niż użycie zapytania śledzenia i może również spowodować problemy, takie jak brakujące wartości właściwości w tle, co utrudnia uzyskanie prawidłowego dostępu.

Generowane a jawne wartości klucza

Domyślnie właściwości klucza całkowitego i identyfikatora GUID są skonfigurowane do używania automatycznie generowanych wartości kluczy. Ma to główną zaletę śledzenia zmian: nieustawiona wartość klucza wskazuje, że jednostka jest "nowa". Przez "nowy" oznaczamy, że nie został on jeszcze wstawiony do bazy danych.

Dwa modele są używane w poniższych sekcjach. Pierwszy jest skonfigurowany do nieużytowania wygenerowanych wartości klucza:

public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    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; }
}

Niegenerowane (tj. jawnie ustawione) wartości kluczy są wyświetlane jako pierwsze w każdym przykładzie, ponieważ wszystko jest bardzo jawne i łatwe do naśladowania. Następnie następuje przykład, w którym są używane wygenerowane wartości klucza:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

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; }
}

Zwróć uwagę, że właściwości klucza w tym modelu nie wymagają dodatkowej konfiguracji, ponieważ użycie wygenerowanych wartości klucza jest wartością domyślną dla prostych kluczy całkowitych.

Wstawianie nowych jednostek

Jawne wartości klucza

Jednostka musi być śledzona w Added stanie, który ma zostać wstawiony przez SaveChangeselement . Jednostki są zwykle umieszczane w stanie Dodano przez wywołanie jednej z DbContext.Addmetod , DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsynclub równoważnych w elemencie DbSet<TEntity>.

Napiwek

Wszystkie te metody działają w taki sam sposób w kontekście śledzenia zmian. Aby uzyskać więcej informacji, zobacz Dodatkowe funkcje śledzenia zmian.

Aby na przykład rozpocząć śledzenie nowego bloga:

context.Add(
    new Blog { Id = 1, Name = ".NET Blog", });

Inspekcja widoku debugowania monitora zmian po tym wywołaniu pokazuje, że kontekst śledzi nową jednostkę w Added stanie:

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Jednak metody Dodaj nie działają tylko na pojedynczej jednostce. Oni rzeczywiście rozpocząć śledzenie całego grafu powiązanych jednostek, umieszczając je wszystkie w Added stanie. Aby na przykład wstawić nowy blog i skojarzone nowe wpisy:

context.Add(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Kontekst śledzi teraz wszystkie te jednostki jako Added:

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
  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}
Post {Id: 2} Added
  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}

Zwróć uwagę, że jawne wartości zostały ustawione dla Id właściwości klucza w powyższych przykładach. Jest to spowodowane tym, że model w tym miejscu został skonfigurowany do używania jawnie ustawionych wartości kluczy, a nie automatycznie generowanych wartości kluczy. Jeśli nie używasz wygenerowanych kluczy, przed wywołaniem metody Addnależy jawnie ustawić właściwości klucza . Te wartości klucza są następnie wstawiane po wywołaniu funkcji SaveChanges. Na przykład w przypadku korzystania z biblioteki SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);

Wszystkie te jednostki są śledzone w stanie po zakończeniu Unchanged zapisywania zmian, ponieważ te jednostki istnieją teraz w bazie danych:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
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}

Wygenerowane wartości klucza

Jak wspomniano powyżej, właściwości klucza całkowitego i identyfikatora GUID są domyślnie skonfigurowane do używania automatycznie generowanych wartości kluczy. Oznacza to, że aplikacja nie może jawnie ustawić żadnej wartości klucza. Aby na przykład wstawić nowy blog i wpisów z wygenerowanymi wartościami klucza:

context.Add(
    new Blog
    {
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                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
            {
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Podobnie jak w przypadku jawnych wartości klucza, kontekst śledzi teraz wszystkie te jednostki jako Added:

Blog {Id: -2147482644} Added
  Id: -2147482644 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
  Id: -2147482637 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: -2147482644}

Zwróć uwagę, że w tym przypadku wartości klucza tymczasowego zostały wygenerowane dla każdej jednostki. Te wartości są używane przez program EF Core do momentu wywołania funkcji SaveChanges, w którym rzeczywiste wartości klucza są odczytywane z bazy danych. Na przykład w przypadku korzystania z biblioteki SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Po zakończeniu operacji SaveChanges wszystkie jednostki zostały zaktualizowane przy użyciu ich rzeczywistych wartości klucza i są śledzone w Unchanged stanie, ponieważ są teraz zgodne ze stanem w bazie danych:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
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}

Jest to dokładnie taki sam stan końcowy jak w poprzednim przykładzie, który używał jawnych wartości kluczy.

Napiwek

Jawna wartość klucza może być nadal ustawiana nawet w przypadku używania wygenerowanych wartości klucza. Następnie program EF Core podejmie próbę wstawienia przy użyciu tej wartości klucza. Niektóre konfiguracje bazy danych, w tym program SQL Server z kolumnami Tożsamości, nie obsługują takich wstawiania i zgłaszają wyjątek (zobacz te dokumenty, aby obejść ten problem).

Dołączanie istniejących jednostek

Jawne wartości klucza

Jednostki zwracane z zapytań są śledzone w Unchanged stanie . Stan Unchanged oznacza, że jednostka nie została zmodyfikowana, ponieważ została odpytowana. Jednostka odłączona, być może zwrócona z klienta internetowego w żądaniu HTTP, może zostać umieszczona w tym stanie przy użyciu DbContext.Attachmetod , lub DbContext.AttachRangerównoważnych metod w elemencie DbSet<TEntity>. Aby na przykład rozpocząć śledzenie istniejącego bloga:

context.Attach(
    new Blog { Id = 1, Name = ".NET Blog", });

Uwaga

W tym przykładzie jednostki są tworzone jawnie dla new uproszczenia. Zwykle wystąpienia jednostek pochodzą z innego źródła, takiego jak deserializowane z klienta lub tworzone na podstawie danych w poście HTTP.

Inspekcja widoku debugowania monitora zmian po tym wywołaniu pokazuje, że jednostka jest śledzona w Unchanged stanie:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Podobnie jak Add, Attach faktycznie ustawia cały wykres połączonych jednostek ze stanem Unchanged . Aby na przykład dołączyć istniejący blog i skojarzone istniejące wpisy:

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Kontekst śledzi teraz wszystkie te jednostki jako Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
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}

Wywołanie metody SaveChanges w tym momencie nie będzie miało żadnego wpływu. Wszystkie jednostki są oznaczone jako Unchanged, więc nie ma nic do zaktualizowania w bazie danych.

Wygenerowane wartości klucza

Jak wspomniano powyżej, właściwości klucza całkowitego i identyfikatora GUID są domyślnie skonfigurowane do używania automatycznie generowanych wartości kluczy. Ma to główną zaletę podczas pracy z odłączonymi jednostkami: niezwiązana wartość klucza wskazuje, że jednostka nie została jeszcze wstawiona do bazy danych. Dzięki temu monitor zmian automatycznie wykrywa nowe jednostki i umieszcza je w Added stanie. Rozważ na przykład dołączenie tego grafu w blogu i wpisów:

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

W blogu znajduje się wartość klucza 1 wskazująca, że już istnieje w bazie danych. Dwa wpisy mają również ustawione wartości klucza, ale trzeci nie. Platforma EF Core zobaczy tę wartość klucza jako 0— domyślną wartość clR dla liczby całkowitej. Spowoduje to oznaczenie nowej jednostki przez program EF Core jako Added zamiast Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {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}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'

Wywołanie metody SaveChanges w tym momencie nie powoduje niczego z Unchanged jednostkami, ale wstawia nową jednostkę do bazy danych. Na przykład w przypadku korzystania z biblioteki SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Należy zauważyć, że przy wygenerowanych wartościach kluczy program EF Core może automatycznie odróżnić nowe jednostki od istniejących jednostek w rozłączonym grafie. W skrócie podczas korzystania z wygenerowanych kluczy program EF Core zawsze wstawia jednostkę, gdy ta jednostka nie ma ustawionej wartości klucza.

Aktualizowanie istniejących jednostek

Jawne wartości klucza

DbContext.Update, DbContext.UpdateRangei równoważne metody DbSet<TEntity> zachowują się dokładnie tak, jak Attach metody opisane powyżej, z tą różnicą, że jednostki są umieszczane w Modified stanie zamiast Unchanged . Aby na przykład rozpocząć śledzenie istniejącego bloga jako Modified:

context.Update(
    new Blog { Id = 1, Name = ".NET Blog", });

Inspekcja widoku debugowania śledzenia zmian po tym wywołaniu pokazuje, że kontekst śledzi tę jednostkę w Modified stanie:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: []

Podobnie jak w przypadku i , faktycznie oznacza cały wykres powiązanych jednostek jako Modified. Update AttachAdd Aby na przykład dołączyć istniejący blog i skojarzone istniejące wpisy jako Modified:

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Kontekst śledzi teraz wszystkie te jednostki jako Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Wywołanie metody SaveChanges w tym momencie spowoduje wysłanie aktualizacji do bazy danych dla wszystkich tych jednostek. Na przykład w przypadku korzystania z biblioteki SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

Wygenerowane wartości klucza

Podobnie jak w przypadku Attach, wygenerowane wartości klucza mają taką samą główną korzyść dla Update: niezstawiona wartość klucza wskazuje, że jednostka jest nowa i nie została jeszcze wstawiona do bazy danych. Podobnie jak w przypadku Attachelementu , umożliwia to usłudze DbContext automatyczne wykrywanie nowych jednostek i umieszczanie ich w Added stanie. Rozważ na przykład wywołanie Update za pomocą tego grafu bloga i wpisów:

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

Tak jak w przykładzie Attach , wpis bez wartości klucza jest wykrywany jako nowy i ustawiony na Added stan. Pozostałe jednostki są oznaczone jako Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
  Id: -2147482633 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Wywołanie SaveChanges w tym momencie spowoduje wysłanie aktualizacji do bazy danych dla wszystkich istniejących jednostek, podczas gdy nowa jednostka zostanie wstawiona. Na przykład w przypadku korzystania z biblioteki SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Jest to bardzo prosty sposób generowania aktualizacji i wstawiania z odłączonego grafu. Jednak powoduje to, że aktualizacje lub wstawki są wysyłane do bazy danych dla każdej właściwości każdej śledzonej jednostki, nawet jeśli niektóre wartości właściwości mogły nie zostać zmienione. Nie obawiaj się tego; w przypadku wielu aplikacji z małymi grafami może to być łatwy i pragmatyczny sposób generowania aktualizacji. Oznacza to, że inne bardziej złożone wzorce mogą czasami powodować bardziej wydajne aktualizacje, zgodnie z opisem w temacie Identity Resolution in EF Core (Rozpoznawanie tożsamości w programie EF Core).

Usuwanie istniejących jednostek

Aby jednostka została usunięta przez polecenie SaveChanges, musi być śledzona w Deleted stanie . Jednostki są zwykle umieszczane w Deleted stanie przez wywołanie jednej z DbContext.Removemetod , DbContext.RemoveRangelub równoważnych w pliku DbSet<TEntity>. Aby na przykład oznaczyć istniejący wpis jako Deleted:

context.Remove(
    new Post { Id = 2 });

Inspekcja widoku debugowania śledzenia zmian po tym wywołaniu pokazuje, że kontekst śledzi jednostkę w Deleted stanie:

Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: <null> FK
  Content: <null>
  Title: <null>
  Blog: <null>

Ta jednostka zostanie usunięta po wywołaniu metody SaveChanges. Na przykład w przypadku korzystania z biblioteki SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Po zakończeniu operacji SaveChanges usunięta jednostka jest odłączona od obiektu DbContext, ponieważ nie istnieje już w bazie danych. W związku z tym widok debugowania jest pusty, ponieważ nie są śledzone żadne jednostki.

Usuwanie jednostek zależnych/podrzędnych

Usuwanie jednostek zależnych/podrzędnych z grafu jest prostsze niż usuwanie jednostek głównych/nadrzędnych. Aby uzyskać więcej informacji, zobacz następną sekcję i Zmienianie kluczy obcych i nawigacji.

Nietypowe wywołanie Remove jednostki utworzonej za pomocą newpolecenia . Ponadto w przeciwieństwie do Add, Attach i Updaterzadko wywołuje Remove jednostkę, która nie jest jeszcze śledzona w Unchanged stanie lub Modified . Zamiast tego typowe jest śledzenie pojedynczej jednostki lub grafu powiązanych jednostek, a następnie wywoływanie Remove jednostek, które powinny zostać usunięte. Ten wykres śledzonych jednostek jest zwykle tworzony przez:

  1. Uruchamianie zapytania dla jednostek
  2. Attach Używanie metod lub Update na grafie odłączonych jednostek, zgodnie z opisem w poprzednich sekcjach.

Na przykład kod w poprzedniej sekcji jest bardziej prawdopodobne, aby uzyskać wpis od klienta, a następnie wykonać podobne czynności:

context.Attach(post);
context.Remove(post);

Zachowuje się to dokładnie tak samo jak w poprzednim przykładzie, ponieważ wywołanie Remove nieśledzenia jednostki powoduje, że najpierw zostanie dołączony, a następnie oznaczony jako Deleted.

W bardziej realistycznych przykładach wykres jednostek jest najpierw dołączony, a następnie niektóre z tych jednostek są oznaczone jako usunięte. Przykład:

// Attach a blog and associated posts
context.Attach(blog);

// Mark one post as Deleted
context.Remove(blog.Posts[1]);

Wszystkie jednostki są oznaczone jako Unchanged, z wyjątkiem tej, na której Remove została wywołana:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
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: {Id: 1}

Ta jednostka zostanie usunięta po wywołaniu metody SaveChanges. Na przykład w przypadku korzystania z biblioteki SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Po zakończeniu operacji SaveChanges usunięta jednostka jest odłączona od obiektu DbContext, ponieważ nie istnieje już w bazie danych. Inne jednostki pozostają w Unchanged stanie:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  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}

Usuwanie jednostek podmiotów zabezpieczeń/nadrzędnych

Każda relacja łącząca dwa typy jednostek ma główną lub nadrzędną końcu oraz zależny lub podrzędny koniec. Jednostka zależna/podrzędna jest jednostką z właściwością klucza obcego. W relacji jeden do wielu podmiot zabezpieczeń/nadrzędny znajduje się po stronie "jeden", a element zależny/podrzędny znajduje się po stronie "wiele". Aby uzyskać więcej informacji, zobacz Relacje .

W poprzednich przykładach usuwaliśmy wpis, który jest jednostką zależną/podrzędną w relacji "jeden do wielu" w blogu. Jest to stosunkowo proste, ponieważ usunięcie jednostki zależnej/podrzędnej nie ma żadnego wpływu na inne jednostki. Z drugiej strony usunięcie jednostki głównej/nadrzędnej musi mieć również wpływ na wszystkie jednostki zależne/podrzędne. Nie spowoduje to pozostawienia wartości klucza obcego odwołującej się do wartości klucza podstawowego, która już nie istnieje. Jest to nieprawidłowy stan modelu i powoduje wystąpienie błędu ograniczenia odwołania w większości baz danych.

Ten nieprawidłowy stan modelu można obsłużyć na dwa sposoby:

  1. Ustawianie wartości FK na null. Oznacza to, że zależności/elementy podrzędne nie są już powiązane z żadnym podmiotem zabezpieczeń/elementem nadrzędnym. Jest to ustawienie domyślne dla relacji opcjonalnych, w których klucz obcy musi mieć wartość null. Ustawienie klucza FK na wartość null jest nieprawidłowe dla wymaganych relacji, gdzie klucz obcy jest zwykle niepusty.
  2. Usuwanie zależności/elementów podrzędnych. Jest to wartość domyślna dla wymaganych relacji i jest również prawidłowa w przypadku relacji opcjonalnych.

Aby uzyskać szczegółowe informacje na temat śledzenia zmian i relacji, zobacz Zmienianie kluczy obcych i nawigacji.

Relacje opcjonalne

Właściwość Post.BlogId klucza obcego jest dopuszczana do wartości null w modelu, z których korzystaliśmy. Oznacza to, że relacja jest opcjonalna, dlatego domyślne zachowanie platformy EF Core polega na ustawieniu BlogId właściwości klucza obcego na wartość null po usunięciu bloga. Przykład:

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

Inspekcja widoku debugowania monitora zmian po wywołaniu, aby pokazaćRemove, że zgodnie z oczekiwaniami blog jest teraz oznaczony jako Deleted:

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
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>

Co ciekawe, wszystkie powiązane wpisy są teraz oznaczone jako Modified. Jest to spowodowane tym, że właściwość klucza obcego w każdej jednostce została ustawiona na wartość null. Wywołanie polecenia SaveChanges aktualizuje wartość klucza obcego dla każdego wpisu na wartość null w bazie danych, a następnie usuwa blog:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p2;
SELECT changes();

Po zakończeniu operacji SaveChanges usunięta jednostka jest odłączona od obiektu DbContext, ponieważ nie istnieje już w bazie danych. Inne jednostki są teraz oznaczone jako Unchanged z wartościami klucza obcego o wartości null, które są zgodne ze stanem bazy danych:

Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: <null> FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: <null> FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

Wymagane relacje

Jeśli właściwość klucza obcego Post.BlogId jest niepusta, relacja między blogami i wpisami staje się "wymagana". W takiej sytuacji program EF Core domyślnie usunie jednostki zależne/podrzędne po usunięciu podmiotu zabezpieczeń/elementu nadrzędnego. Na przykład usunięcie bloga z powiązanymi wpisami, jak w poprzednim przykładzie:

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

Sprawdź widok debugowania monitora zmian po wywołaniu, aby pokazaćRemove, że zgodnie z oczekiwaniami blog zostanie ponownie oznaczony jako Deleted:

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
  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}
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: {Id: 1}

W tym przypadku bardziej interesujące jest to, że wszystkie powiązane wpisy również zostały oznaczone jako Deleted. Wywołanie polecenia SaveChanges powoduje usunięcie bloga i wszystkich powiązanych wpisów z bazy danych:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;

Po zakończeniu operacji SaveChanges wszystkie usunięte jednostki są odłączone od obiektu DbContext, ponieważ nie istnieją już w bazie danych. W związku z tym dane wyjściowe z widoku debugowania są puste.

Uwaga

Ten dokument zawiera tylko rysowanie powierzchni podczas pracy z relacjami w programie EF Core. Zobacz Relacje , aby uzyskać więcej informacji na temat modelowania relacji oraz Zmienianie kluczy obcych i nawigacji, aby uzyskać więcej informacji na temat aktualizowania/usuwania jednostek zależnych/podrzędnych podczas wywoływania funkcji SaveChanges.

Śledzenie niestandardowe za pomocą usługi TrackGraph

ChangeTracker.TrackGraph działa jak Add, Attach i Update z tą różnicą, że generuje wywołanie zwrotne dla każdego wystąpienia jednostki przed jego śledzeniem. Dzięki temu można używać logiki niestandardowej podczas określania sposobu śledzenia poszczególnych jednostek na grafie.

Rozważmy na przykład użycie reguły programu EF Core podczas śledzenia jednostek z wygenerowanymi wartościami klucza: jeśli wartość klucza wynosi zero, jednostka jest nowa i powinna zostać wstawiona. Rozszerzmy tę regułę, aby powiedzieć, czy wartość klucza jest ujemna, a następnie jednostka powinna zostać usunięta. Dzięki temu możemy zmienić wartości klucza podstawowego w jednostkach rozłączonego grafu, aby oznaczyć usunięte jednostki:

blog.Posts.Add(
    new Post
    {
        Title = "Announcing .NET 5.0",
        Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
    }
);

var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;

Ten odłączony graf można następnie śledzić przy użyciu usługi TrackGraph:

public static void UpdateBlog(Blog blog)
{
    using var context = new BlogsContext();

    context.ChangeTracker.TrackGraph(
        blog, node =>
        {
            var propertyEntry = node.Entry.Property("Id");
            var keyValue = (int)propertyEntry.CurrentValue;

            if (keyValue == 0)
            {
                node.Entry.State = EntityState.Added;
            }
            else if (keyValue < 0)
            {
                propertyEntry.CurrentValue = -keyValue;
                node.Entry.State = EntityState.Deleted;
            }
            else
            {
                node.Entry.State = EntityState.Modified;
            }

            Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
        });

    context.SaveChanges();
}

Dla każdej jednostki na grafie powyższy kod sprawdza wartość klucza podstawowego przed śledzeniem jednostki. W przypadku nieustawionych (zero) wartości klucza kod wykonuje zwykle operacje ef Core. Oznacza to, że jeśli klucz nie jest ustawiony, jednostka jest oznaczona jako Added. Jeśli klucz jest ustawiony, a wartość jest nieujemna, jednostka jest oznaczona jako Modified. Jeśli jednak zostanie znaleziona wartość klucza ujemnego, zostanie przywrócona rzeczywista, nie ujemna wartość, a jednostka zostanie śledzona jako Deleted.

Dane wyjściowe z uruchamiania tego kodu to:

Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added

Uwaga

Dla uproszczenia ten kod zakłada, że każda jednostka ma właściwość klucza podstawowego liczby całkowitej o nazwie Id. Może to być skodyfikowane w abstrakcyjnej klasie bazowej lub interfejsie. Alternatywnie można uzyskać właściwość lub właściwości klucza podstawowego z IEntityType metadanych, tak aby ten kod działał z dowolnym typem jednostki.

TrackGraph ma dwa przeciążenia. W przypadku prostego przeciążenia użytego powyżej program EF Core określa, kiedy zatrzymać przechodzenie grafu. W szczególności zatrzymuje odwiedzanie nowych powiązanych jednostek z danej jednostki, gdy ta jednostka jest już śledzona lub gdy wywołanie zwrotne nie rozpoczyna śledzenia jednostki.

Zaawansowane przeciążenie , ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>)ma wywołanie zwrotne, które zwraca wartość logiczną. Jeśli wywołanie zwrotne zwraca wartość false, przechodzenie grafu zostanie zatrzymane, w przeciwnym razie będzie kontynuowane. Należy zachować ostrożność, aby uniknąć nieskończonych pętli podczas korzystania z tego przeciążenia.

Zaawansowane przeciążenie umożliwia również podanie stanu do usługi TrackGraph, a ten stan jest następnie przekazywany do każdego wywołania zwrotnego.