다음을 통해 공유


외래 키 및 탐색 변경

외래 키 및 탐색 개요

EF Core(Entity Framework Core) 모델의 관계는 FK(외장 키)를 사용하여 표시됩니다. FK는 관계의 종속 엔터티 또는 자식 엔터티에 대한 하나 이상의 속성으로 구성됩니다. 이 종속/자식 엔터티는 종속/자식의 외래 키 속성 값이 보안 주체/부모에 있는 대체 또는 기본 키(PK) 속성의 값과 일치하는 경우 지정된 보안 주체/부모 엔터티와 연결됩니다.

외신 키는 데이터베이스에서 관계를 저장하고 조작하는 좋은 방법이지만 애플리케이션 코드에서 여러 관련 엔터티로 작업할 때는 별로 친숙하지 않습니다. 따라서 대부분의 EF Core 모델은 FK 표현보다 "탐색"을 계층화합니다. 탐색은 외래 키 값을 기본 또는 대체 키 값과 일치시켜 찾은 연결을 반영하는 엔터티 인스턴스 간의 C#/.NET 참조를 형성합니다.

탐색은 관계의 양쪽, 한쪽에서만 사용하거나 전혀 사용하지 않고 FK 속성만 남겨 둘 수 있습니다. FK 속성은 섀도 속성으로 만들어 숨길 수 있습니다. 관계 모델링에 관한 자세한 내용은 관계를 참조하세요.

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

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

예제 모델

다음 모델에는 엔터티 형식 간에 관계가 있는 네 가지 엔터티 형식이 포함되어 있습니다. 코드의 주석은 외세 키, 기본 키 및 탐색 속성이 어느 속성을 나타내는지 나타냅니다.

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

    public IList<Post> Posts { get; } = new List<Post>(); // Collection navigation
    public BlogAssets Assets { get; set; } // Reference navigation
}

public class BlogAssets
{
    public int Id { get; set; } // Primary key
    public byte[] Banner { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation
}

public class Post
{
    public int Id { get; set; } // Primary key
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
}

public class Tag
{
    public int Id { get; set; } // Primary key
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
}

이 모델의 세 가지 관계는 다음과 같습니다.

  • 각 블로그에는 많은 게시물(일대다)이 있을 수 있습니다.
    • Blog는 보안 주체/부모입니다.
    • Post는 종속/자식입니다. 여기에는 관련 블로그의 Blog.Id PK 값과 일치해야 하는 값인 FK 속성 Post.BlogId가 포함되어 있습니다.
    • Post.Blog는 게시물에서 연결된 블로그로의 참조 탐색입니다. Post.BlogBlog.Posts의 역 탐색입니다.
    • Blog.Posts는 블로그에서 연결된 모든 게시물로의 컬렉션 탐색입니다. Blog.PostsPost.Blog의 역 탐색입니다.
  • 각 블로그에는 하나의 자산(일대일)이 있을 수 있습니다.
    • Blog는 보안 주체/부모입니다.
    • BlogAssets는 종속/자식입니다. 여기에는 관련 블로그의 Blog.Id PK 값과 일치해야 하는 값인 FK 속성 BlogAssets.BlogId가 포함되어 있습니다.
    • BlogAssets.Blog는 자산에서 연결된 블로그로의 참조 탐색입니다. BlogAssets.BlogBlog.Assets의 역 탐색입니다.
    • Blog.Assets는 블로그에서 연결된 자산으로의 참조 탐색입니다. Blog.AssetsBlogAssets.Blog의 역 탐색입니다.
  • 각 게시물에는 많은 태그가 있을 수 있으며 각 태그에는 많은 게시물(다대다)이 있을 수 있습니다.
    • 다대다 관계는 두 개의 일대다 관계에 대한 추가 계층입니다. 다대다 관계는 이 문서의 뒷부분에서 다룹니다.
    • Post.Tags는 게시물에서 연결된 모든 태그로의 컬렉션 탐색입니다. Post.TagsTag.Posts의 역 탐색입니다.
    • Tag.Posts는 태그에서 연결된 모든 게시물로의 컬렉션 탐색입니다. Tag.PostsPost.Tags의 역 탐색입니다.

관계를 모델링하고 구성하는 방법에 대한 자세한 내용은 관계를 참조하세요.

관계 수정

EF Core는 외래 키 값과 일치하는 탐색을 유지하며 그 반대의 경우도 마찬가지입니다. 즉, 외래 키 값이 변경되어 이제 다른 보안 주체/부모 엔터티를 참조하는 경우 이 변경 내용을 반영하도록 탐색이 업데이트됩니다. 마찬가지로 탐색이 변경되면 관련된 엔터티의 외래 키 값이 이 변경 사항을 반영하도록 업데이트됩니다. 이를 "관계 수정"이라고 합니다.

쿼리별 수정

수정은 먼저 데이터베이스에서 엔터티를 쿼리할 때 발생합니다. 데이터베이스에는 외래 키 값만 있으므로 EF Core가 데이터베이스에서 엔터티 인스턴스를 만들 때 외래 키 값을 사용하여 참조 탐색을 설정하고 적절하게 컬렉션 탐색에 엔터티를 추가합니다. 예를 들어 블로그 및 관련 게시물 및 자산에 대한 쿼리를 고려합니다.

using var context = new BlogsContext();

var blogs = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToList();

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

각 블로그에 대해 EF Core는 먼저 Blog 인스턴스를 만듭니다. 그런 다음 각 게시물이 데이터베이스에서 로드될 때 Post.Blog 참조 탐색이 연결된 블로그를 가리키도록 설정됩니다. 마찬가지로 게시물이 Blog.Posts 컬렉션 탐색에 추가됩니다. 이 경우 두 탐색이 모두 참조인 경우를 제외하고 BlogAssets에서 동일한 일이 발생합니다. Blog.Assets 탐색은 자산 인스턴스를 가리키도록 설정되고 BlogAsserts.Blog 탐색은 블로그 인스턴스를 가리키도록 설정됩니다.

이 쿼리 후 변경 추적기 디버그 보기를 보면 각각 하나의 자산과 두 개의 게시물이 추적되는 두 개의 블로그가 표시됩니다.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {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: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 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: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

디버그 보기에는 키 값과 탐색이 모두 표시됩니다. 탐색은 관련 엔터티의 기본 키 값을 사용하여 표시됩니다. 예를 들어 위의 출력에서 Posts: [{Id: 1}, {Id: 2}]Blog.Posts 컬렉션 탐색에 기본 키가 각각 1과 2인 두 개의 관련 게시물이 포함되어 있음을 나타냅니다. 마찬가지로 첫 번째 블로그와 연결된 각 게시물에 대해 Blog: {Id: 1} 줄은 Post.Blog 탐색이 기본 키 1이 있는 블로그를 참조한다는 것을 나타냅니다.

로컬로 추적된 엔터티에 대한 수정

관계 수정은 추적 쿼리에서 반환된 엔터티와 DbContext에서 이미 추적한 엔터티 간에도 발생합니다. 예를 들어 블로그, 게시물 및 자산에 대해 세 개의 별도 쿼리를 실행하는 것이 좋습니다.

using var context = new BlogsContext();

var blogs = context.Blogs.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var assets = context.Assets.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var posts = context.Posts.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

디버그 보기를 다시 살펴보면 첫 번째 쿼리 후에는 두 개의 블로그만 추적됩니다.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: []

Blog.Assets 참조 탐색은 null이며 현재 Blog.Posts 컨텍스트에서 추적 중인 연결된 엔터티가 없으므로 컬렉션 탐색은 비어 있습니다.

두 번째 쿼리 후에 새로 추적된 BlogAsset 인스턴스를 가리키도록 Blogs.Assets 참조 탐색이 수정되었습니다. 마찬가지로 BlogAssets.Blog 참조 탐색은 이미 추적된 적절한 Blog 인스턴스를 가리키도록 설정됩니다.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: []
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}

마지막으로, 세 번째 쿼리 후에 Blog.Posts 컬렉션 탐색에는 이제 모든 관련 게시물이 포함되며 Post.Blog 참조는 적절한 Blog 인스턴스를 가리킵니다.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {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: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 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: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

EF Core는 여러 다른 쿼리에서 오는 경우에도 엔터티가 추적될 때 탐색을 고정했기 때문에 원래 단일 쿼리에서 달성한 것과 동일한 최종 상태입니다.

참고 항목

수정으로 인해 데이터베이스에서 더 많은 데이터가 반환되지 않습니다. 쿼리에서 이미 반환되었거나 DbContext에서 이미 추적한 엔터티만 연결합니다. 엔터티를 직렬화할 때 중복 처리에 대한 자세한 내용은 EF Core의 ID 확인을 참조하세요.

탐색을 사용하여 관계 변경

두 엔터티 간의 관계를 변경하는 가장 쉬운 방법은 탐색을 조작하는 동시에 EF Core를 떠나 역 탐색 및 FK 값을 적절하게 수정하는 것입니다. 이 작업은 다음을 통해 수행할 수 있습니다.

  • 컬렉션 탐색에서 엔터티 추가 또는 제거
  • 다른 엔터티를 가리키도록 참조 탐색을 변경하거나 null로 설정합니다.

컬렉션 탐색에서 추가 또는 제거

예를 들어 Visual Studio 블로그의 게시물 중 하나를 .NET 블로그로 이동해 보겠습니다. 이렇게 하려면 먼저 블로그와 게시물을 로드한 다음, 한 블로그의 탐색 컬렉션에서 다른 블로그의 탐색 컬렉션으로 게시물을 이동해야 합니다.

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");
var vsBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == "Visual Studio Blog");

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

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

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

context.SaveChanges();

디버그 보기에 액세스해도 변경 내용이 자동으로 검색되지 않으므로 ChangeTracker.DetectChanges()에 대한 호출이 필요합니다.

위의 코드를 실행한 후 인쇄된 디버그 보기입니다.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: [{Id: 4}]
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: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

.NET 블로그의 Blog.Posts 탐색에는 이제 세 개의 게시물(Posts: [{Id: 1}, {Id: 2}, {Id: 3}])이 있습니다. 마찬가지로 Visual Studio 블로그의 Blog.Posts 탐색에는 하나의 게시물(Posts: [{Id: 4}])만 있습니다. 코드가 이러한 컬렉션을 명시적으로 변경했기 때문에 이 작업이 필요합니다.

더 흥미롭게도 코드가 Post.Blog 탐색을 명시적으로 변경하지는 않았지만 Visual Studio 블로그(Blog: {Id: 1})를 가리키도록 수정되었습니다. 또한 Post.BlogId 외래 키 값이 .NET 블로그의 기본 키 값과 일치하도록 업데이트되었습니다. 의 FK 값에 대한 변경 내용은 SaveChanges가 호출될 때 데이터베이스에 유지됩니다.

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

참조 탐색 변경

이전 예제에서는 각 블로그에서 게시물의 컬렉션 탐색을 조작하여 게시물을 한 블로그에서 다른 블로그로 이동했습니다. 새 블로그를 가리키도록 Post.Blog 참조 탐색을 변경하는 대신 동일한 작업을 수행할 수 있습니다. 예시:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.Blog = dotNetBlog;

이 변경 후의 디버그 보기는 이전 예제와 정확히 동일합니다. 이는 EF Core가 참조 탐색 변경을 감지한 다음 일치하도록 컬렉션 탐색 및 FK 값을 수정했기 때문입니다.

외래 키 값을 사용하여 관계 변경

이전 섹션에서는 외래 키 값이 자동으로 업데이트되도록 하는 탐색을 통해 관계를 조작했습니다. 이는 EF Core에서 관계를 조작하는 데 권장되는 방법입니다. 그러나 FK 값을 직접 조작할 수도 있습니다. 예를 들어 Post.BlogId 외래 키 값을 변경하여 게시물을 한 블로그에서 다른 블로그로 이동할 수 있습니다.

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.BlogId = dotNetBlog.Id;

이는 이전 예제와 같이 참조 탐색을 변경하는 것과 매우 유사합니다.

이 변경 후의 디버그 보기는 이전 두 예제의 경우와 정확히 동일합니다. EF Core가 FK 값 변경을 감지한 다음 일치시킬 참조 및 컬렉션 탐색을 모두 수정했기 때문입니다.

관계가 변경 될 때마다 모든 탐색 및 FK 값을 조작 하는 코드를 작성 하지 마십시오. 이러한 코드는 더 복잡하며 모든 경우에 외신 키 및 탐색을 일관되게 변경해야 합니다. 가능하면 단일 탐색을 조작하거나 두 탐색을 모두 조작하면 됩니다. 필요한 경우 FK 값을 조작하기만 하면됩니다. 탐색 및 FK 값을 모두 조작하지 않습니다.

추가되거나 삭제된 엔터티에 대한 수정

컬렉션 탐색에 추가

EF Core는 컬렉션 탐색에 새 종속/자식 엔터티가 추가되었음을 감지하면 다음 작업을 수행합니다.

  • 엔터티가 추적되지 않으면 추적됩니다. (엔터티는 일반적으로 Added 상태에 있습니다. 그러나 엔터티 형식이 생성된 키를 사용하도록 구성되고 기본 키 값이 설정된 경우 엔터티는 Unchanged 상태에서 추적됩니다.)
  • 엔터티가 다른 보안 주체/부모와 연결된 경우 해당 관계가 끊어집니다.
  • 엔터티는 컬렉션 탐색을 소유하는 보안 주체/부모와 연결됩니다.
  • 탐색 및 외래 키 값은 관련된 모든 엔터티에 대해 고정됩니다.

이를 기반으로 한 블로그에서 다른 블로그로 게시물을 이동하려면 새 컬렉션 탐색에 추가하기 전에 이전 컬렉션 탐색에서 실제로 제거할 필요가 없다는 것을 알 수 있습니다. 따라서 위의 예제의 코드를 다음에서 변경할 수 있습니다.

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

다음으로 변경:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post);

EF Core는 게시물이 새 블로그에 추가된 것을 확인하고 첫 번째 블로그의 컬렉션에서 자동으로 제거합니다.

컬렉션 탐색에서 제거

보안 주체/부모의 컬렉션 탐색에서 종속/자식 엔터티를 제거하면 해당 보안 주체/부모에 대한 관계가 끊어지게 됩니다. 다음에 발생하는 작업은 관계가 선택 사항인지 필수인지에 따라 달라집니다.

선택적 관계

선택적 관계의 경우 기본적으로 외래 키 값은 null로 설정됩니다. 즉, 종속/자식이 더 이상 어떠한 보안 주체/부모와도 연결되지 않습니다. 예를 들어 블로그 및 게시물을 로드한 다음 Blog.Posts 컬렉션 탐색에서 게시물 중 하나를 제거해 보겠습니다.

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

변경한 후 변경 내용 추적 디버그 보기를 보면 다음이 표시됩니다.

  • Post.BlogId FK가 null(BlogId: <null> FK Modified Originally 1)로 설정되었습니다.
  • Post.Blog 참조 탐색이 null(Blog: <null>)로 설정되었습니다.
  • 게시물이 컬렉션 탐색에서 Blog.Posts 제거되었습니다(Posts: [{Id: 1}]).
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  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}
  Tags: []
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>
  Tags: []

게시물이 Deleted로 표시되지 않습니다. SaveChanges가 호출될 때 데이터베이스의 FK 값이 null로 설정되도록 Modified로 표시됩니다.

필수 관계

필요한 관계에 대해 FK 값을 null로 설정하는 것은 허용되지 않으며 일반적으로 불가능합니다. 따라서 필수 관계를 끊으면 참조 제약 조건 위반을 방지하기 위해 SaveChanges가 호출될 때 종속/자식 엔터티를 새 보안 주체/부모로 다시 부모로 다시 설정하거나 데이터베이스에서 제거해야 합니다. 이를 "고아 삭제"라고 하며 필요한 관계에 대한 EF Core의 기본 동작입니다.

예를 들어 블로그와 게시물 간의 관계를 필수로 변경한 다음, 이전 예제와 동일한 코드를 실행해 보겠습니다.

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

이 변경 후 디버그 보기를 보면 다음이 표시됩니다.

  • 이 게시물은 SaveChanges가 호출될 때 데이터베이스에서 삭제되도록 Deleted로 표시되었습니다.
  • Post.Blog 참조 탐색이 null(Blog: <null>)로 설정되었습니다.
  • 게시물이 Blog.Posts 컬렉션 탐색(Posts: [{Id: 1}])에서 제거되었습니다.
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  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}
  Tags: []
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: <null>
  Tags: []

Post.BlogId는 필수 관계의 경우 null로 설정할 수 없으므로 변경되지 않은 상태로 유지됩니다.

SaveChanges를 호출하면 분리된 게시물이 삭제됩니다.

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

고아 타이밍 삭제 및 다시 육아

기본적으로 관계 변경이 감지되는 즉시 고아를 Deleted로 표시합니다. 그러나 SaveChanges가 실제로 호출될 때까지 이 프로세스를 지연할 수 있습니다. 이는 하나의 보안 주체/부모에서 제거되었지만 SaveChanges가 호출되기 전에 새 보안 주체/부모로 다시 부모가 되는 엔터티의 고아를 만들지 않도록 하는 데 유용할 수 있습니다. ChangeTracker.DeleteOrphansTiming은 이 타이밍을 설정하는 데 사용됩니다. 예시:

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);

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

dotNetBlog.Posts.Add(post);

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

context.SaveChanges();

첫 번째 컬렉션에서 게시물을 제거한 후 개체는 이전 예제와 같이 Deleted로 표시되지 않습니다. 대신 EF Core는 필수 관계임에도 불구하고 관계가 끊어지는 것을 추적합니다. (형식이 null을 허용하지 않기 때문에 실제로 null일 수는 없지만 FK 값은 EF Core에서 null로 간주됩니다. 이를 "개념적 null"라고 합니다.) 이를 로컬 배포자라고 합니다.

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []

현재 SaveChanges를 호출하면 분리된 게시물이 삭제됩니다. 그러나 위의 예제와 같이 Post가 SaveChanges가 호출되기 전에 새 블로그와 연결된 경우 해당 새 블로그에 적절하게 수정되며 더 이상 고아로 간주되지 않습니다.

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []

이 시점에서 호출된 SaveChanges는 삭제하지 않고 데이터베이스의 게시물을 업데이트합니다.

고아의 자동 삭제를 해제할 수도 있습니다. 고아가 추적되는 동안 SaveChanges가 호출되면 예외가 발생합니다. 예를 들어 이 코드는 다음과 같습니다.

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

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

context.SaveChanges(); // Throws

이 예외를 throw합니다.

System.InvalidOperationException: 키 값 '{BlogId: 1}'을(를) 가진 엔터티 'Blog'와 'Post' 간의 연결이 끊어졌지만 외래 키가 null을 허용하지 않기 때문에 관계가 필수로 표시되거나 암시적으로 필요합니다. 필요한 관계가 끊어질 때 종속/자식 엔터티를 삭제해야 하는 경우 연계 삭제를 사용하도록 관계를 구성합니다.

고아 삭제와 연속 삭제는 ChangeTracker.CascadeChanges()를 호출하여 언제든지 강제할 수 있습니다. 여기에 더해 고아 삭제 타이밍을 Never로 설정하면 EF Core가 명시적으로 지시하지 않는 한 고아가 삭제되지 않도록 합니다.

참조 탐색 변경

일 대 다 관계의 참조 탐색을 변경하면 관계의 다른 쪽 끝에서 컬렉션 탐색을 변경하는 것과 같은 효과가 있습니다. 종속/자식의 참조 탐색을 null로 설정하는 것은 주/부모의 컬렉션 탐색에서 엔터티를 제거하는 것과 같습니다. 모든 수정 및 데이터베이스 변경은 관계가 필요한 경우 엔터티를 분리로 만드는 것을 포함하여 이전 섹션에서 설명한 대로 수행됩니다.

선택적 일대일 관계

일대일 관계의 경우 참조 탐색을 변경하면 이전 관계가 끊어지게 됩니다. 선택적 관계의 경우 이는 이전에 관련된 종속/자식의 FK 값이 null로 설정되어 있음을 의미합니다. 예시:

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Assets).Single(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

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

context.SaveChanges();

SaveChanges를 호출하기 전의 디버그 보기는 새 자산이 기존 자산을 대체했음을 보여 줍니다. 이 자산은 이제 null BlogAssets.BlogId FK 값을 가진 Modified로 표시됩니다.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482629}
  Posts: []
BlogAssets {Id: -2147482629} Added
  Id: -2147482629 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Modified
  Id: 1 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 1
  Blog: <null>

그러면 SaveChanges가 호출되면 업데이트 및 삽입이 발생합니다.

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

-- Executed DbCommand (0ms) [Parameters=[@p2=NULL, @p3='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p2, @p3);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

필요한 일대일 관계

이전 예제와 동일한 코드를 실행하지만 이번에는 필요한 일 대 일 관계로 를 실행하면 이전에 연결된 BlogAssetsDeleted로 표시됩니다. 새 BlogAssets가 해당 위치를 차지할 때 고아가 되기 때문에 다음과 같습니다.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482639}
  Posts: []
BlogAssets {Id: -2147482639} Added
  Id: -2147482639 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Deleted
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: <null>

그러면 SaveChanges가 호출되면 삭제 및 삽입이 발생합니다.

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

-- Executed DbCommand (0ms) [Parameters=[@p1=NULL, @p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p1, @p2);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

고아를 삭제된 것으로 표시하는 타이밍은 컬렉션 탐색에 표시된 것과 동일한 방식으로 변경될 수 있으며 효과가 동일합니다.

엔터티 삭제

선택적 관계

예를 들어 DbContext.Remove를 호출하여 엔터티가 Deleted로 표시되면 삭제된 엔터티에 대한 참조가 다른 엔터티의 탐색에서 제거됩니다. 선택적 관계의 경우 종속 엔터티의 FK 값은 null로 설정됩니다.

예를 들어 Visual Studio 블로그를 Deleted로 표시해 보겠습니다.

using var context = new BlogsContext();

var vsBlog = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .Single(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

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

context.SaveChanges();

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

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Modified
  Id: 2 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 2
  Blog: <null>
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []
Post {Id: 4} Modified
  Id: 4 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: <null>
  Tags: []

다음에 유의합니다.

  • 블로그가 Deleted로 표시됩니다.
  • 삭제된 블로그와 관련된 자산에는 null FK 값(BlogId: <null> FK Modified Originally 2) 및 null 참조 탐색(Blog: <null>)이 있습니다.
  • 삭제된 블로그와 관련된 각 게시물에는 null FK 값(BlogId: <null> FK Modified Originally 2) 및 null 참조 탐색(Blog: <null>)이 있습니다.

필수 관계

필수 관계에 대한 수정 동작은 종속/자식 엔터티가 보안 주체/부모 없이 존재할 수 없으므로 Deleted로 표시되고 참조 제약 조건 예외를 방지하기 위해 SaveChanges를 호출할 때 데이터베이스에서 제거해야 한다는 점을 제외하고 선택적 관계의 경우와 동일합니다. 이를 "cascade delete"라고 하며 필요한 관계에 대한 EF Core의 기본 동작입니다. 예를 들어 이전 예제와 동일한 코드를 실행하지만 필수 관계로 실행하면 SaveChanges가 호출되기 전에 다음 디버그 보기가 생성됩니다.

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Deleted
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 3} Deleted
  Id: 3 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: []
Post {Id: 4} Deleted
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

예상대로 종속/자식은 이제 Deleted로 표시됩니다. 그러나 삭제된 엔터티의 탐색은 변경되지 않았습니다 . 이상하게 보일 수 있지만 모든 탐색을 지우면 삭제된 엔터티 그래프를 완전히 파쇄하지 않습니다. 즉, 블로그, 자산 및 게시물은 삭제된 후에도 여전히 엔터티 그래프를 형성합니다. 이렇게 하면 그래프가 파쇄된 EF6의 경우보다 엔터티 그래프의 삭제를 훨씬 쉽게 해제할 수 있습니다.

계단식 삭제 타이밍 및 다시 육아

기본적으로 하위 삭제는 부모/보안 주체가 Deleted로 표시되는 즉시 발생합니다. 이는 앞서 설명한 대로 고아를 삭제하는 경우와 동일합니다. 고아 삭제와 마찬가지로 이 프로세스는 SaveChanges가 호출될 때까지 지연되거나 적절하게 설정 ChangeTracker.CascadeDeleteTiming 하여 완전히 비활성화될 수 있습니다. 이는 보안 주체/부모를 삭제한 후 자식/종속을 다시 양육하는 경우를 포함하여 고아를 삭제하는 것과 동일한 방식으로 유용합니다.

고아 삭제뿐만 아니라 계단식 삭제는 언제든지 를 호출 ChangeTracker.CascadeChanges()하여 강제 적용할 수 있습니다. 이를 계단식 삭제 타이밍을 Never로 설정하면 EF Core가 명시적으로 지시하지 않는 한 계단식 삭제가 발생하지 않습니다.

하위 삭제 및 고아 삭제는 밀접하게 관련되어 있습니다. 두 작업은 모두 필수 보안 주체/부모에 대한 관계가 단절될 때 종속/자식 엔터티를 삭제합니다. 하위 삭제의 경우 보안 주체/부모 자체가 삭제되기 때문에 해당 단절이 발생합니다. 고아의 경우 보안 주체/부모 엔터티는 여전히 존재하지만 더 이상 종속/자식 엔터티와 관련되지 않습니다.

다대다 관계

EF Core의 다대다 관계는 조인 엔터티를 사용하여 구현됩니다. 다대다 관계의 각 측면은 일대다 관계가 있는 이 조인 엔터티와 관련이 있습니다. 이 조인 엔터티는 명시적으로 정의 및 매핑되거나 암시적으로 만들어 숨겨질 수 있습니다. 두 경우 모두 기본 동작은 동일합니다. 이 기본 동작을 먼저 살펴보고 다대다 관계 추적이 어떻게 작동하는지 이해하겠습니다.

작동할 수 있는 수 대 다 관계

명시적으로 정의된 조인 엔터티 형식을 사용하여 게시물과 태그 간에 다대다 관계를 만드는 이 EF Core 모델을 고려합니다.

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

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

PostTag 조인 엔터티 형식에는 두 개의 외래 키 속성이 포함되어 있습니다. 이 모델에서 게시물이 태그와 관련되려면 PostTag.PostId 외래 키 값이 Post.Id 기본 키 값과 일치하고 PostTag.TagId 외래 키 값이 Tag.Id 기본 키 값과 일치하는 PostTag 조인 엔터티가 있어야 합니다. 예시:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

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

이 코드를 실행한 후 변경 추적기 디버그 보기를 보면 게시물 및 태그가 새 PostTag 조인 엔터티와 관련이 있다는 것이 나타납니다.

Post {Id: 3} Unchanged
  Id: 3 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: <null>
  PostTags: [{PostId: 3, TagId: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]

PostTag에 대한 참조 탐색과 마찬가지로 PostTag의 컬렉션 탐색이 수정되었습니다. 이러한 관계는 앞의 모든 예제와 마찬가지로 FK 값 대신 탐색을 통해 조작할 수 있습니다. 예를 들어 위의 코드를 수정하여 조인 엔터티에서 참조 탐색을 설정하여 관계를 추가할 수 있습니다.

context.Add(new PostTag { Post = post, Tag = tag });

이로 인해 FK 및 탐색이 이전 예제와 정확히 동일하게 변경됩니다.

탐색 건너뛰기

조인 테이블을 수동으로 조작하는 것은 번거로울 수 있습니다. 다 대 다 관계는 조인 엔터티를 "건너뛰는" 특수 컬렉션 탐색을 사용하여 직접 조작할 수 있습니다. 예를 들어 위의 모델에 두 개의 건너뛰기 탐색을 추가할 수 있습니다. 하나는 게시에서 태그로, 다른 하나는 태그에서 게시물로:

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

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

이 다대다 관계에는 건너뛰기 탐색 및 일반 탐색이 모두 동일한 다대다 관계에 사용되도록 하려면 다음 구성이 필요합니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne(t => t.Tag).WithMany(p => p.PostTags),
            j => j.HasOne(t => t.Post).WithMany(p => p.PostTags));
}

다대다 관계 매핑에 대한 자세한 내용은 관계를 참조하세요.

건너뛰기 탐색은 일반 컬렉션 탐색처럼 보이고 동작합니다. 그러나 외래 키 값으로 작업하는 방식은 다릅니다. 이번에는 건너뛰기 탐색을 사용하여 게시물을 태그와 연결해 보겠습니다.

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

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

이 코드는 조인 엔터티를 사용하지 않습니다. 대신 일대다 관계인 경우와 동일한 방식으로 엔터티를 탐색 컬렉션에 추가합니다. 결과 디버그 보기는 기본적으로 이전과 동일합니다.

Post {Id: 3} Unchanged
  Id: 3 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: <null>
  PostTags: [{PostId: 3, TagId: 1}]
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]
  Posts: [{Id: 3}]

PostTag 조인 엔터티의 인스턴스는 이제 연결된 태그 및 게시물의 PK 값으로 설정된 FK 값을 사용하여 자동으로 생성됩니다. 이러한 FK 값과 일치하도록 모든 일반 참조 및 컬렉션 탐색이 수정되었습니다. 또한 이 모델에는 건너뛰기 탐색이 포함되어 있으므로 이러한 항목도 수정되었습니다. 특히 Post.Tags 건너뛰기 탐색에 태그를 추가했지만 이 관계의 반대편에 있는 Tag.Posts 역 건너뛰기 탐색도 연결된 게시물을 포함하도록 수정되었습니다.

탐색 건너뛰기를 맨 위에 계층화한 경우에도 기본 다 대 다 관계를 직접 조작할 수 있다는 점을 주목할 필요가 있습니다. 예를 들어 태그 및 Post는 건너뛰기 탐색을 도입하기 전에 했던 것처럼 연결될 수 있습니다.

context.Add(new PostTag { Post = post, Tag = tag });

또는 FK 값을 사용합니다.

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

이로 인해 건너뛰기 탐색이 올바르게 수정되어 이전 예제와 동일한 디버그 보기 출력이 생성됩니다.

탐색만 건너뛰기

이전 섹션에서는 두 개의 기본 일대다 관계를 완전히 정의하는 것 외에도 건너뛰기 탐색을 추가했습니다. 이는 FK 값에 발생하는 작업을 설명하는 데 유용하지만 종종 필요하지 않습니다. 대신 탐색 건너뛰기만 사용하여 다대다 관계를 정의할 수 있습니다. 이 문서의 맨 위에 있는 모델에서 다 대 다 관계가 정의되는 방식입니다. 이 모델을 사용하면 Tag.Posts 건너뛰기 탐색에 게시물을 추가하여 게시물과 태그를 다시 연결할 수 있습니다(또는 Post.Tags 건너뛰기 탐색에 태그 추가).

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

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

이 변경 후 디버그 보기를 보면 EF Core가 조인 엔터티를 나타내는 Dictionary<string, object>의 인스턴스를 만든 것으로 표시됩니다. 이 조인 엔터티에는 연결된 게시물 및 태그의 PK 값과 일치하도록 설정된 PostsIdTagsId 외래 키 속성이 모두 포함됩니다.

Post {Id: 3} Unchanged
  Id: 3 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: <null>
  Tags: [{Id: 1}]
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 1} Added
  PostsId: 3 PK FK
  TagsId: 1 PK FK

암시적 조인 엔터티 및 엔터티 형식 사용에 대한 자세한 내용은 관계Dictionary<string, object>를 참조하세요.

Important

규칙에 따라 조인 엔터티 형식에 사용되는 CLR 형식은 향후 릴리스에서 성능 향상을 위해 변경될 수 있습니다. 명시적으로 구성되지 않은 경우 조인 유형이 Dictionary<string, object>라는 데 의존하지 마세요.

페이로드를 사용하여 엔터티 조인

지금까지 모든 예제에서는 다 대 다 관계에 필요한 두 개의 외래 키 속성만 포함하는 조인 엔터티 형식(명시적 또는 암시적)을 사용했습니다. 이러한 FK 값은 관련 엔터티의 기본 키 속성에서 가져오므로 관계를 조작할 때 애플리케이션에서 명시적으로 설정할 필요가 없습니다. 이를 통해 EF Core는 데이터 누락 없이 조인 엔터티의 인스턴스를 만들 수 있습니다.

생성된 값이 있는 페이로드

EF Core는 조인 엔터티 형식에 추가 속성을 추가할 수 있도록 지원합니다. 이를 조인 엔터티에 "페이로드"를 제공하는 것으로 알려져 있습니다. 예를 들어 PostTag 조인 엔터티에 TaggedOn 속성을 추가해 보겠습니다.

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Payload
}

이 페이로드 속성은 EF Core가 조인 엔터티 인스턴스를 만들 때 설정되지 않습니다. 이 문제를 처리하는 가장 일반적인 방법은 자동으로 생성된 값으로 페이로드 속성을 사용하는 것입니다. 예를 들어 각 새 엔터티가 삽입될 때 저장소에서 생성된 타임스탬프를 사용하도록 TaggedOn 속성을 구성할 수 있습니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany(),
            j => j.Property(e => e.TaggedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

이제 이전과 동일한 방식으로 게시물에 태그를 지정할 수 있습니다.

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.SaveChanges();

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

SaveChanges를 호출한 후 변경 추적기 디버그 보기를 보면 페이로드 속성이 적절하게 설정되었음을 확인할 수 있습니다.

Post {Id: 3} Unchanged
  Id: 3 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: <null>
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Unchanged
  PostId: 3 PK FK
  TagId: 1 PK FK
  TaggedOn: '12/29/2020 8:13:21 PM'
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]

페이로드 값을 명시적으로 설정

이전 예제에서 다음을 수행하여 자동으로 생성된 값을 사용하지 않는 페이로드 속성을 추가해 보겠습니다.

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Auto-generated payload property
    public string TaggedBy { get; set; } // Not-generated payload property
}

이제 이전과 동일한 방식으로 게시물에 태그를 지정할 수 있으며 조인 엔터티는 자동으로 만들어집니다. 그런 다음 추적된 엔터티 액세스에 설명된 메커니즘 중 하나를 사용하여 이 엔터티에 액세스할 수 있습니다. 예를 들어 아래 코드는 DbSet<TEntity>.Find를 사용하여 조인 엔터티 인스턴스에 액세스합니다.

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

var joinEntity = context.Set<PostTag>().Find(post.Id, tag.Id);

joinEntity.TaggedBy = "ajcvickers";

context.SaveChanges();

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

조인 엔터티가 배치되면 일반적인 방식으로 조작할 수 있습니다. 이 예제에서는 SaveChanges를 호출하기 전에 TaggedBy 페이로드 속성을 설정합니다.

참고 항목

Find가 사용되기 전에 EF Core에서 탐색 속성 변경을 검색하고 조인 엔터티 인스턴스를 만들 수 있는 기회를 제공하려면 ChangeTracker.DetectChanges()에 대한 호출이 필요합니다. 자세한 내용은 변경 탐지 및 알림을 참조하세요.

또는 조인 엔터티를 명시적으로 만들어 게시물을 태그와 연결할 수 있습니다. 예시:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

context.Add(
    new PostTag { PostId = post.Id, TagId = tag.Id, TaggedBy = "ajcvickers" });

context.SaveChanges();

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

마지막으로 페이로드 데이터를 설정하는 또 다른 방법은 데이터베이스를 업데이트하기 전에 SaveChanges를 재정의하거나 DbContext.SavingChanges 이벤트를 사용하여 엔터티를 처리하는 것입니다. 예시:

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return base.SaveChanges();
}