Skip to main content

Command Palette

Search for a command to run...

[EF Core] 엔터티 속성을 JSON 열에 매핑하는 방법

Updated
3 min read

EF Core는 제한적인 JSON 속성을 지원합니다.

EF Core 8에 추가된 기본 형식 컬렉션으로 JSON 열에 매핑할 수 있습니다.

public class PrimitiveCollections
{
    public IEnumerable<int> Ints { get; set; }
    public ICollection<string> Strings { get; set; }
    public IList<DateOnly> Dates { get; set; }
    public uint[] UnsignedInts { get; set; }
    public List<bool> Booleans { get; set; }
    public List<Uri> Urls { get; set; }
}

Dictionary<TKey, TValue>는 아직 지원하지 않습니다.

다른 방식으로 소유 엔터티 유형(Owned Entity Types)을 이용하는 방법입니다. 이 방식을 이용하면 JSON 열에 매핑 가능합니다.

| JSON 열에 매핑할 구조 (예시)

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

그리고 이것을 Author 엔터티에 적용합니다.

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; }
}

Contract 속성이 소유 엔터티 유형이라는 것을 알리기 위해 다음처럼 등록을 합니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToJson(); // <-
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

ownedNavigationBuilder.ToJson()로 인해 Contact 속성이 JSON 열이 됩니다. Author.Contract -> Address을 OwnsOne()으로 지정해야 하는 이유는 해당 속성을 쿼리할 수 있게 하기 위함입니다.

컬렉션도 OwnsMany()을 이용해 사용 가능하지만 안타깝게도 사전 형식(Dictionary<TKey, TValue>)은 지원하지 않습니다.

번외:사전을 포함한 자유로운 사용자 유형을 JSON으로 저장하고자 한다면?

ValueConverter를 사용해서 문자열 JSON <-> 사용자 인스턴스로 변환하여 사용하는 방법으로 JSON 속성으로 쿼리 하지는 못하지만 JSON으로 저장하고자 하는 목적에 그나마 부합하는 방법이 아닐까 합니다.

다음처럼 인터페이스 및 ValueConverter를 구성한 후

public interface IHaveStringValue<TModel> : IHaveValueWithLength<TModel, string>
{
}

public interface IHaveValue<TModel, TProvider>
{
    public abstract static TModel Create(TProvider Value);

    TProvider Value { get; }
}

public interface IHaveValueWithLength<TModel, TProvider> : IHaveValue<TModel, TProvider>
{
    public abstract static int MaxLength { get; }
}

public class ValueWithLengthConverter<TModel, TProvider> : ValueConverter<TModel, TProvider>
    where TModel : IHaveValueWithLength<TModel, TProvider>
{
    public ValueWithLengthConverter() : base(
        v => v.Value,
        v => Convert(v),
        new ConverterMappingHints(size: TModel.MaxLength)
    )
    {
    }

    private static TModel Convert(TProvider value) => TModel.Create(value);
}

public class StringValueConverter<TModel> : ValueWithLengthConverter<TModel, string>
    where TModel : IHaveValueWithLength<TModel, string>
{
}

JsonValue<TModel>을 정의 합니다.

public record JsonValue<TType>(TType Value) : IHaveStringValue<JsonValue<TType>>
{
    public static int MaxLength => 4192;

    string IHaveValue<JsonValue<TType>, string>.Value => JsonSerializer.Serialize(Value);

    public static JsonValue<TType> Create(string Value) => new(JsonSerializer.Deserialize<TType>(Value)!);
    public static JsonValue<TType> New() => Create("{}");
}

DbContext에 등록을 하고,

            builder
                .Properties<JsonValue<폼속성>>()
                .HaveConversion<StringValueConverter<JsonValue<폼속성>>>();

다음처럼 사용할 수 있습니다.

public class 폼 : BaseEntity
{
    // 키
    [Key]
    public Uid Id { get; init; } = Uid.New();

    // 속성
    public Title? 제목 { get; set; }
    [Required]
    public Description? 설명 { get; set; }
    [Required]
    public Description? 목표 { get; set; }
    public JsonValue<폼속성> 속성 { get; init; } = JsonValue<폼속성>.New();
}

public class 폼속성
{
    public List<KeyTextValue> Meta { get; init; } = [];
    public Dictionary<string, object> Meta2 { get; init; } = [];
}

| 테스트

var form = new 폼
{
    제목 = new("제목1"),
    설명 = new("설명1"),
    목표 = new("목표1"),
    속성 = new JsonValue<폼속성>(new()
    {
        Meta2 = new Dictionary<string, object> { { "key1", "value1" }, { "key2", 2 } }
    })
};

image

image

More from this blog

개발, 테스트, 운영에서의 도커 활용

핵심 원칙: "한 번 빌드하고, 어디서든 실행한다 (Build once, run anywhere)" 도커의 가장 큰 장점은 환경 일관성입니다. 동일한 도커 이미지를 사용하여 개발, 테스트, 운영 환경을 구성함으로써 "제 PC에서는 됐는데..." 하는 문제를 최소화할 수 있습니다. 1. 개발 단계 (Development) 목표: 빠른 코드 변경 반영, 쉬운 디버깅, 실제 운영 환경과 유사한 환경 구성. Docker 사용 방안: Dockerf...

May 9, 20256 min read15

[EF Core] 데이터 삭제 시 소프트 삭제 적용

DB에서 데이터를 삭제하면 일반적으로 복구할 수 없습니다. 또한 관계에 따라 영구 삭제 자체가 어려울 수도 있습니다. 그래서 데이터를 영구 삭제하는 대신 IsDeleted 속성을 true로 주고 IsDeleted 속성을 필터링해서 조회하는 방법을 사용하기도 합니다. 이를 소프트 삭제라고 합니다. 그런데 EF에서 알아서 데이터 삭제 시 소프트 삭제를 하고 쿼리시 IsDeleted 속성을 체크해서 삭제한 데이터를 제외한 데이터만 쿼리하게 하는 ...

Mar 18, 20243 min read19

[EF Core] ValueConverter를 이용해서 엔터티 속성의 도메인 관리

EF Core를 사용하면서 문자열 길이 등의 특성을 일일이 지정하는 것은 번거롭습니다. ... [MaxLength(32)] public string? 제목 { get; set; } 엔터티가 한 개일 때는 상관이 없으나 제목 유형이 여러 엔터티에 사용될 경우 유형을 지정하기 번거롭습니다. 속성 유형을 도메인으로 관리하면 참 편할텐데요, ValueConverter를 이용할 수 있습니다. 그런데 이것을 인터페이스 정적 추상를 사용해서 다음처럼 ...

Mar 16, 20242 min read8

디모이 블로그

154 posts

.NET 관련 기술을 선호하고 새로운 언어를 배우는데 관심이 있습니다.