EF Core 변경 내용 추적

DbContext 인스턴스는 엔터티 변경 내용을 추적합니다. 이렇게 추적되는 엔터티는 차례로 SaveChanges가 호출될 때 변경 내용을 데이터베이스에 적용합니다.

이 문서에서는 EF Core(Entity Framework Core) 변경 내용 추적과 쿼리 및 업데이트와의 관계에 대한 개요를 제공합니다.

GitHub에서 샘플 코드를 다운로드하여 이 문서의 모든 코드를 실행하고 디버그할 수 있습니다.

편의상 이 문서에서는 SaveChangesAsync와 같은 비동기 메서드보다는 SaveChanges와 같이 그에 해당하는 동기화 메서드를 사용하고 참조합니다. 별도로 언급하지 않는 한 비동기 메서드 호출 및 대기로 대체할 수 있습니다.

엔터티를 추적하는 방법

엔터티 인스턴스는 다음과 같은 경우에 추적됩니다.

  • 데이터베이스에 대해 실행되는 쿼리에서 반환된 경우
  • Add, Attach, Update 또는 유사한 메서드를 통해 명시적으로 DbContext에 연결된 경우
  • 추적된 기존 엔터티에 연결된 새 엔터티로 검색된 경우

다음과 같은 경우에는 엔터티 인스턴스가 더 이상 추적되지 않습니다.

  • DbContext가 삭제된 경우
  • 변경 추적기가 지워집니다.
  • 엔터티가 명시적으로 분리된 경우

DbContext는 DbContext 초기화 및 구성에 설명된 대로 일시적인 작업 단위를 나타내도록 설계되었습니다. 따라서 DbContext 삭제는 엔터티 추적을 중지하는 ‘일반적인 방법’입니다. 다시 말해 DbContext의 수명은 다음과 같아야 합니다.

  1. DbContext 인스턴스 만들기
  2. 일부 엔터티 추적
  3. 엔터티를 일부 변경
  4. SaveChanges를 호출하여 데이터베이스 업데이트
  5. DbContext 인스턴스 삭제

이 방법을 사용할 경우 변경 내용 추적기를 지우거나 엔터티 인스턴스를 명시적으로 분리할 필요가 없습니다. 하지만 엔터티를 분리해야 하는 경우에는 엔터티를 하나씩 분리하는 것보다 ChangeTracker.Clear를 호출하는 것이 더 효율적입니다.

엔터티 상태

모든 엔터티는 특정 EntityState와 연결됩니다.

  • Detached 엔터티는 DbContext에서 추적되고 있지 않습니다.
  • Added 엔터티는 새 엔터티이며, 아직 데이터베이스에 삽입되지 않았습니다. 이는 SaveChanges가 호출될 때 삽입됨을 의미합니다.
  • Unchanged 엔터티는 데이터베이스에서 쿼리된 이후 변경되지 않았습니다. 쿼리에서 반환된 모든 엔터티는 처음에 이 상태입니다.
  • Modified 엔터티는 데이터베이스에서 쿼리된 이후 변경되었습니다. 따라서 SaveChanges가 호출될 때 업데이트됩니다.
  • Deleted 엔터티는 데이터베이스에 존재하지만 SaveChanges가 호출될 때 삭제하도록 표시됩니다.

EF Core는 속성 수준에서 변경 내용을 추적합니다. 예를 들어 단일 속성 값만 수정되는 경우 데이터베이스 업데이트에서 해당 값만 변경됩니다. 다만 속성은 엔터티 자체가 Modified 상태일 때만 수정된 것으로 표시될 수 있습니다. (또는 다른 관점에서 보면 Modified 상태는 하나 이상의 속성 값이 수정된 것으로 표시되었음을 의미합니다.)

다음 표에는 여러 상태가 요약되어 있습니다.

엔터티 상태 DbContext에서 추적 데이터베이스에 존재 속성 수정됨 SaveChanges 시 작업
Detached 아니요 - - -
Added 아니요 - 삽입
Unchanged 아니요 -
Modified 업데이트
Deleted - DELETE

참고

명확성을 위해 이 텍스트에서는 관계형 데이터베이스 용어를 사용합니다. 일반적으로 NoSQL 데이터베이스도 유사한 작업을 지원하지만 이름이 다를 수 있습니다. 자세한 내용은 데이터베이스 공급자 설명서를 참조하세요.

쿼리에서 추적

EF Core 변경 내용 추적은 엔터티 쿼리와 SaveChanges 호출을 통한 엔터티 업데이트에 동일한 DbContext 인스턴스를 사용할 때 가장 효과적입니다. 이는 EF Core가 쿼리된 엔터티의 상태를 자동으로 추적한 다음, SaveChanges가 호출될 때 이러한 엔터티의 변경 내용을 검색하기 때문입니다.

이 방법은 엔터티 인스턴스를 명시적으로 추적하는 것에 비해 다음과 같은 여러 가지 이점을 제공합니다.

  • 해당 인스턴스가 단순합니다. 엔터티 상태를 명시적으로 조작할 필요가 거의 없으며, EF Core가 상태 변경을 처리합니다.
  • 실제 변경된 값으로만 업데이트가 제한됩니다.
  • 섀도 속성 값은 유지되며 필요에 따라 사용됩니다. 이는 외래 키가 섀도 상태로 저장된 경우에 특히 관련이 있습니다.
  • 속성의 원래 값은 자동으로 유지되고 효율적인 업데이트를 위해 사용됩니다.

단순 쿼리 및 업데이트

예를 들어 간단한 블로그/게시물 모델을 생각해 보겠습니다.

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

이 모델을 사용하여 블로그 및 게시물을 쿼리한 다음 데이터베이스에 대한 몇 가지 업데이트를 수행할 수 있습니다.

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

blog.Name = ".NET Blog (Updated!)";

foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
{
    post.Title = post.Title.Replace("5", "5.0");
}

context.SaveChanges();

SQLite를 예제 데이터베이스로 사용하여 SaveChanges를 호출하면 다음 데이터베이스 업데이트가 수행됩니다.

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

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "Title" = @p0
WHERE "Id" = @p1;
SELECT changes();

변경 내용 추적기 디버그 보기는 추적되는 엔터티와 엔터티의 상태를 시각화하는 유용한 방법입니다. 예를 들어 SaveChanges를 호출하기 전에 위의 샘플에 다음 코드를 삽입합니다.

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

다음 출력이 생성됩니다.

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
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} Modified
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
  Blog: {Id: 1}

특히 다음에 유의하세요.

  • Blog.Name 속성은 수정된 것으로 표시되며(Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'), 이로 인해 블로그는 Modified 상태가 됩니다.
  • post 2의 Post.Title 속성은 수정된 것으로 표시되며(Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'), 이로 인해 이 게시물은 Modified 상태가 됩니다.
  • post 2의 다른 속성 값은 변경되지 않았으므로 수정된 것으로 표시되지 않습니다. 따라서 이러한 값은 데이터베이스 업데이트에 포함되지 않습니다.
  • 다른 게시물은 전혀 수정되지 않았습니다. 따라서 여전히 Unchanged 상태이고 데이터베이스 업데이트에 포함되지 않습니다.

쿼리 후 삽입, 업데이트, 삭제

앞의 예제와 같은 업데이트는 동일한 작업 단위에서 삽입 및 삭제와 결합할 수 있습니다. 예를 들면 다음과 같습니다.

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Modify property values
blog.Name = ".NET Blog (Updated!)";

// Insert a new Post
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

// Mark an existing Post as Deleted
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

이 예제에서는 다음이 적용됩니다.

  • 블로그 및 관련 게시물이 데이터베이스에서 쿼리되고 추적됨
  • Blog.Name 속성이 변경됨
  • 새 게시물이 블로그의 기존 게시물 컬렉션에 추가됨
  • 기존 게시물은 DbContext.Remove를 호출하여 삭제하도록 표시됨

SaveChanges를 호출하기 전에 변경 추적기 디버그 보기를 다시 살펴보면 EF Core가 이러한 변경 내용을 추적하는 방법을 알 수 있습니다.

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
Post {Id: -2147482638} Added
  Id: -2147482638 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  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} 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}

다음에 유의합니다.

  • 블로그가 Modified로 표시됩니다. 그러면 데이터베이스 업데이트가 생성됩니다.
  • post 2는 Deleted로 표시됩니다. 그러면 데이터베이스 삭제가 생성됩니다.
  • 임시 ID가 있는 새 게시물은 blog 1과 연결되고 Added로 표시됩니다. 그러면 데이터베이스 삽입이 생성됩니다.

그러면 SaveChanges가 호출될 때 다음 데이터베이스 명령이 생성됩니다(SQLite 사용).

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
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=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], 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();

엔터티 삽입 및 삭제에 대한 자세한 내용은 명시적으로 엔터티 추적을 참조하세요. 이와 같은 변경 내용을 EF Core가 자동으로 검색하는 방법에 대한 자세한 내용은 변경 내용 검색 및 알림을 참조하세요.

SaveChanges가 데이터베이스를 업데이트하도록 하는 변경이 수행되었는지 여부를 확인하려면 ChangeTracker.HasChanges()를 호출하세요. HasChanges가 false를 반환하는 경우 SaveChanges는 no-op가 됩니다.