EF Core 특수 맡핑

EF Core 7.0 κ³Ό EF Core 8.0 μ—μ„œλŠ” κΈ°μ‘΄ λ²„μ „μ˜ EF λ˜λŠ” RDB의 ν•œκ³„λ‘œ 인해 닀루기 νž˜λ“€μ—ˆλ˜ ν˜•μ‹λ“€μ— λŒ€ν•œ 지원이 λŒ€ν­ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

이 지원이 ν•„μš”ν•œ μ΄μœ μ™€ μ‚¬μš© 방법에 λŒ€ν•΄ μ•Œμ•„ λ΄…λ‹ˆλ‹€.

Linq Filtering & Projection

μš°μ„ , EF μ—μ„œ μ§€μ›ν•œλ‹€λŠ” μ˜λ―ΈλŠ” IQueryable Linq μ—μ„œ 필터링(Where) κ³Ό νˆ¬μ‚¬(Projection, Select)κ°€ κ°€λŠ₯ν•˜λ‹€λŠ” μ˜λ―ΈλΌλŠ” 점을 μ•Œμ•„ λ‘μ‹œλŠ” 게 μ’‹μŠ΅λ‹ˆλ‹€.

μ €μž₯ 객체

데이터 λ² μ΄μŠ€μ— μ €μž₯될 κ°μ²΄λŠ” λŒ€μ²΄λ‘œ μ•„λž˜ μ„Έ 가지 큰 λΆ€λ₯˜λ‘œ λ‚˜λˆŒ 수 μžˆμŠ΅λ‹ˆλ‹€.

  1. κΈ°λ³Έ μžλ£Œν˜•(Primitive Types)
    ν•„λ“œμ˜ μ§‘ν•©μœΌλ‘œ ν‘œν˜„λ˜μ§€ μ•Šκ³ , 단일 값을 λ‚˜νƒ€λƒ„. (int, char, text …).

  2. μ—”ν‹°ν‹°(Entity Types)
    ν•„λ“œμ˜ μ§‘ν•©μœΌλ‘œ ν‘œν˜„λ˜κ³  Key κ°€ 있음.

  3. 볡합 μžλ£Œν˜•(Complex Value Types)
    ν•„λ“œμ˜ μ§‘ν•©μœΌλ‘œ ν‘œν˜„λ˜κ³  Key κ°€ μ—†μŒ. ???

1λ²ˆμ€ μ½”λ“œ λͺ¨λΈμ˜ 속성 ↔ λ ˆμ½”λ“œλ‘œ, 2λ²ˆμ€ μ½”λ“œ λͺ¨λΈ(클래슀) ↔ ν…Œμ΄λΈ”λ‘œ 맡핑이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

κ·ΈλŸ¬λ‚˜, 3번의 경우 μ½”λ“œ λͺ¨λΈ μž…μž₯μ—μ„œλŠ” λ¬Έμ œκ°€ μ—†μ§€λ§Œ, Keyλ₯Ό μ „μ œν•˜λŠ” RDB의 μž…μž₯μ—μ„œλŠ” μ²˜λ¦¬ν•  방법이 μ—†μŠ΅λ‹ˆλ‹€. 즉, μ½”λ“œ λͺ¨λΈ β†’ 데이터 베이슀 맡핑에 κ΄€ν•œ μ•ˆμ „ν•œ 방법이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

볡합 μžλ£Œν˜•(Complex Types)

EF 7 μ΄μ „μ—λŠ” 볡합 ν˜•μ‹μ„ 우회 λ§΅ν•‘ν•˜λŠ” 방법은 크게 μ„Έ κ°€μ§€μ˜€μŠ΅λ‹ˆλ‹€.

  1. Owned Type 으둜 μ„ μ–Έ
    1. Owner의 ν…Œμ΄λΈ”μ„ 곡유
      볡합 μžλ£Œν˜•μ˜ 속성이 Owner ν…Œμ΄λΈ”μ˜ ν•„λ“œλ‘œ μ‚½μž…λ¨
    2. λ¬΅μ‹œμ  ν‚€λ₯Ό λ³΄μœ ν•œ ν…Œμ΄λΈ”λ‘œ μ„ μ–Έ
      Owner 의 ν‚€λ₯Ό μ™Έλž˜ν‚€λ‘œ λ³΄μœ ν•œ ν…Œμ΄λΈ”μ„ μ •μ˜. 이 ν…Œμ΄λΈ”μ˜ KeyλŠ” λ³„λ„λ‘œ μ •μ˜.
  2. 직렬화λ₯Ό 톡해 κΈ°λ³Έ μžλ£Œν˜•μœΌλ‘œ λ³€ν™˜

Owned Type 으둜 μ„ μ–Έν•˜λŠ” 것은 이미 Keyλ₯Ό μ „μ œν•˜λŠ” Entity 이기에 key κ°€ μ—†λŠ” 볡합 μžλ£Œν˜•κ³Ό 의미둠적으둜 λΆ€ν•©ν•˜μ§€ μ•Šκ³ , Keyλ₯Ό μΈμœ„μ μœΌλ‘œ μ •μ˜ν•΄μ•Ό ν•΄μ„œ, μ—λŸ¬ 유발 μš”μ†Œκ°€ μžˆμŠ΅λ‹ˆλ‹€.

μ§λ ¬ν™”μ—λŠ” μ•„λž˜ 두 가지 방법이 μžˆμŠ΅λ‹ˆλ‹€.

  1. CSV (Comma Seperated Values)
  2. Json

μ§λ ¬ν™”μ˜ κ°€μž₯ 큰 λ¬Έμ œμ μ€ Linq κ°€ 먹지 μ•ŠλŠ”λ‹€λŠ” μ μž…λ‹ˆλ‹€.
(PostgreSQL 은 "Json ν˜•μ‹"을 μ§€μ›ν•˜κΈ° λ•Œλ¬Έμ— μ˜ˆμ™Έμž…λ‹ˆλ‹€)

μ΄λŸ¬ν•œ 볡합 ν˜•μ‹μ˜ 맡핑 어렀움은, EF 7에 λ“€μ–΄μ„œ, "Json Column"μ΄λΌλŠ” κΈ°λŠ₯을 λ„μž…ν•˜μ—¬ ν•΄κ²°ν–ˆκ³ , EF 8 은 "Complex Type"μ΄λΌλŠ” μ΄λ¦„μœΌλ‘œ λ‹€λ₯Έ 접근법을 μ œμ‹œν•©λ‹ˆλ‹€.

Json ν˜•μ‹ ν•„λ“œ(Json Column)

데이터 λ² μ΄μŠ€κ°€ λ¬Έμžμ—΄ ν˜• ν•„λ“œμ— μ €μž₯된 Json textλ₯Ό Json 객체둜 μ·¨κΈ‰ν•˜λŠ” κΈ°λŠ₯을 κ°€λ¦¬ν‚€λŠ”λ°, λŒ€λΆ€λΆ„μ˜ RDBλŠ” 이에 λŒ€ν•œ 지원을 ν•˜κ³  μžˆμ–΄, 쿼리의 λŒ€μƒ(From)이 될 수 μžˆμŠ΅λ‹ˆλ‹€.

이 κΈ°λŠ₯은 NoSQL 같은 document 데이터 베이슀의 νŠΉμ§•μ„ RDB에 μž…νžŒ 것이라 ν•  수 μžˆλŠ”λ°, μ‹œμž₯ 방어적 μ°¨μ›μ—μ„œ κΌ­ ν•„μš”ν•œ λΆ€λΆ„μ΄μ—ˆλ˜ 것 κ°™μŠ΅λ‹ˆλ‹€.

EF λŠ” 버전 7λΆ€ν„°, "Json Column"에 λŒ€ν•œ 지원을 μΆ”κ°€ν–ˆλŠ”λ°, Owned둜 μ§€μ •λ˜λŠ” 볡합 ν˜•μ‹μ„ Json Column으둜 μ§€μ •ν•˜λŠ” μ˜΅μ…˜μ„ μ œκ³΅ν•˜λŠ” 것이 μ£Όμš” κ³¨μžμž…λ‹ˆλ‹€.

Fluent Api

μš°μ„ , EF 7μ—μ„œλŠ” Fluent API λ°©μ‹μœΌλ‘œ μ„€μ •ν•  수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

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

μœ„ μ½”λ“œμ—μ„œ AuthorλŠ” Entityμž…λ‹ˆλ‹€.

AuthorλŠ” Contact λ₯Ό μ†Œμœ ν•˜κ³ , Contact λŠ” λ‹€μ‹œ Addressλ₯Ό μ†Œμœ ν•©λ‹ˆλ‹€.

Author ν…Œμ΄λΈ”μ— Contact λ₯Ό Json Column 으둜 μ§€μ •ν•˜κΈ° μœ„ν•΄μ„œ, Contact λΉŒλ”μ˜ ToJson() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€. κ·Έ 결과둜 Contact κ°μ²΄λŠ” [Author].[Contact] ν•„λ“œμ— Json λ¬Έμžμ—΄λ‘œ μ €μž₯λ©λ‹ˆλ‹€.

그런데, Address λΉŒλ”μ—μ„œλŠ” 이λ₯Ό ν˜ΈμΆœν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. μ™œλƒν•˜λ©΄ Address 의 Owner인 Contact κ°€ 이미 Json 으둜 μ§€μ •λ˜μ—ˆκΈ° λ•Œλ¬Έμ—, Address 도 λ‹Ήμ—°νžˆ Json으둜 μ§€μ •λ˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

Owns & Aggregate

EF의 Owns κ΄€κ³„λŠ” μ›λž˜ 애그리것(Aggregate)을 λ‚˜νƒ€λ‚΄κΈ° μœ„ν•œ κ²ƒμ΄μ—ˆμŠ΅λ‹ˆλ‹€.

즉, Owner λŠ” Aggregate Root, Owned λŠ” Aggregate 의 Sub Entity λ₯Ό κ°€λ¦¬ν‚€λŠ”λ°, 이 κ°œλ…μ— 맞게 EF도 μΌμ •ν•œ κ°•μ œ 사항을 λΆ€μ—¬ν•©λ‹ˆλ‹€.

  1. Sub Entity λŠ” DbSet<T>의 λŒ€μƒμœΌλ‘œ μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€.
    Owned λ₯Ό λ‹€λ₯Έ Aggregateμ—μ„œ μ°Έμ‘°ν•˜λ €λ©΄, λ°˜λ“œμ‹œ DbSet<TOwner>을 ν†΅ν•˜λŠ” 것이 κ°•μ œλ©λ‹ˆλ‹€.
  2. OwnedλŠ” μ–Έμ œλ‚˜ Onwer에 Include 되고, Cacade Delete λ©λ‹ˆλ‹€.

Json Column 의 지정은 Aggregate Root μ—”ν‹°ν‹°μ—μ„œλ§Œ κ°€λŠ₯ν•©λ‹ˆλ‹€.

쑰회(Query)

μ΄λ ‡κ²Œ Json ν•„λ“œλ‘œ μ§€μ •λœ 볡합 ν˜•μ‹μ— λŒ€ν•΄ μ•„λž˜μ™€ 같은 Linq κ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.

필터링

var authorsInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .ToListAsync();

λ‹¨μˆœ νˆ¬μ‚¬

var postcodesInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .Select(author => author.Contact.Address.Postcode)
    .ToListAsync();

μ‘°ν•© νˆ¬μ‚¬

var orderedAddresses = await context.Authors
    .Where(
        author => (author.Contact.Address.City == "Chigley"
                   && author.Contact.Phone != null)
                  || author.Name.StartsWith("D"))
    .OrderBy(author => author.Contact.Phone)
    .Select(
        author => author.Name + " (" + author.Contact.Address.Street
                  + ", " + author.Contact.Address.City
                  + " " + author.Contact.Address.Postcode + ")")
    .ToListAsync();

μ§‘ν•©μ˜ νˆ¬μ‚¬

var postsWithViews = await context.Posts
    .Where(post => post.Metadata!.Views > 3000)
    .AsNoTracking()
    .Select(
        post => new
        {
            post.Author!.Name, post.Metadata!.Views, Searches = post.Metadata.TopSearches, Commits = post.Metadata.Updates
        })
    .ToListAsync();

Update

DbContext.SaveChanges 도 일반 Entity 처럼 λ‹€λ£° 수 μžˆλ„λ‘ μ§€μ›ν•©λ‹ˆλ‹€.

볡합 μžλ£Œν˜•μ— μƒˆλ‘œμš΄ ν• λ‹Ή.

var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy"));

jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" };

await context.SaveChangesAsync();

볡합 μžλ£Œν˜•μ˜ μ†μ„±λ§Œ ν• λ‹Ή.

var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));

brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");

await context.SaveChangesAsync();

Array 인덱싱

EF 8 μ—μ„œλŠ” Json Column 으둜 μ§€μ •λœ 볡합 객체의 집합 속성에 λŒ€ν•΄ Array Indexing 이 κ°€λŠ₯ν•΄μ‘ŒμŠ΅λ‹ˆλ‹€.

필터링

var cutoff = DateOnly.FromDateTime(DateTime.UtcNow - TimeSpan.FromDays(365));
var updatedPosts = await context.Posts
    .Where(
        p => p.Metadata!.Updates[0].UpdatedOn < cutoff
             && p.Metadata!.Updates[1].UpdatedOn < cutoff)
    .ToListAsync();

νˆ¬μ‚¬

var postsAndRecentUpdates = await context.Posts
    .Where(p => p.Metadata!.Updates[0].UpdatedOn != null
                && p.Metadata!.Updates[1].UpdatedOn != null)
    .Select(p => new
    {
        p.Title,
        LatestUpdate = p.Metadata!.Updates[0].UpdatedOn,
        SecondLatestUpdate = p.Metadata.Updates[1].UpdatedOn
    })
    .ToListAsync();

μΊμ‹±λœ 집합을 ν†΅ν•œ 필터링

var searchTerms = new[] { "Search #2", "Search #3", "Search #5", "Search #8", "Search #13", "Search #21", "Search #34" };

var postsWithSearchTerms = await context.Posts
    .Where(post => post.Metadata!.TopSearches.Any(s => searchTerms.Contains(s.Term)))
    .ToListAsync();

볡합 μžλ£Œν˜•(Complex Type)

EF 8에 λ„μž…λœ 이 κΈ°λŠ₯은 말 κ·ΈλŒ€λ‘œ 볡합 μžλ£Œν˜•μ„ λ‹€λ£¨λŠ” κΈ°λŠ₯의 μ΄λ¦„μž…λ‹ˆλ‹€.

볡합 μžλ£Œν˜•μ— κ΄€ν•œ 지원은 EF 7μ—μ„œλŠ” Json Column 으둜, EF 8 μ—μ„œλŠ” Complex Type(κ³Ό Json Column) 으둜 μ§€μ›λ˜λŠ” 것이죠.

지정 방법

Attribute

ComplexTypeAttribute λ₯Ό λͺ¨λΈ ν΄λž˜μŠ€μ— λΆ€μ—¬ν•©λ‹ˆλ‹€.

[ComplexType]
public class Address
{
    public required string Line1 { get; set; }
    public string? Line2 { get; set; }
    public required string City { get; set; }
    public required string Country { get; set; }
    public required string PostCode { get; set; }
}

Fluent Api

EntityEntry.ComplexProperty() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>()
        .ComplexProperty(e => e.Address);

    modelBuilder.Entity<Order>(b =>
    {
        b.ComplexProperty(e => e.BillingAddress);
        b.ComplexProperty(e => e.ShippingAddress);
    });
}

μ΄λ ‡κ²Œ μ§€μ •λ˜λ©΄, 이 λͺ¨λΈ 클래슀의 속성듀은 μ†Œμœ  객체의 ν•„λ“œλ‘œ λ§΅ν•‘λ©λ‹ˆλ‹€.

INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);

이 κΈ°λŠ₯은,

  • Owner ν…Œμ΄λΈ”μ˜ ν•„λ“œλ‘œ κ°•μ œ μ„€μ •λœλ‹€λŠ” μ μ—μ„œ Json Column κ³Ό λ‹€λ¦…λ‹ˆλ‹€.
    데이터 베이슀의 Json νŒŒμ‹± κΈ°λŠ₯을 ν˜ΈμΆœν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— 더 효율적이라고 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

  • μΈμŠ€ν„΄μŠ€κ°€ νŠΉμ • Owner에 μ†ν•˜λŠ” 것이 κ°•μ œλ˜μ§€ μ•ŠλŠ”λ‹€λŠ” μ μ—μ„œ Owned 와 λ‹€λ¦…λ‹ˆλ‹€.

특히, 두 번째 νŠΉμ§•μ€ Complex Type의 쑴재의 이유라고 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Owned Type 과 차이점

Owned Type 은 Onwer 의 PKλ₯Ό FK 둜 λ³΄μœ ν•œ Entity 이고, 이λ₯Ό λ°”κΏ€ 수 μ—†μŠ΅λ‹ˆλ‹€.

μ΄λŸ¬ν•œ νŠΉμ§•μ€ μ•„λž˜μ˜ κ°„λ‹¨ν•œ μ½”λ“œμ— 문제λ₯Ό μΌμœΌν‚΅λ‹ˆλ‹€.

customer.Orders.Add(
    new Order { 
       Contents = "Tesco Tasty Treats", 
       BillingAddress = customer.Address, 
       ShippingAddress = customer.Address, 
    });

await context.SaveChangesAsync();

warn: 8/20/2023 12:48:01.678 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types β€˜Order.BillingAddress#Address’ and β€˜Customer.Address#Address’ with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
// …
fail: 8/20/2023 12:48:01.709 CoreEventId.SaveChangesFailed[10000] (Microsoft.EntityFrameworkCore.Update)
An exception occurred in the database while saving changes for context type β€˜NewInEfCore8.ComplexTypesSample+CustomerContext’.
System.InvalidOperationException: Cannot save instance of β€˜Order.ShippingAddress#Address’ because it is an owned entity without any reference to its owner. Owned entities can only be saved as part of an aggregate also including the owner entity.
// …

Customer.Address λŠ” Customer 에 μ†ν•œ 객체인데, 이λ₯Ό Orger.Address 에 ν• λ‹Ήν•˜λŠ” 것은 (μ½”λ“œλ‘œλŠ” λ¬Έμ œκ°€ μ „ν˜€ μ—†μ§€λ§Œ) μ†Œμœ  관계가 ν‹€μ–΄μ§„λ‹€λŠ” 점을 λ§ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

Complex Type 은 μ •ν™•νžˆ 이 문제λ₯Ό ν•΄κ²°ν•©λ‹ˆλ‹€.
λ‹¨μˆœνžˆ Customer의 ν•„λ“œ 값듀이, Order 의 ν•„λ“œκ°’μœΌλ‘œ λ³΅μ‚¬λ˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

λΆˆλ³€μ„±

κ·ΈλŸ¬λ‚˜, μ£Όμ˜ν•΄μ•Ό ν•  점이 μžˆμŠ΅λ‹ˆλ‹€.
Complex Type 인 경우, μ•„λž˜μ˜ μ½”λ“œλ₯Ό μ‹€ν–‰ν•˜λ©΄,

customer.Orders.Add(
    new Order { 
       Contents = "Tesco Tasty Treats", 
       BillingAddress = customer.Address, 
       ShippingAddress = customer.Address, 
    });

customer.Address.Line1 = "Peacock Lodge";

await context.SaveChangesAsync();

Order.BillingAddress, Order.ShippingAddress, Customer.Addresss 의 Line1 속성이 μ „λΆ€ β€œPeacock Lodge” 둜 λ³€κ²½λ©λ‹ˆλ‹€.

μ΄λŠ” Address κ°€ μ°Έμ‘°ν˜• 객체이기 λ•Œλ¬Έμ— λ‹Ήμ—°ν•œ κ²°κ³Όμž…λ‹ˆλ‹€.

λ§Œμ•½, 이 것이 μ›ν•˜λŠ” 결과라면, Address λŠ” Complext Type으둜 μ„ μ–Έλ˜λ©΄ μ•ˆλ˜κ³ , λ³„λ„μ˜ Entity둜 μ„ μ–Έλ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.

λ§Œμ•½, μ›ν•˜μ§€ μ•ŠλŠ” 것이라면, Complex Type 을 λΆˆλ³€ νƒ€μž…μœΌλ‘œ μ„ μ–Έν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

  • record class
  • readonly record struct

μƒνƒœ νŠΈλž™ν‚Ή

Complex Type 은 EF에 μ˜ν•΄ 값이 μΆ”μ λ©λ‹ˆλ‹€.

var billingAddress = context.Entry(order)
    .ComplexProperty(e => e.BillingAddress)
    .CurrentValue;

var postCode = context.Entry(order)
    .ComplexProperty(e => e.BillingAddress)
    .Property(e => e.PostCode)
    .CurrentValue;

μ°Έμ‘°

5개의 μ’‹μ•„μš”