다음을 통해 공유


추가 변경 내용 추적 기능

이 문서에서는 변경 내용 추적과 관련된 기타 기능 및 시나리오에 대해 설명합니다.

팁 (조언)

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

팁 (조언)

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

AddAddAsync

EF Core(Entity Framework Core)는 해당 메서드를 사용하면 데이터베이스 상호 작용이 발생할 때마다 비동기 메서드를 제공합니다. 고성능 비동기 액세스를 지원하지 않는 데이터베이스를 사용할 때 오버헤드를 방지하기 위해 동기 메서드도 제공됩니다.

DbContext.Add DbSet<TEntity>.Add 이러한 메서드는 기본적으로 엔터티 추적을 시작하기 때문에 일반적으로 데이터베이스에 액세스하지 마세요. 그러나 키 값을 생성하기 위해 일부 형식의 값 생성이 데이터베이스에 액세스할 수 있습니다 . 이 작업을 수행하고 EF Core와 함께 제공되는 유일한 값 생성기는 바로 HiLoValueGenerator<TValue>입니다. 이 생성기를 사용하는 것은 일반적이지 않습니다. 기본적으로 구성되지 않습니다. 즉, 대부분의 애플리케이션은 Add을 사용해야 하고, AddAsync을 사용하지 않아야 합니다.

와 같은 UpdateAttach다른 유사한 메서드는 Remove 새 키 값을 생성하지 않으므로 비동기 오버로드가 없으므로 데이터베이스에 액세스할 필요가 없습니다.

AddRange, UpdateRange, AttachRangeRemoveRange

DbSet<TEntity>DbContext는 단일 호출에서 여러 인스턴스를 허용하는 Add, Update, Attach, 및 Remove의 대체 버전을 제공합니다. 이러한 메서드는 각각 AddRange, UpdateRange, AttachRange, 그리고 RemoveRange입니다.

이러한 메서드는 편의를 위해 제공됩니다. "range" 메서드를 사용하면 해당 비 범위 메서드에 대한 여러 호출과 동일한 기능이 있습니다. 두 방법 사이에는 큰 성능 차이가 없습니다.

비고

이는 EF6과 다르게, AddRangeAdd이 자동으로 DetectChanges를 호출하지만, Add를 여러 번 호출할 경우 DetectChanges가 한 번이 아니라 여러 번 호출됩니다. 이렇게 하면 AddRange EF6에서 더 효율적으로 작업할 수 있습니다. EF Core에서는 이러한 메서드 중 어느 것도 자동으로 호출 DetectChanges되지 않습니다.

DbContext 및 DbSet 메서드

Add, Update, Attach, 및 Remove를 포함한 많은 메서드는 DbSet<TEntity>DbContext에서 모두 구현됩니다. 이러한 메서드는 일반 엔터티 형식 에 대해 정확히 동일한 동작 을 갖습니다. 엔터티의 CLR 형식은 EF Core 모델에서 하나의 엔터티 형식에만 매핑되기 때문입니다. 따라서 CLR 형식은 엔터티가 모델에 맞는 위치를 완전히 정의하므로 사용할 DbSet을 암시적으로 확인할 수 있습니다.

이 규칙의 예외는 주로 다대다 조인 엔터티에 사용되는 공유 형식 엔터티 형식을 사용하는 경우입니다. 공유 형식 엔터티 형식을 사용하는 경우 사용 중인 EF Core 모델 형식에 대해 먼저 DbSet을 만들어야 합니다. 와 AddUpdateAttach 같은 Remove메서드는 EF Core 모델 형식이 사용되는지 모호하지 않고 DbSet에서 사용할 수 있습니다.

공유 형식 엔터티 형식은 기본적으로 다대다 관계의 조인 엔터티에 사용됩니다. 다 대 다 관계에서 사용하도록 공유 형식 엔터티 형식을 명시적으로 구성할 수도 있습니다. 예를 들어 아래 코드는 조인 엔터티 형식으로 구성됩니다 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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(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);

await context.SaveChangesAsync();

DbContext.Set<TEntity>(String) 엔터티 형식을 위한 DbSet을 생성하는 데 PostTag가 사용된다는 점에 주목하십시오. 그런 다음 이 DbSet을 사용하여 새 조인 엔터티 인스턴스를 호출 Add 할 수 있습니다.

중요합니다

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

속성과 필드 액세스 비교

엔터티 속성에 대한 액세스는 기본적으로 속성의 지원 필드를 사용합니다. 이는 효율적이며 속성 getter 및 setter를 호출할 때 부작용을 유발하지 않도록 합니다. 예를 들어 지연 로드는 무한 루프 트리거를 방지할 수 있는 방법입니다. 모델에서 지원 필드를 구성하는 방법에 대한 자세한 내용은 지원 필드를 참조하세요.

속성 값을 수정할 때 EF Core에서 부작용을 생성하는 것이 바람직할 수 있습니다. 예를 들어 엔터티에 데이터를 바인딩할 때 속성을 설정하면 필드를 직접 설정할 때 발생하지 않는 U.I.에 대한 알림이 생성됩니다. 이 작업은 PropertyAccessMode을 변경하여 수행할 수 있습니다.

속성 액세스 모드 FieldPreferField는 EF Core가 백킹 필드를 통해 속성 값에 액세스하게 만듭니다. 마찬가지로 PropertyPreferProperty은 EF Core가 getter와 setter 메서드를 통해 속성 값에 접근하도록 합니다.

Field 또는 Property가 사용되고 EF Core가 해당 필드 또는 속성 getter/setter를 통해 값에 액세스할 수 없는 경우, EF Core는 예외를 발생시킵니다. 이렇게 하면 EF Core가 필드/속성 액세스를 사용한다고 생각하는 경우 항상 그렇게 되도록 보장합니다.

반면에 PreferFieldPreferProperty 모드는 선호되는 접근 방식을 사용 불가능할 경우 각각 속성 또는 지원 필드를 사용하도록 대체됩니다. PreferField 기본값입니다. 즉, EF Core는 가능할 때마다 필드를 사용하지만 대신 getter 또는 setter를 통해 속성에 액세스해야 하는 경우 실패하지 않습니다.

FieldDuringConstructionPreferFieldDuringConstruction엔티티 인스턴스를 생성할 때만 지원 필드를 사용하도록 EF Core를 구성합니다. 이렇게 하면 getter 및 setter 부작용 없이 쿼리를 실행할 수 있지만 나중에 EF Core의 속성을 변경하면 이러한 부작용이 발생합니다.

다른 속성 액세스 모드는 다음 표에 요약되어 있습니다.

속성접근방식 선호 엔터티 만들기 기본 설정 대체 엔터티 생성의 대체 방법
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);

await context.SaveChangesAsync();

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

await context.SaveChangesAsync();

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);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

명시적으로 0으로 설정된 인스턴스 Count 는 의도한 것이 아닌 데이터베이스에서 기본값을 계속 가져옵니다. 이 문제를 처리하는 쉬운 방법은 Count 속성을 null 가능으로 설정하는 것입니다.

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

이렇게 하면 CLR 기본 null이 0이 아닌 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);
await context.SaveChangesAsync();

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

이렇게 하면 도메인 모델에서 속성을 nullable로 노출할 필요가 없지만 속성이 명시적으로 0으로 설정된 경우 CLR 기본값(0)을 삽입할 수 있습니다. 다음은 그 예입니다.

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);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

bool 속성의 Null 허용 백킹 필드

이 패턴은 저장소에서 생성된 기본값과 함께 bool 속성을 사용할 때 특히 유용합니다. CLR 기본값 bool 은 "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);

await context.SaveChangesAsync();

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