소유 엔터티 형식

EF Core를 사용하면 다른 엔터티 형식의 탐색 속성에만 표시할 수 있는 엔터티 형식을 모델링할 수 있습니다. 이를 소유 엔터티 형식이라고 합니다. 소유된 엔터티 형식을 포함하는 엔터티가 소유자입니다.

소유 엔터티는 본질적으로 소유자의 일부이며 소유자 없이는 존재할 수 없으며 개념적으로 집계와 유사합니다. 즉, 소유 엔터티는 소유자와의 관계의 종속 쪽에 정의되어 있습니다.

형식을 소유 형식으로 구성

대부분의 공급자에서 엔터티 형식은 규칙에 의해 소유된 형식으로 구성되지 않습니다. 형식을 소유 형식으로 구성하려면 OnModelCreating에서 OwnsOne 메서드를 명시적으로 사용하거나 OwnedAttribute가 있는 형식에 주석을 추가해야 합니다. Azure Cosmos DB 공급자는 예외입니다. Azure Cosmos DB는 문서 데이터베이스이므로 공급자는 기본적으로 모든 관련 엔터티 형식을 소유로 구성합니다.

이 예제에서 StreetAddress는 ID 속성이 없는 형식입니다. 특정 주문의 배송 주소를 지정하기 위한 Order 형식 속성으로 사용됩니다.

OwnedAttribute를 사용하여 다른 엔터티 형식에서 참조할 때 소유 엔터티로 처리할 수 있습니다.

[Owned]
public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}
public class Order
{
    public int Id { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

OnModelCreatingOwnsOne 메서드를 사용하여 ShippingAddress 속성이 Order 엔터티 형식의 소유 엔터티임을 지정하고 필요한 경우 추가 패싯을 구성할 수도 있습니다.

modelBuilder.Entity<Order>().OwnsOne(p => p.ShippingAddress);

ShippingAddress 속성이 Order 형식에서 비공개인 경우 OwnsOne 메서드의 문자열 버전을 사용할 수 있습니다.

modelBuilder.Entity<Order>().OwnsOne(typeof(StreetAddress), "ShippingAddress");

위의 모델은 다음 데이터베이스 스키마에 매핑됩니다.

Screenshot of the database model for entity containing owned reference

자세한 컨텍스트는 전체 샘플 프로젝트를 참조하세요.

소유 엔터티 형식을 필수로 표시할 수 있습니다. 자세한 내용은 필수 일대일 종속성을 참조하세요.

암시적 키

OwnsOne으로 구성되거나 참조 탐색을 통해 검색된 소유 형식은 항상 소유자와 일대일 관계를 가지므로 외래 키 값이 고유하기 때문에 고유한 키 값이 필요하지 않습니다. 이전 예제에서 StreetAddress 형식은 키 속성을 정의할 필요가 없습니다.

EF Core가 이러한 개체를 추적하는 방법을 이해하기 위해 기본 키가 소유된 형식에 대한 섀도 속성으로 생성된다는 것을 아는 것이 유용합니다. 소유 형식의 인스턴스 키 값은 소유자 인스턴스의 키 값과 동일합니다.

소유 형식의 컬렉션

소유 형식의 컬렉션을 구성하려면 OnModelCreating에서 OwnsMany를 사용합니다.

소유된 형식에는 기본 키가 필요합니다. .NET 형식에 적합한 후보 속성이 없으면 EF Core에서 만들 수 있습니다. 그러나 소유된 형식이 컬렉션을 통해 정의되는 경우 OwnsOne에 대해서와 마찬가지로 소유된 인스턴스의 외래 키와 소유된 인스턴스의 기본 키 역할을 하는 섀도 속성을 만드는 것만으로는 충분하지 않습니다. 각 소유자에 대해 여러 개의 소유 형식 인스턴스가 있을 수 있으므로 소유자의 키가 각 소유 인스턴스에 고유한 ID를 제공하기에 충분하지 않습니다.

이에 대한 가장 간단한 두 가지 솔루션은 다음과 같습니다.

  • 소유자를 가리키는 외래 키와 관계없이 새 속성에서 서로게이트 기본 키를 정의합니다. 포함된 값은 모든 소유자에서 고유해야 합니다(예: 부모 {1}에게 자식 {1}이 있는 경우 부모 {2}는 자식 {1}을 가질 수 없음). 따라서 값에는 내재된 의미가 없습니다. 외래 키는 기본 키의 일부가 아니므로 값을 변경할 수 있으므로 한 부모에서 다른 부모 키로 자식 키를 이동할 수 있지만 일반적으로 집계 의미 체계에 어긋나게 됩니다.
  • 외래 키 및 추가 속성을 복합 키로 사용합니다. 이제 추가 속성 값은 지정된 부모에 대해서만 고유해야 합니다. 따라서 부모 {1}에게 자식 {1,1}이 있는 경우 부모 {2}는 여전히 자식 {2,1}을 가질 수 있습니다. 기본 키의 외래 키를 소유자와 소유 엔터티 간의 관계를 변경할 수 없게 하여 집계 의미 체계를 더 잘 반영합니다. EF Core가 기본적으로 수행하는 동작입니다.

이 예제에서는 Distributor 클래스를 사용합니다.

public class Distributor
{
    public int Id { get; set; }
    public ICollection<StreetAddress> ShippingCenters { get; set; }
}

기본적으로 ShippingCenters 탐색 속성을 통해 참조되는 소유 형식에 사용되는 기본 키는 ("DistributorId", "Id")이며 여기에서 "DistributorId"는 FK이고 "Id"는 고유한 int 값입니다.

다른 기본 키를 구성하려면 HasKey를 호출합니다.

modelBuilder.Entity<Distributor>().OwnsMany(
    p => p.ShippingCenters, a =>
    {
        a.WithOwner().HasForeignKey("OwnerId");
        a.Property<int>("Id");
        a.HasKey("Id");
    });

위의 모델은 다음 데이터베이스 스키마에 매핑됩니다.

Sceenshot of the database model for entity containing owned collection

테이블 분할을 사용하여 소유 형식 매핑

관계형 데이터베이스를 사용하는 경우 기본적으로 참조 소유 형식은 소유자와 동일한 테이블에 매핑됩니다. 이렇게 하려면 테이블을 두 으로 분할해야 합니다. 일부 열은 소유자의 데이터를 저장하는 데 사용되고 일부 열은 소유 엔터티의 데이터를 저장하는 데 사용됩니다. 테이블 분할이라고 하는 일반적인 기능입니다.

기본적으로 EF Core는 패턴 Navigation_OwnedEntityProperty에 따라 소유 엔터티 형식의 속성에 대한 데이터베이스 열의 이름을 지정합니다. 따라서 StreetAddress 속성은 이름이 'ShippingAddress_Street' 및 'ShippingAddress_City'인 'Orders' 테이블에 표시됩니다.

HasColumnName 메서드를 사용하여 해당 열의 이름을 바꿀 수 있습니다.

modelBuilder.Entity<Order>().OwnsOne(
    o => o.ShippingAddress,
    sa =>
    {
        sa.Property(p => p.Street).HasColumnName("ShipsToStreet");
        sa.Property(p => p.City).HasColumnName("ShipsToCity");
    });

참고 항목

Ignore와 같은 대부분의 일반 엔터티 형식 구성 메서드는 동일한 방식으로 호출할 수 있습니다.

여러 소유 형식 간에 동일한 .NET 형식 공유

소유 엔터티 형식은 다른 소유 엔터티 형식과 동일한 .NET 형식일 수 있으므로 .NET 형식으로는 소유된 형식을 식별하기에 충분하지 않을 수 있습니다.

이러한 경우 소유자에서 소유된 엔터티로 가리키는 속성은 소유 엔터티 형식의 정의 탐색이 됩니다. EF Core의 관점에서 정의 탐색은 .NET 형식과 함께 형식의 ID에 속합니다.

예를 들어 다음 클래스 ShippingAddressBillingAddress는 모두 동일한 .NET 형식인 StreetAddress입니다.

public class OrderDetails
{
    public DetailedOrder Order { get; set; }
    public StreetAddress BillingAddress { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

EF Core가 이러한 개체의 추적된 인스턴스를 구분하는 방법을 이해하기 위해 정의 탐색이 소유자의 키 값과 소유 형식의 .NET 형식과 함께 인스턴스 키의 일부가 되었다고 생각하는 것이 유용할 수 있습니다.

중첩된 소유 형식

다음과 같은 가상의 예제에서 OrderDetailsBillingAddressShippingAddress를 소유하며, 둘 다 StreetAddress 형식입니다. 그리고 OrderDetailsDetailedOrder 형식입니다.

public class DetailedOrder
{
    public int Id { get; set; }
    public OrderDetails OrderDetails { get; set; }
    public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
    Pending,
    Shipped
}

소유 형식에 대한 각 탐색은 완전히 독립적인 구성을 사용하여 별도의 엔터티 형식을 정의합니다.

중첩된 소유 형식 외에도 소유된 형식은 소유된 엔터티가 종속된 쪽에 있는 한 소유자 또는 다른 엔터티가 될 수 있는 일반 엔터티를 참조할 수 있습니다. 이 기능은 소유 엔터티 형식을 EF6의 복합 형식과 별도로 설정합니다.

public class OrderDetails
{
    public DetailedOrder Order { get; set; }
    public StreetAddress BillingAddress { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

소유 형식 구성

흐름 호출에서 OwnsOne 메서드를 연결하여 이 모델을 구성할 수 있습니다.

modelBuilder.Entity<DetailedOrder>().OwnsOne(
    p => p.OrderDetails, od =>
    {
        od.WithOwner(d => d.Order);
        od.Navigation(d => d.Order).UsePropertyAccessMode(PropertyAccessMode.Property);
        od.OwnsOne(c => c.BillingAddress);
        od.OwnsOne(c => c.ShippingAddress);
    });

소유자를 가리키는 탐색 속성을 정의하는 데 사용되는 WithOwner 호출을 확인합니다. 소유 관계의 일부가 아닌 소유자 엔터티 형식에 대한 탐색을 정의하려면 WithOwner()를 인수 없이 호출해야 합니다.

OrderDetailsStreetAddress 모두에 OwnedAttribute를 사용하여 이 결과를 얻을 수도 있습니다.

또한 Navigation 호출을 확인합니다. 소유된 형식에 대한 탐색 속성은 소유되지 않은 탐색 속성과 마찬가지로 추가로 구성할 수 있습니다.

위의 모델은 다음 데이터베이스 스키마에 매핑됩니다.

Screenshot of the database model for entity containing nested owned references

소유 형식을 별도의 테이블에 저장

또한 EF6 복합 형식과 달리 소유 형식은 소유자와 별도의 테이블에 저장할 수 있습니다. 소유 형식을 소유자와 동일한 테이블에 매핑하는 규칙을 재정의하기 위해 ToTable을 호출하고 다른 테이블 이름을 제공할 수 있습니다. 다음 예제에서는 OrderDetails 및 해당 두 주소를 DetailedOrder와 별도의 테이블에 매핑합니다.

modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od => { od.ToTable("OrderDetails"); });

이 작업을 수행하는 데 TableAttribute를 사용할 수도 있지만 소유 형식에 대한 탐색이 여러 개인 경우 여러 엔터티 형식이 동일한 테이블에 매핑되므로 이 작업은 실패합니다.

소유 형식 쿼리

소유자를 쿼리할 때 소유된 형식은 기본적으로 포함됩니다. 소유 형식이 별도의 테이블에 저장되어 있더라도 Include 메서드를 사용할 필요가 없습니다. 앞에서 설명한 모델에 따라 다음 쿼리는 데이터베이스에서 Order, OrderDetails 및 소유한 두 StreetAddresses를 가져옵니다.

var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.OrderDetails.ShippingAddress.City}");

제한 사항

이러한 제한 사항 중 일부는 소유 엔터티 형식의 작동 방식에 대한 기본 사항이지만, 일부는 향후 릴리스에서 제거할 수 있는 제한 사항입니다.

디자인별 제한 사항

  • 소유된 형식에 대한 DbSet<T>을 만들 수 없습니다.
  • ModelBuilder에서 소유된 형식으로 Entity<T>()를 호출할 수 없습니다.
  • 소유 엔터티 형식의 인스턴스는 여러 소유자가 공유할 수 없습니다(소유된 엔터티 형식을 사용하여 구현할 수 없는 값 개체에 대해 잘 알려진 시나리오임).

현재 단점

  • 소유된 엔터티 형식에는 상속 계층 구조가 있을 수 없습니다.

이전 버전의 단점

  • EF Core 2.x에서 소유 엔터티 형식에 대한 참조 탐색은 소유자와 별도의 테이블에 명시적으로 매핑되지 않는 한 null일 수 없습니다.
  • EF Core 3.x에서 소유자와 동일한 테이블에 매핑된 소유 엔터티 형식의 열은 항상 null 허용으로 표시됩니다.