추가 변경 내용 추적 기능
이 문서에서는 변경 내용 추적과 관련된 기타 기능 및 시나리오에 대해 설명합니다.
팁
이 문서에서는 엔터티 상태와 EF Core 변경 내용 추적의 기본 사항을 이해한다고 가정합니다. 이러한 항목에 대한 자세한 내용은 EF Core의 변경 내용 추적을 참조하세요.
팁
GitHub에서 샘플 코드를 다운로드하여 이 문서의 모든 코드를 실행하고 디버그할 수 있습니다.
Add
및 AddAsync
EF Core(Entity Framework Core)는 해당 메서드를 사용하면 데이터베이스 상호 작용이 발생할 때마다 비동기 메서드를 제공합니다. 고성능 비동기 액세스를 지원하지 않는 데이터베이스를 사용할 때 오버헤드를 방지하기 위해 동기 메서드도 제공됩니다.
이러한 메서드는 기본적으로 엔터티 추적을 시작하기 때문에 DbContext.Add 및 DbSet<TEntity>.Add는 일반적으로 데이터베이스에 액세스하지 않습니다. 그러나 일부 형식의 값 생성은 키 값을 생성하기 위해 데이터베이스에 액세스할 수 있습니다 . 이 작업을 수행하고 EF Core와 함께 제공되는 유일한 값 생성기는 HiLoValueGenerator<TValue>입니다. 이 생성기를 사용하는 것은 일반적이지 않습니다. 기본적으로 구성되지 않습니다. 즉, 대부분의 애플리케이션은 AddAsync
가 아닌 Add
를 사용해야 합니다.
Update
, Attach
, Remove
와 다른 유사한 메서드는 새 키 값을 생성하지 않아 비동기 오버로드가 없으므로 데이터베이스에 액세스할 필요가 없습니다.
AddRange
, UpdateRange
, AttachRange
및 RemoveRange
DbSet<TEntity> 및 DbContext는 단일 호출에서 여러 인스턴스를 허용하는 Add
, Update
, Attach
, Remove
의 대체 버전을 제공합니다. 이러한 메서드는 각각 AddRange, UpdateRange, AttachRange, RemoveRange입니다.
이러한 메서드는 편의를 위해 제공됩니다. "range" 메서드를 사용하면 동일한 비 범위 메서드에 대한 여러 호출과 동일한 기능이 있습니다. 두 방법 간에는 상당한 성능 차이가 없습니다.
참고 항목
이는 AddRange
및 Add
모두 자동으로 DetectChanges
를 호출되는 EF6와 다르지만 Add
를 여러 번 호출하면 DetectChanges가 한 번이 아닌 여러 번 호출됩니다. 이로 인해 AddRange
는 EF6에서 더 효율적이게 되었습니다. EF Core에서는 이러한 메서드 중 어느 것도 DetectChanges
를 자동으로 호출하지 않습니다.
DbContext 및 DbSet 메서드
Add
, Update
, Attach
, Remove
를 비롯한 많은 메서드에는 DbSet<TEntity> 및 DbContext 모두에 대한 구현이 있습니다. 이러한 메서드는 일반 엔터티 형식에 대해 정확히 동일한 동작을 갖습니다. 엔터티의 CLR 형식이 EF Core 모델에서 하나의 엔터티 형식과 하나의 엔터티 형식에만 매핑되었기 때문입니다. 따라서 CLR 형식은 엔터티가 모델에 맞는 위치를 완전히 정의하므로 사용할 DbSet를 암시적으로 확인할 수 있습니다.
이 규칙의 예외는 주로 다대다 조인 엔터티에 사용되는 공유 형식 엔터티 형식을 사용하는 경우입니다. 공유 형식 엔터티 형식을 사용하는 경우 사용 중인 EF Core 모델 형식에 대해 먼저 DbSet을 만들어야 합니다. 그러면 어떤 EF Core 모델 형식이 사용되는지에 대한 모호함 없이 DbSet에서 Add
, Update
, Attach
, Remove
와 같은 메서드를 사용할 수 있습니다.
공유 형식 엔터티 형식은 다대다 관계의 조인 엔터티에 기본적으로 사용됩니다. 다대다 관계에서 사용할 수 있도록 공유 형식 엔터티 형식을 명시적으로 구성할 수도 있습니다. 예를 들어 아래 코드는 Dictionary<string, int>
를 조인 엔터티 형식으로 구성합니다.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.SharedTypeEntity<Dictionary<string, int>>(
"PostTag",
b =>
{
b.IndexerProperty<int>("TagId");
b.IndexerProperty<int>("PostId");
});
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, int>>(
"PostTag",
j => j.HasOne<Tag>().WithMany(),
j => j.HasOne<Post>().WithMany());
}
외래 키 및 탐색을 변경하면 새 조인 엔터티 인스턴스를 추적하여 두 엔터티를 연결하는 방법을 보여 줍니다. 아래 코드는 조인 엔터티에 사용되는 Dictionary<string, int>
공유 형식 엔터티 형식에 대해 이 작업을 수행합니다.
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
DbContext.Set<TEntity>(String)는 PostTag
엔터티 형식에 대한 DbSet을 만드는 데 사용됩니다. 그런 다음 이 DbSet을 사용하여 새 조인 엔터티 인스턴스로 Add
를 호출할 수 있습니다.
Important
규칙에 따라 조인 엔터티 형식에 사용되는 CLR 형식은 향후 릴리스에서 성능 향상을 위해 변경될 수 있습니다. 위의 코드에서 Dictionary<string, int>
에 대해 수행된 대로 명시적으로 구성되지 않은 경우 특정 조인 엔터티 형식에 의존하지 마세요.
속성과 필드 액세스 비교
엔터티 속성에 대한 액세스는 기본적으로 속성의 지원 필드를 사용합니다. 이는 효율적이며 속성 getter 및 setter를 호출하여 부작용을 트리거하지 않습니다. 예를 들어 지연 로드는 무한 루프 트리거를 방지할 수 있는 방법입니다. 모델에서 지원 필드를 구성하는 방법에 대한 자세한 내용은 지원 필드를 참조하세요.
경우에 따라 EF Core가 속성 값을 수정할 때 부작용을 생성하는 것이 바람직할 수 있습니다. 예를 들어 엔터티에 데이터를 바인딩할 때 속성을 설정하면 필드를 직접 설정할 때 발생하지 않는 U.I.에 대한 알림이 생성됩니다. 다음을 위해 PropertyAccessMode를 변경하여 이 작업을 수행할 수 있습니다.
- ModelBuilder.UsePropertyAccessMode를 사용하는 모델의 모든 엔터티 형식
- EntityTypeBuilder<TEntity>.UsePropertyAccessMode를 사용하여 특정 엔터티 형식의 모든 속성 및 탐색
- PropertyBuilder.UsePropertyAccessMode를 사용하는 특정 속성
- NavigationBuilder.UsePropertyAccessMode를 사용하는 특정 탐색
속성 액세스 모드 Field
및 PreferField
는 EF Core가 지원 필드를 통해 속성 값에 액세스하도록 합니다. 마찬가지로 Property
및 PreferProperty
는 EF Core가 getter 및 setter를 통해 속성 값에 액세스하도록 합니다.
Field
또는 Property
가 사용되고 EF Core가 각각 필드 또는 속성 getter/setter를 통해 값에 액세스할 수 없는 경우 EF Core는 예외를 throw합니다. 이렇게 하면 EF Core가 항상 필드/속성 액세스를 사용할 수 있습니다.
반면에 PreferField
및 PreferProperty
모드는 기본 설정을 사용할 수 없는 경우 속성 또는 지원 필드를 각각 사용하는 것으로 대체됩니다. 기본값은 PreferField
입니다. 즉, EF Core는 가능할 때마다 필드를 사용하지만 대신 getter 또는 setter를 통해 속성에 액세스해야 하는 경우 실패하지 않습니다.
FieldDuringConstruction
및 PreferFieldDuringConstruction
은 엔터티 인스턴스를 만들 때만 지원 필드를 사용하도록 EF Core를 구성합니다. 이렇게 하면 getter 및 setter 부작용 없이 쿼리를 실행할 수 있으며, 이후 EF Core의 속성 변경으로 인해 이러한 부작용이 발생합니다.
다른 속성 액세스 모드는 다음 표에 요약되어 있습니다.
PropertyAccessMode | 기본 설정 | 엔터티 만들기 기본 설정 | 대체 | 엔터티 만들기 대체 |
---|---|---|---|---|
Field |
필드 | 필드 | Throw | Throw |
Property |
속성 | 속성 | Throw | Throw |
PreferField |
필드 | 필드 | 속성 | 속성 |
PreferProperty |
속성 | 속성 | 필드 | 필드 |
FieldDuringConstruction |
속성 | 필드 | 필드 | Throw |
PreferFieldDuringConstruction |
속성 | 필드 | 필드 | 속성 |
임시 값
EF Core는 SaveChanges가 호출될 때 데이터베이스에서 생성된 실제 키 값이 있는 새 엔터티를 추적할 때 임시 키 값을 만듭니다. 이러한 임시 값이 사용되는 방법에 대한 개요는 EF Core의 변경 내용 추적을 참조하세요.
임시 값에 액세스
임시 값은 변경 추적기에서 저장되며 엔터티 인스턴스에 직접 설정되지 않습니다. 그러나 이러한 임시 값은 추적된 엔터티에 액세스하기 위한 다양한 메커니즘을 사용할 때 노출됩니다. 예를 들어 다음 코드는 를 사용하여 EntityEntry.CurrentValues를 사용하여 임시 값에 액세스합니다.
using var context = new BlogsContext();
var blog = new Blog { Name = ".NET Blog" };
context.Add(blog);
Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");
이 코드의 출력은 다음과 같습니다.
Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643
PropertyEntry.IsTemporary를 사용하여 임시 값을 확인할 수 있습니다.
임시 값 조작
임시 값을 명시적으로 사용하는 것이 유용한 경우도 있습니다. 예를 들어 웹 클라이언트에서 새 엔터티 컬렉션을 만든 다음 서버로 다시 직렬화할 수 있습니다. 외래 키 값은 이러한 엔터티 간의 관계를 설정하는 한 가지 방법입니다. 다음 코드는 SaveChanges가 호출될 때 실제 키 값을 생성하도록 허용하면서 이 방법을 사용하여 외래 키로 새 엔터티 그래프를 연결합니다.
var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };
var posts = new List<Post>
{
new Post
{
Id = -1,
BlogId = -1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = -2,
BlogId = -2,
Title = "Disassembly improvements for optimized managed debugging",
Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
}
};
using var context = new BlogsContext();
foreach (var blog in blogs)
{
context.Add(blog).Property(e => e.Id).IsTemporary = true;
}
foreach (var post in posts)
{
context.Add(post).Property(e => e.Id).IsTemporary = true;
}
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
다음에 유의합니다.
- 음수는 임시 키 값으로 사용됩니다. 이는 필수는 아니지만 주요 충돌을 방지하기 위한 일반적인 규칙입니다.
Post.BlogId
FK 속성에는 연결된 블로그의 PK와 동일한 음수 값이 할당됩니다.- PK 값은 각 엔터티가 추적된 후 IsTemporary를 설정하여 임시로 표시됩니다. 이는 애플리케이션에서 제공하는 모든 키 값이 실제 키 값으로 간주되기 때문에 필요합니다.
SaveChanges를 호출하기 전에 변경 추적기 디버그 보기를 보면 PK 값이 임시로 표시되고 게시물이 탐색 수정을 포함하여 올바른 블로그와 연결되어 있음을 보여 줍니다.
Blog {Id: -2} Added
Id: -2 PK Temporary
Name: 'Visual Studio Blog'
Posts: [{Id: -2}]
Blog {Id: -1} Added
Id: -1 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -1}]
Post {Id: -2} Added
Id: -2 PK Temporary
BlogId: -2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: -2}
Tags: []
Post {Id: -1} Added
Id: -1 PK Temporary
BlogId: -1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: -1}
SaveChanges를 호출한 후 이러한 임시 값은 데이터베이스에서 생성된 실제 값으로 대체되었습니다.
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Posts: [{Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
기본값 작업
EF Core를 사용하면 SaveChanges가 호출되면 속성이 데이터베이스에서 기본값을 가져올 수 있습니다. 생성된 키 값과 마찬가지로 EF Core는 명시적으로 설정된 값이 없는 경우에만 데이터베이스의 기본값을 사용합니다. 예를 들어 다음 엔터티 형식을 살펴봅니다.
public class Token
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime ValidFrom { get; set; }
}
ValidFrom
속성은 데이터베이스에서 기본값을 얻도록 구성됩니다.
modelBuilder
.Entity<Token>()
.Property(e => e.ValidFrom)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
이 형식의 엔터티를 삽입할 때 EF Core는 명시적 값이 대신 설정되지 않은 한 데이터베이스에서 값을 생성하도록 합니다. 예시:
using var context = new BlogsContext();
context.AddRange(
new Token { Name = "A" },
new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
변경 추적기 디버그 보기를 보면 첫 번째 토큰이 데이터베이스에 의해 ValidFrom
이 생성되도록 한 반면 두 번째 토큰은 명시적으로 설정된 값을 사용했음을 보여줍니다.
Token {Id: 1} Unchanged
Id: 1 PK
Name: 'A'
ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
Id: 2 PK
Name: 'B'
ValidFrom: '11/11/1111 11:11:11 AM'
참고 항목
데이터베이스 기본값을 사용하려면 데이터베이스 열에 기본값 제약 조건이 구성되어 있어야 합니다. 이 작업은 또는 HasDefaultValueSql 또는 HasDefaultValue를 사용할 때 EF Core 마이그레이션에 의해 자동으로 수행됩니다. EF Core 마이그레이션을 사용하지 않을 때는 다른 방법으로 열에 기본 제약 조건을 만들어야 합니다.
nullable 속성 사용
EF Core는 속성 값을 해당 형식의 CLR 기본값과 비교하여 속성이 설정되었는지 여부를 확인할 수 있습니다. 대부분의 경우 잘 작동하지만 CLR 기본값을 데이터베이스에 명시적으로 삽입할 수 없음을 의미합니다. 예를 들어 정수 속성이 있는 엔터티를 고려합니다.
public class Foo1
{
public int Id { get; set; }
public int Count { get; set; }
}
데이터베이스 기본값이 -1이 되도록 해당 속성이 구성된 위치:
modelBuilder
.Entity<Foo1>()
.Property(e => e.Count)
.HasDefaultValue(-1);
명시적 값이 설정되지 않을 때마다 기본값 -1이 사용됩니다. 그러나 값을 0(정수의 CLR 기본값)으로 설정하면 값을 설정하지 않고 EF Core와 구별할 수 없습니다. 즉, 이 속성에 대해 0을 삽입할 수 없습니다. 예시:
using var context = new BlogsContext();
var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);
Count
가 명시적으로 0으로 설정된 인스턴스는 의도한 것이 아닌 데이터베이스에서 기본값을 계속 가져옵니다. 이 문제를 처리하는 쉬운 방법은 Count
속성을 null 허용으로 만드는 것입니다.
public class Foo2
{
public int Id { get; set; }
public int? Count { get; set; }
}
이렇게 하면 0이 아닌 CLR 기본 null이 만들어집니다. 즉, 명시적으로 설정할 때 0이 삽입됩니다.
using var context = new BlogsContext();
var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
nullable 지원 필드 사용
도메인 모델에서 개념적으로 null을 허용하지 않을 수 있는 속성을 null 허용으로 만드는 문제입니다. 따라서 속성을 null 허용으로 강제 적용하면 모델이 손상됩니다.
속성은 null을 허용하지 않고 백업 필드만 null 허용 상태로 둘 수 있습니다. 예시:
public class Foo3
{
public int Id { get; set; }
private int? _count;
public int Count
{
get => _count ?? -1;
set => _count = value;
}
}
이렇게 하면 속성이 명시적으로 0으로 설정된 경우 CLR 기본값(0)을 삽입할 수 있지만 도메인 모델에서 속성을 nullable로 노출할 필요가 없습니다. 예시:
using var context = new BlogsContext();
var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
bool 속성에 대한 Null 허용 지원 필드
이 패턴은 저장소에서 생성된 기본값과 함께 bool 속성을 사용할 때 특히 유용합니다. bool
에 대한 CLR 기본값은 "false"이므로 일반 패턴을 사용하여 "false"를 명시적으로 삽입할 수 없습니다. 예를 들어 User
엔터티 형식을 고려합니다.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
private bool? _isAuthorized;
public bool IsAuthorized
{
get => _isAuthorized ?? true;
set => _isAuthorized = value;
}
}
IsAuthorized
속성은 데이터베이스 기본값 "true"로 구성됩니다.
modelBuilder
.Entity<User>()
.Property(e => e.IsAuthorized)
.HasDefaultValue(true);
IsAuthorized
속성을 삽입하기 전에 명시적으로 "true" 또는 "false"로 설정하거나 데이터베이스 기본값이 사용되는 경우 설정되지 않은 상태로 두면 됩니다.
using var context = new BlogsContext();
var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!
context.AddRange(userA, userB, userC);
context.SaveChanges();
SQLite를 사용할 때 SaveChanges의 출력은 데이터베이스 기본값이 Mac에 사용되는 반면 Alice 및 Baxter에 대해 명시적 값이 설정됨을 보여 줍니다.
-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
스키마 기본값만
경우에 따라 삽입에 이러한 값을 사용하지 않고 EF Core 마이그레이션에서 만든 데이터베이스 스키마에 기본값을 사용하는 것이 유용합니다. 속성을 PropertyBuilder.ValueGeneratedNever로 구성하여 이 작업을 수행할 수 있습니다. 예:
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();
.NET