Принадлежащие типы сущностей

EF Core позволяет моделировать типы сущностей, которые могут отображаться только в свойствах навигации других типов сущностей. Они называются собственными типами сущностей. Сущность, содержащая тип принадлежащей сущности, является ее владельцем.

Принадлежащие сущности по сути являются частью владельца и не могут существовать без него, они концептуально похожи на агрегаты. Это означает, что собственная сущность по определению находится на зависимой стороне связи с владельцем.

Настройка типов как принадлежащих

В большинстве поставщиков типы сущностей никогда не настраиваются как принадлежащие соглашению. Необходимо явно использовать OwnsOne метод в OnModelCreating или добавить к типу заметки с OwnedAttribute , чтобы настроить тип как принадлежащий. Поставщик Azure Cosmos DB является исключением из этого. Так как Azure Cosmos DB является базой данных документов, поставщик настраивает все связанные типы сущностей как принадлежащие по умолчанию.

В этом примере является типом без StreetAddress свойства identity. Он используется как свойство сущности 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; }
}

Можно также использовать OwnsOne метод в OnModelCreating , чтобы указать, что ShippingAddress свойство является принадлежащим сущностью Order типа сущности, и при необходимости настроить дополнительные аспекты.

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

ShippingAddress Если свойство является частным в типе Order , можно использовать строковую версию OwnsOne метода:

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

Приведенная выше модель сопоставлена со следующей схемой базы данных:

Снимок экрана: модель базы данных для сущности, содержащей ссылку на принадлежащие

Дополнительные сведения см. в полном примере проекта .

Совет

Собственный тип сущности можно пометить как обязательный. Дополнительные сведения см. в разделе Обязательные зависимые сущности "один к одному ".

Неявные ключи

Принадлежащие типы, настроенные с OwnsOne помощью или обнаруженные с помощью ссылочной навигации, всегда имеют связь "один к одному" с владельцем, поэтому им не нужны собственные значения ключей, так как значения внешнего ключа являются уникальными. В предыдущем примере StreetAddress типу не нужно определять свойство ключа.

Чтобы понять, как EF Core отслеживает эти объекты, полезно знать, что первичный ключ создается как теневое свойство для принадлежащего типа. Значение ключа экземпляра принадлежащего типа будет таким же, как и значение ключа экземпляра владельца.

Коллекции принадлежащих типов

Чтобы настроить коллекцию принадлежащих типов, используйте OwnsMany в OnModelCreating.

Принадлежащим типам требуется первичный ключ. Если в типе .NET нет хороших свойств кандидатов, EF Core может попытаться создать их. Однако, когда собственные типы определяются с помощью коллекции, недостаточно просто создать теневое свойство, которое будет выступать в качестве внешнего ключа владельца и первичного ключа принадлежащего экземпляра, как это делается для OwnsOne: для каждого владельца может быть несколько экземпляров собственных типов, и, следовательно, ключа владельца недостаточно для предоставления уникального удостоверения для каждого экземпляра.

Ниже приведены два наиболее простых решения.

  • Определение суррогатного первичного ключа для нового свойства независимо от внешнего ключа, указывающего на владельца. Содержащиеся значения должны быть уникальными для всех владельцев (например, если у Parent {1} есть дочерний {1}элемент , то у Parent {2} не может быть дочерний {1}), поэтому значение не имеет никакого врожденного значения. Так как внешний ключ не является частью первичного ключа, его значения можно изменить, поэтому можно переместить дочерний ключ из одного родительского ключа в другой, однако это обычно противоречит агрегатной семантике.
  • Использование внешнего ключа и дополнительного свойства в качестве составного ключа. Дополнительное значение свойства теперь должно быть уникальным только для заданного родительского элемента (поэтому, если у Parent {1} есть дочерний {1,1} элемент, у Parent {2} по-прежнему может быть дочерний {2,1}элемент ). Благодаря тому, что внешний ключ является частью первичного ключа, связь между владельцем и принадлежащей сущностью становится неизменяемой и лучше отражает агрегатную семантику. Это то, что EF Core делает по умолчанию.

В этом примере мы будем использовать Distributor класс .

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

По умолчанию первичный ключ, используемый для принадлежащего типа, на который ссылается ShippingCenters свойство навигации, будет где "DistributorId"("DistributorId", "Id") — это FK, а "Id" — уникальное int значение.

Чтобы настроить другой первичный ключ, вызовите HasKey.

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

Приведенная выше модель сопоставлена со следующей схемой базы данных:

Снимок экрана модели базы данных для сущности, содержащей принадлежащей коллекции

Сопоставление принадлежащих типов с разделением таблиц

При использовании реляционных баз данных ссылочные типы по умолчанию сопоставляются с той же таблицей, что и владелец. Для этого необходимо разделить таблицу на две части: некоторые столбцы будут использоваться для хранения данных владельца, а некоторые столбцы — для хранения данных принадлежащего объекта. Это распространенная функция, известная как разделение таблиц.

По умолчанию EF Core будет называть столбцы базы данных для свойств типа принадлежащих сущностей в соответствии с шаблоном Navigation_OwnedEntityProperty. StreetAddress Поэтому свойства будут отображаться в таблице "Заказы" с именами "ShippingAddress_Street" и "ShippingAddress_City".

Для переименования этих столбцов можно использовать 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.

Например, в следующем классе ShippingAddress и BillingAddress относятся к одному и тому же типу .NET, StreetAddress.

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

Чтобы понять, как EF Core будет различать отслеживаемые экземпляры этих объектов, может быть полезно думать, что определяющая навигация стала частью ключа экземпляра наряду со значением ключа владельца и типом .NET собственного типа.

Вложенные типы

В этом примере OrderDetails принадлежат BillingAddress и ShippingAddress, которые являются обоими StreetAddress типами. А OrderDetails, в свою очередь, принадлежит типу DetailedOrder.

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() владения, необходимо вызвать без аргументов.

Этот результат также можно достичь с помощью OwnedAttribute как для , так OrderDetails и StreetAddress.

Кроме того, обратите внимание на Navigation вызов. Свойства навигации для принадлежащих типов можно дополнительно настроить как для свойств навигации, не принадлежащих.

Приведенная выше модель сопоставлена со следующей схемой базы данных:

Снимок экрана: модель базы данных для сущности, содержащей вложенные ссылки

Хранение принадлежащих типов в отдельных таблицах

Кроме того, в отличие от сложных типов 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> для принадлежащего типа.
  • Вы не можете вызвать Entity<T>() с принадлежащим типом в ModelBuilder.
  • Экземпляры принадлежащих типов сущностей не могут совместно использоваться несколькими владельцами (это хорошо известный сценарий для объектов значений, которые не могут быть реализованы с использованием собственных типов сущностей).

Текущие недостатки

  • Типы принадлежащих сущностей не могут иметь иерархии наследования

Недостатки в предыдущих версиях

  • В EF Core 2.x ссылочные навигации для принадлежащих типов сущностей не могут иметь значение NULL, если они явно не сопоставлены с отдельной таблицей от владельца.
  • В EF Core 3.x столбцы для принадлежащих типов сущностей, сопоставленные с той же таблицей, что и владелец, всегда помечаются как допускающий значение NULL.