Tipos de entidade de propriedade
O EF Core permite que você modele tipos de entidade que só podem aparecer nas propriedades de navegação de outros tipos de entidade. Eles são chamados de tipos de entidade de propriedade. A entidade que contém um tipo de entidade de propriedade é seu proprietário.
As entidades de propriedade são essencialmente parte do proprietário e não podem existir sem ele, elas são conceitualmente semelhantes às agregações. Isso significa que a entidade de propriedade é por definição do lado dependente da relação com o proprietário.
Configurando tipos de propriedade
Na maioria dos provedores, os tipos de entidade nunca são configurados como propriedade da convenção – você deve usar explicitamente o método OwnsOne
em OnModelCreating
ou anotar o tipo para OwnedAttribute
configurar o tipo como de propriedade. O provedor do Azure Cosmos DB é uma exceção a isso. Como o Azure Cosmos DB é um banco de dados de documento, o provedor configura todos os tipos de entidades relacionadas de propriedade por padrão.
Neste exemplo, StreetAddress
é um tipo sem propriedade de identidade. Ele é usado como uma propriedade do tipo Ordem para especificar o endereço para entrega para uma ordem específica.
Podemos usá-la OwnedAttribute
para tratá-la como uma entidade de propriedade quando referenciada de outro tipo de entidade:
[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; }
}
Também é possível usar o método OwnsOne
em OnModelCreating
para especificar que a propriedade ShippingAddress
é uma entidade de propriedade do tipo de entidade Order
e configurar facetas adicionais, se necessário.
modelBuilder.Entity<Order>().OwnsOne(p => p.ShippingAddress);
Se a propriedade ShippingAddress
for privada no tipo Order
, você poderá usar a versão da cadeia de caracteres do método OwnsOne
:
modelBuilder.Entity<Order>().OwnsOne(typeof(StreetAddress), "ShippingAddress");
O modelo acima é mapeado para o seguinte esquema de banco de dados:
Consulte o projeto de exemplo completo para obter mais contexto.
Dica
O tipo de entidade de propriedade pode ser marcado como necessário, consulte dependentes um-para-um necessários para obter mais informações.
Chaves implícitas
Tipos de propriedade configurados com OwnsOne
ou descobertos por meio de uma navegação de referência sempre têm uma relação um-para-um com o proprietário, portanto, eles não precisam de seus próprios valores de chave, pois os valores de chave estrangeira são exclusivos. No exemplo anterior, o tipo StreetAddress
não precisa definir uma propriedade de chave.
Para entender como o EF Core rastreia esses objetos, é útil saber que uma chave primária é criada como uma propriedade de sombra para o tipo de propriedade. O valor da chave de uma instância do tipo de propriedade será o mesmo que o valor da chave da instância do proprietário.
Coleções de tipos de propriedade
Para configurar uma coleção de tipos de propriedade usados OwnsMany
em OnModelCreating
.
Os tipos de propriedade precisam de uma chave primária. Se não houver boas propriedades de candidatos no tipo .NET, o EF Core poderá tentar criar uma. No entanto, quando os tipos de propriedade são definidos por meio de uma coleção, não basta apenas criar uma propriedade de sombra para atuar como a chave estrangeira no proprietário e a chave primária da instância de propriedade, como fazemos para OwnsOne
: pode haver várias instâncias de tipo de propriedade para cada proprietário e, portanto, a chave do proprietário não é suficiente para fornecer uma identidade exclusiva para cada instância de propriedade.
As duas soluções mais simples para isso são:
- Definindo uma chave primária substituta em uma nova propriedade independente da chave estrangeira que aponta para o proprietário. Os valores contidos precisariam ser exclusivos em todos os proprietários (por exemplo, se Pai {1} tiver Filho {1}, então Pai {2} não pode ter Filho {1}), portanto, o valor não tem nenhum significado inerente. Como a chave estrangeira não faz parte da chave primária, seus valores podem ser alterados, portanto, você pode mover um filho de um pai para outro, no entanto, isso geralmente vai contra a semântica agregada.
- Usando a chave estrangeira e uma propriedade adicional como uma chave composta. O valor da propriedade adicional agora só precisa ser exclusivo para um determinado pai (portanto, se o pai {1} tiver um filho {1,1}, o pai {2} ainda poderá ter filho {2,1}). Ao tornar a chave estrangeira parte da chave primária, a relação entre o proprietário e a entidade de propriedade torna-se imutável e reflete melhor a semântica de agregação. Isso é o que o EF Core faz por padrão.
Neste exemplo, usaremos a classe Distributor
.
public class Distributor
{
public int Id { get; set; }
public ICollection<StreetAddress> ShippingCenters { get; set; }
}
Por padrão, a chave primária usada para o tipo de propriedade referenciada por meio da propriedade de navegação ShippingCenters
será ("DistributorId", "Id")
onde "DistributorId"
está o FK e "Id"
é um valor int
exclusivo.
Para configurar uma chamada de chave primária diferente HasKey
.
modelBuilder.Entity<Distributor>().OwnsMany(
p => p.ShippingCenters, a =>
{
a.WithOwner().HasForeignKey("OwnerId");
a.Property<int>("Id");
a.HasKey("Id");
});
O modelo acima é mapeado para o seguinte esquema de banco de dados:
Mapeamento de tipos de propriedade com divisão de tabela
Ao usar bancos de dados relacionais, por padrão, os tipos de propriedade de referência são mapeados para a mesma tabela que o proprietário. Isso requer a divisão da tabela em duas: algumas colunas serão usadas para armazenar os dados do proprietário e algumas colunas serão usadas para armazenar dados da entidade de propriedade. Esse é um recurso comum conhecido como divisão de tabela.
Por padrão, o EF Core nomeará as colunas de banco de dados para as propriedades do tipo de entidade de propriedade seguindo o padrão Navigation_OwnedEntityProperty. Portanto, as propriedades StreetAddress
serão exibidas na tabela 'Orders' com os nomes 'ShippingAddress_Street' e 'ShippingAddress_City'.
Você pode usar o método HasColumnName
para renomear essas colunas.
modelBuilder.Entity<Order>().OwnsOne(
o => o.ShippingAddress,
sa =>
{
sa.Property(p => p.Street).HasColumnName("ShipsToStreet");
sa.Property(p => p.City).HasColumnName("ShipsToCity");
});
Observação
A maioria dos métodos normais de configuração de tipo de entidade, como Ignore, pode ser chamada da mesma maneira.
Compartilhando o mesmo tipo .NET entre vários tipos de propriedade
Um tipo de entidade de propriedade pode ser do mesmo tipo .NET que outro tipo de entidade de propriedade, portanto, o tipo .NET pode não ser suficiente para identificar um tipo de propriedade.
Nesses casos, a propriedade que aponta do proprietário para a entidade de propriedade torna-se a navegação definidora do tipo de entidade de propriedade. Da perspectiva do EF Core, a definição de navegação faz parte da identidade do tipo junto com o tipo .NET.
Por exemplo, na classe a seguir ambos ShippingAddress
e BillingAddress
são do mesmo tipo .NET. StreetAddress
public class OrderDetails
{
public DetailedOrder Order { get; set; }
public StreetAddress BillingAddress { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
Para entender como o EF Core distinguirá instâncias controladas desses objetos, pode ser útil pensar que a definição de navegação se tornou parte da chave da instância juntamente com o valor da chave do proprietário e o tipo .NET do tipo de propriedade.
Tipos aninhados de propriedade
Neste exemplo OrderDetails
, possui BillingAddress
e ShippingAddress
, que são ambos StreetAddress
tipos. Então OrderDetails
pertence ao tipo DetailedOrder
.
public class DetailedOrder
{
public int Id { get; set; }
public OrderDetails OrderDetails { get; set; }
public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
Pending,
Shipped
}
Cada navegação para um tipo de propriedade define um tipo de entidade separado com configuração completamente independente.
Além dos tipos aninhados, um tipo de propriedade pode referenciar uma entidade regular que pode ser o proprietário ou uma entidade diferente, desde que a entidade de propriedade esteja no lado dependente. Essa funcionalidade define tipos de entidade de propriedade separados de tipos complexos no EF6.
public class OrderDetails
{
public DetailedOrder Order { get; set; }
public StreetAddress BillingAddress { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
Configurando tipos de propriedade
É possível encadear o método OwnsOne
em uma chamada fluente para configurar este modelo:
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);
});
Observe a chamada WithOwner
usada para definir a propriedade de navegação apontando de volta para o proprietário. Para definir uma navegação para o tipo de entidade de proprietário que não faz parte da relação de propriedade WithOwner()
deve ser chamada sem argumentos.
Também é possível obter esse resultado usando OwnedAttribute
em ambos OrderDetails
e StreetAddress
.
Além disso, observe a chamada Navigation
. As propriedades de navegação para tipos de propriedade podem ser configuradas ainda mais como para propriedades de navegação não pertencentes.
O modelo acima é mapeado para o seguinte esquema de banco de dados:
Armazenando tipos de propriedade em tabelas separadas
Além disso, ao contrário dos tipos complexos EF6, os tipos de propriedade podem ser armazenados em uma tabela separada do proprietário. Para substituir a convenção que mapeia um tipo de propriedade para a mesma tabela que o proprietário, você pode simplesmente chamar ToTable
e fornecer um nome de tabela diferente. O exemplo a seguir mapeará OrderDetails
e seus dois endereços para uma tabela separada de DetailedOrder
:
modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od => { od.ToTable("OrderDetails"); });
Também é possível usar TableAttribute
para fazer isso, mas observe que isso falhará se houver várias navegações para o tipo de propriedade, pois nesse caso vários tipos de entidade seriam mapeados para a mesma tabela.
Consultando tipos de propriedade
Ao consultar o proprietário, os tipos próprios serão incluídos por padrão. Não é necessário usar o método Include
, mesmo que os tipos de propriedade sejam armazenados em uma tabela separada. Com base no modelo descrito anteriormente, a consulta a seguir obterá Order
, OrderDetails
e os dois pertencentes StreetAddresses
do banco de dados:
var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.OrderDetails.ShippingAddress.City}");
Limitações
Algumas dessas limitações são fundamentais para como os tipos de entidade de propriedade funcionam, mas algumas outras são restrições que podemos remover em versões futuras:
Restrições de by-design
- Você não pode criar um tipo de propriedade
DbSet<T>
. - Não é possível chamar
Entity<T>()
com um tipo próprio.ModelBuilder
- Instâncias de tipos de entidade de propriedade não podem ser compartilhadas por vários proprietários (este é um cenário bem conhecido para objetos de valor que não podem ser implementados usando tipos de entidade de propriedade).
Deficiências atuais
- Tipos de entidade de propriedade não podem ter hierarquias de herança
Deficiências em versões anteriores
- No EF Core 2.x, as navegações de referência para tipos de entidade de propriedade não podem ser nulas, a menos que sejam explicitamente mapeadas para uma tabela separada do proprietário.
- No EF Core 3.x, as colunas para tipos de entidade de propriedade mapeadas para a mesma tabela que o proprietário são sempre marcadas como anuláveis.