값 비교자

이 문서의 코드는 GitHub에서 실행 가능한 샘플로 찾을 수 있습니다.

배경

변경 내용 추적은 EF Core가 로드된 엔터티 인스턴스에서 애플리케이션이 수행한 변경 내용을 자동으로 결정하므로 SaveChanges가 호출될 때 해당 변경 내용을 데이터베이스에 다시 저장할 수 있습니다. EF Core는 일반적으로 데이터베이스에서 로드될 때 인스턴스의 스냅샷을 만들고 해당 스냅샷을 애플리케이션에 전달된 인스턴스와 비교하여 이 작업을 수행합니다.

EF Core에는 데이터베이스에 사용되는 대부분의 표준 형식을 스냅샷하고 비교하기 위한 기본 제공 논리가 제공되므로 사용자는 일반적으로 이 항목에 대해 걱정할 필요가 없습니다. 그러나 속성이 값 변환기를 통해 매핑되는 경우 EF Core는 복잡할 수 있는 임의의 사용자 유형에 대한 비교를 수행해야 합니다. 기본적으로 EF Core는 형식(예: Equals 메서드)에 정의된 기본 같음 비교를 사용합니다. 스냅샷의 경우 값 형식이 복사되어 스냅샷을 생성하지만 참조 형식의 경우 복사가 발생하지 않으며 동일한 인스턴스가 스냅샷으로 사용됩니다.

기본 제공 비교 동작이 적절하지 않은 경우 사용자는 해시 코드를 스냅샷, 비교 및 계산하기 위한 논리를 포함하는 값 비교자를 제공할 수 있습니다. 예를 들어 다음에서는 List<int> 속성에 대한 값 변환을 데이터베이스의 JSON 문자열로 변환되도록 설정하고 적절한 값 비교자도 정의합니다.

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyListProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
        new ValueComparer<List<int>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()));

자세한 내용은 아래의 변경 가능한 클래스를 참조하세요.

관계를 확인할 때 두 키 값이 같은지 여부를 결정할 때도 값 비교자가 사용됩니다. 이에 대한 설명은 아래에 있습니다.

얕은 비교와 심층 비교

int와 같은 변경할 수 없는 작은 값 형식의 경우 EF Core의 기본 논리가 잘 작동합니다. 이 값은 스냅샷을 만들 때 그대로 복사되고 형식의 기본 제공 같음 비교와 비교됩니다. 고유한 값 비교자를 구현할 때 심층 비교 또는 단순 비교(및 스냅샷) 논리가 적절한지 여부를 고려하는 것이 중요합니다.

임의로 클 수 있는 바이트 배열을 고려합니다. 다음을 비교할 수 있습니다.

  • 참고로, 새 바이트 배열이 사용되는 경우에만 차이가 감지됩니다.
  • 자세히 비교하면 배열의 바이트 변형이 검색됩니다.

기본적으로 EF Core는 키가 아닌 바이트 배열에 이러한 방법 중 첫 번째 방법을 사용합니다. 즉, 참조만 비교되고 기존 바이트 배열을 새 바이트 배열로 대체할 때만 변경 내용이 검색됩니다. 이는 SaveChanges를 실행할 때 전체 배열을 복사하고 바이트 대 바이트를 비교하지 않는 실용적인 결정입니다. 즉, 한 이미지를 다른 이미지로 바꾸는 일반적인 시나리오가 성능이 좋은 방식으로 처리됩니다.

반면에 바이트 배열을 사용하여 이진 키를 나타내는 경우 참조 같음이 작동하지 않습니다. FK 속성이 비교해야 하는 PK 속성과 동일한 인스턴스로 설정될 가능성은 매우 낮기 때문에 이진 키를 나타냅니다. 따라서 EF Core는 키 역할을 하는 바이트 배열에 대해 심층 비교를 사용합니다. 이진 키는 일반적으로 짧기 때문에 성능이 크게 저하될 가능성이 낮습니다.

선택한 비교 및 스냅샷 논리는 서로 일치해야 합니다. 심층 비교를 수행하려면 딥 스냅샷이 올바르게 작동해야 합니다.

변경할 수 없는 간단한 클래스

값 변환기를 사용하여 변경할 수 없는 간단한 클래스를 매핑하는 속성을 고려합니다.

public sealed class ImmutableClass
{
    public ImmutableClass(int value)
    {
        Value = value;
    }

    public int Value { get; }

    private bool Equals(ImmutableClass other)
        => Value == other.Value;

    public override bool Equals(object obj)
        => ReferenceEquals(this, obj) || obj is ImmutableClass other && Equals(other);

    public override int GetHashCode()
        => Value.GetHashCode();
}
modelBuilder
    .Entity<MyEntityType>()
    .Property(e => e.MyProperty)
    .HasConversion(
        v => v.Value,
        v => new ImmutableClass(v));

이 형식의 속성은 다음과 같은 이유로 특별한 비교 또는 스냅샷이 필요하지 않습니다.

  • 서로 다른 인스턴스가 올바르게 비교되도록 같음이 재정의됩니다.
  • 형식은 변경할 수 없으므로 스냅샷 값이 변경될 가능성이 없습니다.

따라서 이 경우 EF Core의 기본 동작은 그대로 적용됩니다.

변경할 수 없는 간단한 구조체

간단한 구조체에 대한 매핑도 간단하며 특별한 비교자 또는 스냅샷이 필요하지 않습니다.

public readonly struct ImmutableStruct
{
    public ImmutableStruct(int value)
    {
        Value = value;
    }

    public int Value { get; }
}
modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyProperty)
    .HasConversion(
        v => v.Value,
        v => new ImmutableStruct(v));

EF Core는 구조체 속성의 컴파일된 멤버별 비교 생성을 기본적으로 지원합니다. 즉, 구조체는 EF Core에 대해 같음이 재정의되지 않아도 되지만 다른 이유로 이 작업을 계속 선택할 수 있습니다. 또한 구조체는 변경할 수 없으며 항상 멤버로 복사되므로 특별한 스냅샷이 필요하지 않습니다. 변경 가능한 구조체의 경우에도 마찬가지이지만 변경 가능한 구조체는 일반적으로 피해야 합니다.

변경 가능한 클래스

가능하면 변경할 수 없는 형식(클래스 또는 구조체)을 값 변환기와 함께 사용하는 것이 좋습니다. 일반적으로 더 효율적이며 변경 가능한 형식을 사용하는 것보다 더 명확한 의미 체계가 있습니다. 그러나 애플리케이션에서 변경할 수 없는 형식의 속성을 사용하는 것이 일반적입니다. 예를 들어 숫자 목록을 포함하는 속성을 매핑합니다.

public List<int> MyListProperty { get; set; }

List<T> 클래스:

  • 참조 같음이 있습니다. 동일한 값을 포함하는 두 개의 목록이 다른 것으로 처리됩니다.
  • 변경할 수 있습니다. 목록의 값을 추가하고 제거할 수 있습니다.

목록 속성의 일반적인 값 변환은 목록을 JSON으로 변환할 수 있습니다.

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyListProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
        new ValueComparer<List<int>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()));

ValueComparer<T> 생성자는 다음 세 가지 식을 허용합니다.

  • 같음 확인을 위한 식
  • 해시 코드를 생성하기 위한 식
  • 값을 스냅샷할 식

이 경우 숫자 시퀀스가 같은지 확인하여 비교를 수행합니다.

마찬가지로 해시 코드는 이 동일한 시퀀스에서 빌드됩니다. (변경 가능한 값에 대한 해시 코드이므로 문제를 일으킬 수 있습니다. 가능한 경우 변경할 수 없도록 하세요.)

ToList로 목록을 복제하여 스냅샷이 만들어집니다. 다시 말하지만 목록이 변경될 경우에만 필요합니다. 가능한 경우 변경할 수 없습니다.

참고 항목

값 변환기 및 비교자는 단순 대리자가 아닌 식을 사용하여 생성됩니다. EF Core는 이러한 식을 훨씬 더 복잡한 식 트리에 삽입한 다음 엔터티 셰이퍼 대리자로 컴파일하기 때문입니다. 개념적으로 컴파일러 인라인 처리와 비슷합니다. 예를 들어 단순 변환은 변환을 수행하는 다른 메서드에 대한 호출이 아니라 캐스트에서 컴파일된 것일 수 있습니다.

키 비교자

백그라운드 섹션에서는 키 비교에 특별한 의미 체계가 필요한 이유를 설명합니다. 기본, 보안 주체 또는 외래 키 속성에서 키를 설정할 때 키에 적합한 비교자를 만들어야 합니다.

동일한 속성에 다른 의미 체계가 필요한 드문 경우에 SetKeyValueComparer를 사용합니다.

참고 항목

SetStructuralValueComparer는 사용되지 않습니다. 대신 SetKeyValueComparer을 사용합니다.

기본 비교자 재정의

경우에 따라 EF Core에서 사용하는 기본 비교가 적절하지 않을 수 있습니다. 예를 들어 바이트 배열의 변형은 기본적으로 EF Core에서 검색되지 않습니다. 속성에 다른 비교자를 설정하여 재정의할 수 있습니다.

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyBytes)
    .Metadata
    .SetValueComparer(
        new ValueComparer<byte[]>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToArray()));

EF Core는 이제 바이트 시퀀스를 비교하므로 바이트 배열 변형을 검색합니다.