Partilhar via


Tipos de entidades possuídas

O EF Core permite modelar tipos de entidade que só podem aparecer nas propriedades de navegação de outros tipos de entidade. Estes são chamados tipos de entidades próprias. A entidade que contém um tipo de entidade de propriedade é o seu proprietário.

As entidades detidas são essencialmente uma parte do proprietário e não podem existir sem ele, são conceptualmente semelhantes aos agregados. Isto significa que a entidade detida está, por definição, do lado dependente da relação com o proprietário.

Configurando tipos como propriedade

Na maioria dos provedores, os tipos de entidade nunca são configurados como propriedade por convenção - é necessário usar explicitamente o método OwnsOne em OnModelCreating ou anotar o tipo com OwnedAttribute para configurar o tipo como propriedade. O provedor do Azure Cosmos DB é uma exceção a isso. Como o Azure Cosmos DB é um banco de dados de documentos, o provedor configura todos os tipos de entidade relacionados como propriedade por padrão.

Neste exemplo, StreetAddress é um tipo sem propriedade de identidade. É usado como uma propriedade do tipo Pedido para especificar o endereço de entrega para um determinado pedido.

Podemos usar o OwnedAttribute para tratá-lo como uma entidade de propriedade quando referenciado a partir 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 Proprietária do tipo de entidade Order e configurar facetas adicionais, se necessário.

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

Se a ShippingAddress propriedade for privada no Order tipo, você pode usar a versão de cadeia de caracteres do OwnsOne método:

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

O modelo acima é mapeado para o seguinte esquema de banco de dados:

Imagem do ecrã do modelo de base de dados para entidade que contém referência própria

Consulte o projeto de exemplo completo para obter mais contexto.

Sugestão

O tipo de entidade possuída pode ser marcado como necessário, consulte Dependentes necessários um-para-um para obter mais informações.

Chaves implícitas

Os tipos de propriedade configurados com OwnsOne ou descobertos por uma navegação de referência têm sempre uma relação um-para-um com o proprietário, portanto, não precisam de suas próprias chaves, pois os valores de chave estrangeira são exclusivos. No exemplo anterior, o StreetAddress tipo 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 oculta para o tipo proprietário. O valor da chave de uma instância do tipo owned será o mesmo que o valor da chave da instância owner.

Coleções de tipos próprios

Para configurar uma coleção de tipos próprios, use OwnsMany em OnModelCreating.

Os tipos de propriedade precisam de uma chave primária. Se não houver boas propriedades candidatas 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 é suficiente apenas criar uma propriedade sombra para atuar como a chave estrangeira para o 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:

  • Definir 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 tem Filho {1}{1}, então Pai {2} não pode ter Filho {1}), para que o valor não tenha qualquer significado inerente. Como a chave estrangeira não faz parte da chave primária, seus valores podem ser alterados, então 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 apenas precisa ser único para um determinado pai (assim, se o Pai {1} tem o Filho {1,1}, então o Pai {2} ainda pode ter o Filho {2,1}). Ao tornar a chave estrangeira parte da chave primária, a relação entre o proprietário e a entidade proprietária torna-se imutável e reflete melhor a semântica agregada. Isso é o que o EF Core faz por padrão.

Neste exemplo, usaremos a Distributor classe.

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 referenciado através da ShippingCenters propriedade de navegação será ("DistributorId", "Id") onde "DistributorId" é o FK e "Id" é um valor exclusivo int .

Para configurar uma chave primária diferente, ligue para 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:

Captura de tela do modelo de banco de dados para entidade que contém coleção própria

Mapeamento de tipos próprios com partilha de tabelas

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. Este é um recurso comum conhecido como divisão de tabela.

Por padrão, o EF Core nomeará as colunas do banco de dados para as propriedades do tipo de entidade de propriedade seguindo o padrão Navigation_OwnedEntityProperty. Portanto, as StreetAddress propriedades aparecerão na tabela 'Pedidos' com os nomes 'ShippingAddress_Street' e 'ShippingAddress_City'.

Você pode usar o HasColumnName método 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 possuída torna-se a navegação definidora do tipo de entidade possuída. Da perspetiva do EF Core, a navegação definidora faz parte da identidade do tipo ao lado do tipo .NET.

Por exemplo, na classe ShippingAddress a seguir e BillingAddress ambos 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 monitoradas desses objetos, pode ser útil pensar que a navegação que define passa a constituir parte da chave da instância, juntamente com o valor da chave do proprietário e o tipo .NET do tipo possuído.

Tipos de propriedades aninhados

Neste exemplo OrderDetails possui BillingAddress e ShippingAddress, que são ambos tipos de StreetAddress. Então OrderDetails é propriedade do 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 próprio define um tipo de entidade separado com configuração completamente independente.

Além dos tipos de propriedade aninhados, um tipo de propriedade pode fazer referência a uma entidade regular que pode ser o proprietário ou uma entidade diferente, desde que a entidade de propriedade esteja no lado dependente. Esse recurso diferencia os tipos de entidade de propriedade dos tipos complexos no EF6.

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

Configurando tipos próprios

É possível encadear o OwnsOne método 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);
    });

Note a chamada WithOwner utilizada 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 chamado sem quaisquer argumentos.

Também é possível alcançar este resultado usando OwnedAttribute tanto em OrderDetails quanto em StreetAddress.

Além disso, observe a Navigation chamada. As propriedades de navegação para tipos de propriedade própria podem ser configuradas como para propriedades de navegação de tipos não próprios.

O modelo acima é mapeado para o seguinte esquema de banco de dados:

Captura de ecrã do modelo de base de dados para entidade que contém referências próprias aninhadas

Armazenando tipos próprios em tabelas separadas

Também 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 possuído para a mesma tabela que o proprietário, basta 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 o TableAttribute para fazer isso, mas observe que isso falharia se houver várias navegações para o tipo de propriedade, já que, nesse caso, vários tipos de entidade seriam mapeados para a mesma tabela.

Consultando tipos de propriedade

Ao consultar o proprietário, os tipos de propriedade serão incluídos por padrão. Não é necessário usar o Include método, 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 duas instâncias de StreetAddresses do banco de dados:

var order = await context.DetailedOrders.FirstAsync(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 o funcionamento dos tipos de entidades proprietárias, mas outras são restrições que poderemos remover em versões futuras:

Restrições em função da conceção

  • Não é possível criar um DbSet<T> para um tipo próprio.
  • Não é possível invocar Entity<T>() com um tipo possuído em ModelBuilder.
  • As instâncias de tipos de entidades próprias não podem ser compartilhadas por múltiplos proprietários (este é um cenário conhecido para objetos de valor que não podem ser adequadamente implementados usando tipos de entidades próprias).

Insuficiências atuais

  • Os tipos de entidade de propriedade não podem ter hierarquias de herança

Insuficiências nas 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 mapeados para a mesma tabela que o proprietário são sempre marcadas como anuláveis.