명시적으로 엔터티 추적

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

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

이 문서에서는 엔터티 상태 및 EF Core 변경 내용 추적의 기본 사항을 이해한다고 가정합니다. 이러한 항목에 대한 자세한 내용은 EF Core의 변경 내용 추적을 참조하세요.

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

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

소개

컨텍스트가 해당 엔터티를 추적하도록 명시적으로 엔터티를 DbContext에 "연결"할 수 있습니다. 이는 다음과 같은 경우에 주로 유용합니다.

  1. 데이터베이스에 삽입할 새 엔터티 만들기
  2. 이전에 다른 DbContext 인스턴스에서 쿼리한 연결이 끊긴 엔터티를 다시 연결하는 경우입니다.

이들 중 첫 번째는 대부분의 애플리케이션에서 필요하며 주로 DbContext.Add 메서드에 의해 처리됩니다.

두 번째는 엔터티가 추적되지 않는 동안 엔터티 또는 해당 관계를 변경하는 애플리케이션에서만 필요합니다. 예를 들어 웹 애플리케이션은 사용자가 변경하고 엔터티를 다시 보내는 웹 클라이언트에 엔터티를 보낼 수 있습니다. 이러한 엔터티는 원래 DbContext에서 쿼리되었지만 클라이언트로 전송될 때 해당 컨텍스트에서 연결이 끊어졌기 때문에 "연결 끊김"이라고 합니다.

이제 웹 애플리케이션이 이러한 엔터티를 다시 연결하여 다시 추적되고 SaveChanges가 데이터베이스를 적절하게 업데이트할 수 있도록 변경된 내용을 나타내야 합니다. 이는 주로 DbContext.AttachDbContext.Update 메서드에 의해 처리됩니다.

쿼리된 동일한 DbContext 인스턴스에 엔터티를 연결하는 작업은 일반적으로 필요하지 않습니다. 추적 금지 쿼리를 정기적으로 수행한 다음 반환된 엔터티를 동일한 컨텍스트에 연결하지 마세요. 이는 추적 쿼리를 사용하는 것보다 속도가 느리며 그림자 속성 값 누락과 같은 문제가 발생하여 제대로 찾기가 더 어려워질 수 있습니다.

생성된 키 값 및 명시적 키 값 비교

기본적으로 정수 및 GUID 키 속성자동으로 생성된 키 값을 사용하도록 구성됩니다. 이는 변경 내용 추적에 큰 이점이 있습니다. 설정되지 않은 키 값은 엔터티가 "새 엔터티"임을 나타냅니다. "새 엔터티”는 데이터베이스에 아직 삽입되지 않았다는 의미입니다.

다음 섹션에서는 두 가지 모델이 사용됩니다. 첫 번째는 생성된 키 값을 사용하지 않도록 구성됩니다.

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

모든 항목이 매우 명시적이고 따라하기 쉽기 때문에 생성되지 않은(즉, 명시적으로 설정된) 키 값이 각 예제에서 먼저 표시됩니다. 그런 다음 생성된 키 값이 사용되는 예제가 뒤따릅니다.

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

생성된 키 값을 사용하는 것이 단순 정수 키의 기본값이므로 이 모델의 키 속성에는 추가 구성이 필요하지 않습니다.

새 엔터티 삽입

명시적 키 값

SaveChanges에 의해 삽입될 Added 상태에서 엔터티를 추적해야 합니다. 엔터티는 일반적으로 DbSet<TEntity>에서 DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync 또는 해당 메서드 중 하나를 호출하여 추가된 상태로 배치됩니다.

이러한 메서드는 모두 변경 내용 추적 컨텍스트에서 동일한 방식으로 작동합니다. 자세한 내용은 추가 변경 내용 추적 기능을 참조하세요.

예를 들어 새 블로그 추적을 시작하려면 다음을 수행합니다.

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

이 호출 후 변경 추적기 디버그 보기를 검사하면 컨텍스트가 Added 상태의 새 엔터티를 추적하고 있음을 보여줍니다.

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

그러나 Add 메서드는 개별 엔터티에서만 작동하는 것이 아닙니다. 실제로 관련 엔터티의 전체 그래프를 추적하여 모든 항목을 Added 상태로 만들기 시작합니다. 예를 들어 새 블로그 및 관련된 새 게시물을 삽입하려면 다음을 수행합니다.

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..."
            }
        }
    });

이제 컨텍스트는 이러한 모든 엔터티를 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}

위의 예제에서 Id 키 속성에 대해 명시적 값이 설정되었습니다. 이는 여기에 있는 모델이 자동으로 생성된 키 값이 아니라 명시적으로 설정된 키 값을 사용하도록 구성되었기 때문입니다. 생성된 키를 사용하지 않는 경우 을 호출하기 전Add에 키 속성을 명시적으로 설정해야 합니다. 그런 다음 SaveChanges가 호출될 때 이러한 키 값이 삽입됩니다. 예를 들어 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);

이제 이러한 엔터티는 데이터베이스에 있으므로 SaveChanges가 완료된 후 모든 엔터티가 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}

생성된 키 값

위에서 설명한 대로 정수 및 GUID 키 속성은 기본적으로 자동으로 생성된 키 값을 사용하도록 구성됩니다. 즉, 애플리케이션은 키 값을 명시적으로 설정해서는 안됩니다. 예를 들어 새 블로그를 삽입하고 생성된 키 값이 있는 모든 게시물을 게시하려면 다음을 수행합니다.

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..."
            }
        }
    });

명시적 키 값과 마찬가지로 컨텍스트는 이제 이러한 모든 엔터티를 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}

이 경우 각 엔터티에 대해 임시 키 값이 생성되었습니다. 이러한 값은 SaveChanges가 호출될 때까지 EF Core에서 사용되며, 이때 실제 키 값은 데이터베이스에서 다시 읽습니다. 예를 들어 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();

SaveChanges가 완료되면 모든 엔터티가 실제 키 값으로 업데이트되고 이제 데이터베이스의 상태와 일치하므로 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}

이는 명시적 키 값을 사용한 이전 예제와 정확히 동일한 최종 상태입니다.

생성된 키 값을 사용하는 경우에도 명시적 키 값을 설정할 수 있습니다. 그런 다음 EF Core는 이 키 값을 사용하여 삽입을 시도합니다. ID 열이 있는 SQL Server를 포함하여 일부 데이터베이스 구성은 이러한 삽입을 지원하지 않으며 throw됩니다(해결 방법은 다음 문서 참조).

기존 엔터티 연결

명시적 키 값

쿼리에서 반환된 엔터티는 Unchanged 상태에서 추적됩니다. Unchanged 상태는 엔터티가 쿼리된 이후 수정되지 않았다는 것을 의미합니다. HTTP 요청 시 웹 클라이언트에서 반환된 연결이 끊긴 엔터티는 DbContext.Attach, DbContext.AttachRange 또는 DbSet<TEntity>에 해당하는 메서드를 사용하여 해당 상태로 전환할 수 있습니다. 예를 들어 기존 블로그 추적을 시작하려면 다음을 수행합니다.

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

참고 항목

여기서 예제는 단순성을 위해 new를 사용하여 명시적으로 엔터티를 만드는 것입니다. 일반적으로 엔터티 인스턴스는 클라이언트에서 역직렬화되거나 HTTP Post의 데이터에서 생성되는 것과 같은 다른 원본에서 제공됩니다.

이 호출 후 변경 추적기 디버그 보기를 검사하면 엔터티가 Unchanged 상태에서 추적됨이 표시됩니다.

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

Add과 마찬가지로 Attach는 실제로 연결된 엔터티의 전체 그래프를 Unchanged 상태로 설정합니다. 예를 들어 기존 블로그 및 관련된 기존 게시물을 첨부하려면 다음을 수행합니다.

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..."
            }
        }
    });

이제 컨텍스트는 이러한 모든 엔터티를 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}

이 시점에서 SaveChanges를 호출해도 아무런 효과가 없습니다. 모든 엔터티는 Unchanged로 표시되므로 데이터베이스에서 업데이트할 항목이 없습니다.

생성된 키 값

위에서 설명한 대로 정수 및 GUID 키 속성은 기본적으로 자동으로 생성된 키 값을 사용하도록 구성됩니다. 연결이 끊긴 엔터티로 작업할 때 주요 이점이 있습니다. 설정되지 않은 키 값은 엔터티가 데이터베이스에 아직 삽입되지 않았다는 것을 나타냅니다. 이를 통해 변경 추적기가 새 엔터티를 자동으로 검색하여 Added 상태에 배치할 수 있습니다. 예를 들어 블로그 및 게시물의 이 그래프를 첨부하는 것이 좋습니다.

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..."
            },
        }
    });

블로그의 키 값은 1이며, 이는 데이터베이스에 이미 있음을 나타냅니다. 게시물 중 2개에는 키 값이 설정되어 있지만 세 번째 게시물은 설정되지 않습니다. EF Core는 이 키 값을 정수의 CLR 기본값인 0으로 표시합니다. 그러면 EF Core에서 새 엔터티를 Unchanged 대신 Added로 표시합니다.

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...'

이 시점에서 SaveChanges를 호출하면 Unchanged 엔터티에서는 아무 작업도 수행하지 않지만 새 엔터티를 데이터베이스에 삽입합니다. 예를 들어 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();

여기서 주목해야 할 중요한 점은 생성된 키 값을 사용하여 EF Core가 연결이 끊긴 그래프의 기존 엔터티와 새 항목을 자동으로 구분할 수 있다는 것입니다. 간단히 말해서, 생성된 키를 사용할 때 EF Core는 해당 엔터티에 키 값이 설정되지 않은 경우 항상 엔터티를 삽입합니다.

기존 엔터티 업데이트

명시적 키 값

DbContext.Update, DbContext.UpdateRangeDbSet<TEntity>에 해당하는 메서드는 엔터티가 Unchanged 상태 대신 Modified에 배치된다는 점을 제외하고 위에서 설명한 Attach 메서드와 정확하게 작동합니다. 예를 들어 기존 블로그를 Modified로 추적하기 시작합니다.

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

이 호출 후 변경 추적기 디버그 보기를 검사하면 컨텍스트가 Modified 상태의 엔터티를 추적하고 있음을 보여줍니다.

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

AddAttach와 마찬가지로 Update는 실제로 관련 엔터티의 전체 그래프Modified로 표시합니다. 예를 들어 기존 블로그 및 관련된 기존 게시물을 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..."
            }
        }
    });

이제 컨텍스트는 이러한 모든 엔터티를 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}

이 시점에서 SaveChanges를 호출하면 이러한 모든 엔터티에 대한 업데이트가 데이터베이스로 전송됩니다. 예를 들어 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();

생성된 키 값

Attach와 마찬가지로 생성된 키 값은 Update에 대해동일한 주요 이점을 갖습니다. 설정되지 않은 키 값은 엔터티가 새 항목이며 아직 데이터베이스에 삽입되지 않았다는 것을 나타냅니다. Attach와 같이 DbContext는 새 엔터티를 자동으로 검색하여 Added 상태에 배치할 수 있습니다. 예를 들어 블로그 및 게시물의 그래프를 사용하여 Update를 호출하는 것이 좋습니다.

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..."
            },
        }
    });

Attach 예제와 마찬가지로 키 값이 없는 게시물은 새로 검색되며 Added 상태로 설정됩니다. 다른 엔터티는 다음과 같이 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}

이 시점에서 SaveChanges를 호출하면 새 엔터티가 삽입되는 동안 모든 기존 엔터티에 대한 업데이트가 데이터베이스로 전송됩니다. 예를 들어 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();

이는 연결이 끊긴 그래프에서 업데이트 및 삽입을 생성하는 매우 쉬운 방법입니다. 그러나 일부 속성 값이 변경되지 않은 경우에도 추적된 모든 엔터티의 모든 속성에 대해 업데이트 또는 삽입이 데이터베이스로 전송됩니다. 이는 그래프가 작은 많은 애플리케이션에서 업데이트를 생성하는 쉽고 실용적인 방법이 될 수 있으니 걱정하지 마세요. 즉, 다른 더 복잡한 패턴으로 인해 EF Core의 ID 확인에 설명된 대로 더 효율적인 업데이트가 발생할 수 있습니다.

기존 엔터티 삭제

SaveChanges에서 엔터티를 삭제하려면 Deleted 상태에서 추적해야 합니다. 엔터티는 일반적으로 DbContext.Remove, DbContext.RemoveRange 또는 DbSet<TEntity>에서 해당 메서드 중 하나를 호출하여 Deleted 상태에 배치됩니다. 예를 들어 기존 게시물을 Deleted로 표시하려면 다음을 수행합니다.

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

이 호출 후 변경 추적기 디버그 보기를 검사하면 컨텍스트가 Deleted 상태의 엔터티를 추적하고 있음을 보여줍니다.

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

SaveChanges가 호출되면 이 엔터티가 삭제됩니다. 예를 들어 SQLite를 사용하는 경우:

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

SaveChanges가 완료되면 삭제된 엔터티는 데이터베이스에 더 이상 존재하지 않으므로 DbContext에서 분리됩니다. 따라서 추적 중인 엔터티가 없으므로 디버그 보기는 비어 있게 됩니다.

종속/자식 엔터티 삭제

그래프에서 종속/자식 엔터티를 삭제하는 것은 보안 주체/부모 엔터티를 삭제하는 것보다 더 간단합니다. 자세한 내용은 외래 키 및 탐색 변경을 참조하세요.

new를 사용하여 만든 엔터티에서 Remove를 호출하는 것은 드문 경우입니다. 또한 Add, AttachUpdate와 달리 Unchanged 또는 Modified 상태에서 아직 추적되지 않은 엔터티에서 Remove를 호출하는 것은 일반적이지 않습니다. 대신 관련 엔터티의 단일 엔터티 또는 그래프를 추적한 다음 삭제해야 하는 엔터티에서 Remove를 호출하는 것이 일반적입니다. 추적된 엔터티의 이 그래프는 일반적으로 다음 중 하나를 통해 생성됩니다.

  1. 엔터티에 대한 쿼리 실행
  2. 이전 섹션에서 설명한 대로 연결이 끊긴 엔터티 그래프에서 Attach 또는 Update 메서드를 사용합니다.

예를 들어 이전 섹션의 코드는 클라이언트에서 게시물을 가져온 다음 다음과 같은 작업을 수행할 가능성이 높습니다.

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

추적되지 않은 엔터티에서 Remove를 호출하면 먼저 연결된 후 Deleted로 표시되므로 이전 예제와 정확히 동일한 방식으로 작동합니다.

보다 현실적인 예제에서는 엔터티 그래프가 먼저 연결된 후 해당 엔터티 중 일부는 삭제된 것으로 표시됩니다. 예시:

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

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

Remove가 호출된 엔터티를 제외한 모든 엔터티는 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} 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}

SaveChanges가 호출되면 이 엔터티가 삭제됩니다. 예를 들어 SQLite를 사용하는 경우:

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

SaveChanges가 완료되면 삭제된 엔터티는 데이터베이스에 더 이상 존재하지 않으므로 DbContext에서 분리됩니다. 다른 엔터티는 Unchanged 상태로 유지됩니다.

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}

보안 주체/부모 엔터티 삭제

두 엔터티 형식을 연결하는 각 관계에는 보안 주체 또는 부모 끝 및 종속 또는 자식 끝이 있습니다. 종속/자식 엔터티는 외래 키 속성이 있는 엔터티입니다. 일대다 관계에서 보안 주체/부모는 "일" 쪽에 있고 종속/자식은 "다" 쪽에 있습니다. 자세한 내용은 관계를 참조하세요.

앞의 예제에서는 블로그 게시물 일대다 관계의 종속/자식 엔터티인 게시물을 삭제했습니다. 종속/자식 엔터티를 제거해도 다른 엔터티에 영향을 미치지 않으므로 이는 비교적 간단한 작업입니다. 반면에 보안 주체/부모 엔터티를 삭제하면 종속/자식 엔터티에도 영향을 미쳐야 합니다. 그렇지 않으면 더 이상 존재하지 않는 기본 키 값을 참조하는 외래 키 값이 남게 됩니다. 이는 잘못된 상태이며 대부분의 데이터베이스에서 참조 제약 조건 위반을 초래합니다.

이러한 잘못된 모델 상태는 다음 두 가지 방법으로 처리할 수 있습니다.

  1. FK 값을 null로 설정합니다. 이는 종속/자식이 더 이상 보안 주체/부모와 관련이 없음을 나타냅니다. 이는 외래 키가 null을 허용해야 하는 선택적 관계의 기본값입니다. 외래 키가 일반적으로 null을 허용하지 않는 필수 관계에는 FK를 null로 설정하는 것이 유효하지 않습니다.
  2. 종속/자식 삭제. 이는 필수 관계의 기본값이며 선택적 관계에도 유효합니다.

변경 내용 추적 및 관계에 대한 자세한 내용은 외래 키 및 탐색 변경을 참조하세요.

선택적 관계

Post.BlogId 외래 키 속성은 사용 중인 모델에서 null을 허용합니다. 즉, 해당 관계는 선택 사항이므로 EF Core의 기본 동작은 블로그가 삭제되는 경우 BlogId 외래 키 속성을 null로 설정하는 것입니다. 예시:

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

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

Remove를 호출한 후 변경 추적기 디버그 보기를 검사하면 블로그가 예상대로 이제 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>

더 흥미롭게도 모든 관련 게시물은 이제 Modified로 표시됩니다. 이는 각 엔터티의 외래 키 속성이 null로 설정되었기 때문입니다. SaveChanges를 호출하면 데이터베이스에서 각 게시물에 대한 외래 키 값이 null로 업데이트된 후 블로그를 삭제합니다.

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

SaveChanges가 완료되면 삭제된 엔터티는 데이터베이스에 더 이상 존재하지 않으므로 DbContext에서 분리됩니다. 이제 다른 엔터티는 데이터베이스의 상태와 일치하는 null 외래 키 값으로 Unchanged로 표시됩니다.

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>

필수 관계

Post.BlogId 외래 키 속성이 null을 허용하지 않는 경우 블로그와 게시물 간의 관계는 "필수"가 됩니다. 이 경우 EF Core는 기본적으로 보안 주체/부모가 삭제될 때 종속/자식 엔터티를 삭제합니다. 예를 들어 이전 예제와 같이 관련 게시물이 있는 블로그를 삭제합니다.

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

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

Remove를 호출한 후 변경 추적기 디버그 보기를 검사하면 블로그가 예상대로 다시 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}

이 경우 더 흥미롭게도 모든 관련 게시물도 Deleted로 표시 되었습니다. SaveChanges를 호출하면 블로그 및 모든 관련 게시물이 데이터베이스에서 삭제됩니다.

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

SaveChanges가 완료되면 삭제된 모든 엔터티는 데이터베이스에 더 이상 존재하지 않으므로 DbContext에서 분리됩니다. 따라서 디버그 보기의 출력은 비어 있습니다.

참고 항목

이 문서는 EF Core의 관계 작업에 대한 표면만 긁습니다. 모델링 관계에 대한 자세한 내용은 관계를 참조하고 SaveChanges를 호출할 때 종속/자식 엔터티를 업데이트/삭제하는 방법에 대한 자세한 내용은 외래 키 및 탐색 변경을 참조하세요.

TrackGraph를 사용한 사용자 지정 추적

ChangeTracker.TrackGraph는 추적하기 전에 모든 엔터티 인스턴스에 대한 콜백을 생성한다는 점을 제외하고 Add, Attach, Update와 같이 작동합니다. 이를 통해 그래프에서 개별 엔터티를 추적하는 방법을 결정할 때 사용자 지정 논리를 사용할 수 있습니다.

예를 들어 EF Core가 생성된 키 값으로 엔터티를 추적할 때 사용하는 규칙을 고려합니다. 키 값이 0이면 엔터티가 새 항목이며 삽입되어야 합니다. 키 값이 음수이면 엔터티를 삭제해야 한다고 지정되도록 이 규칙을 확장해 보겠습니다. 이를 통해 연결이 끊긴 그래프의 엔터티에서 기본 키 값을 변경하여 삭제된 엔터티를 표시할 수 있습니다.

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;

연결이 끊긴 이 그래프는 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();
}

그래프의 각 엔터티에 대해 위의 코드는 엔터티를 추적하기 전에 기본 키 값을 확인합니다. 설정되지 않은(0) 키 값의 경우 코드는 EF Core가 일반적으로 수행하는 작업을 수행합니다. 즉, 키가 설정되지 않은 경우 엔터티는 Added로 표시됩니다. 키가 설정되고 값이 음수가 아닌 경우 엔터티는 Modified로 표시됩니다. 그러나 음수 키 값이 발견되면 실제 음수가 아닌 값이 복원되고 엔터티가 Deleted로 추적됩니다.

이 코드를 실행하는 출력은 다음과 같습니다.

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

참고 항목

간단히 하기 위해 이 코드는 각 엔터티에 Id이라는 정수 기본 키 속성이 있다고 가정합니다. 이는 추상 기본 클래스 또는 인터페이스로 코딩할 수 있습니다. 또는 이 코드가 모든 형식의 엔터티에서 작동할 수 있도록 IEntityType 메타데이터에서 기본 키 속성 또는 속성을 가져올 수 있습니다.

TrackGraph에는 두 개의 오버로드가 있습니다. 위에서 사용한 간단한 오버로드에서 EF Core는 그래프 트래버스를 중지할 시기를 결정합니다. 특히 해당 엔터티가 이미 추적되었거나 콜백이 엔터티 추적을 시작하지 않을 때 지정된 엔터티에서 새 관련 엔터티를 방문하지 않습니다.

고급 오버로드인 ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>)에는 bool을 반환하는 콜백이 있습니다. 콜백이 false를 반환하면 그래프 순회가 중지되며, 그렇지 않으면 계속됩니다. 이 오버로드를 사용할 때 무한 루프를 방지하려면 주의해야 합니다.

또한 고급 오버로드를 사용하면 상태를 TrackGraph에 전달할 수 있으며 이러한 상태는 각 콜백에 전달됩니다.