Types d’entité détenus
EF Core vous permet de modéliser des types d’entités qui ne peuvent apparaître que sur les propriétés de navigation d’autres types d’entités. C’est ce qu’on appelle des types d’entités détenus. L’entité contenant un type d’entité détenu est son propriétaire.
Les entités détenues font essentiellement partie du propriétaire et ne peuvent pas exister sans lui, et sont conceptuellement similaires à des agrégats. Cela signifie que l’entité détenue est par définition du côté dépendant de la relation avec le propriétaire.
Configuration des types comme détenus
Par convention, dans la plupart des fournisseurs, les types d’entités ne sont jamais configurés comme détenus. Vous devez utiliser explicitement la méthode OwnsOne
dans OnModelCreating
ou annoter le type avec OwnedAttribute
pour configurer le type comme détenu. Le fournisseur Azure Cosmos DB est une exception. Étant donné qu’Azure Cosmos DB est une base de données de documents, le fournisseur configure par défaut tous les types d’entités associés comme détenus.
Dans cet exemple, StreetAddress
est un type sans propriété d’identité. Il est utilisé comme propriété du type Order pour spécifier l’adresse d’expédition d’une commande particulière.
Nous pouvons utiliser l’attribut OwnedAttribute
pour le traiter comme une entité détenue lorsqu’elle est référencée à partir d’un autre type d’entité :
[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; }
}
Il est également possible d’utiliser la méthode OwnsOne
dans OnModelCreating
pour spécifier que la propriété ShippingAddress
est une entité détenue du type d’entité Order
, puis de configurer des facettes supplémentaires si nécessaire.
modelBuilder.Entity<Order>().OwnsOne(p => p.ShippingAddress);
Si la propriété ShippingAddress
est privée dans le type Order
, vous pouvez utiliser la version de chaîne de la méthode OwnsOne
:
modelBuilder.Entity<Order>().OwnsOne(typeof(StreetAddress), "ShippingAddress");
Le modèle ci-dessus est mappé au schéma de base de données suivant :
Pour plus de contexte, consultez l’exemple de projet complet.
Conseil
Le type d’entité détenu peut être marqué comme obligatoire. Pour en savoir plus, consultez Dépendants un-à-un requis.
Clés implicites
Les types détenus configurés avec OwnsOne
ou découverts par le biais d’une navigation de référence ont toujours une relation un-à-un avec le propriétaire. Par conséquent, ils n’ont pas besoin de leurs propres valeurs de clé, car les valeurs de clé étrangère sont uniques. Dans l’exemple précédent, le type StreetAddress
n’a pas besoin de définir une propriété de clé.
Pour comprendre comment EF Core effectue le suivi de ces objets, il est utile de savoir qu’une clé primaire est créée en tant que propriété cachée pour le type détenu. La valeur de la clé d’une instance du type détenu est identique à la valeur de la clé de l’instance propriétaire.
Collections de types détenus
Pour configurer une collection de types détenus, utilisez OwnsMany
dans OnModelCreating
.
Les types détenus ont besoin d’une clé primaire. S’il n’existe pas de bonnes propriétés candidates sur le type .NET, EF Core peut essayer d’en créer une. Toutefois, lorsque les types détenus sont définis par le biais d’une collection, il n’est pas suffisant de créer une propriété cachée pour servir à la fois de clé étrangère dans le propriétaire et de clé primaire de l’instance détenue, comme nous le faisons pour OwnsOne
. Il peut y avoir plusieurs instances de type détenu pour chaque propriétaire, et la clé du propriétaire n’est donc pas suffisante pour fournir une identité unique à chaque instance détenue.
Les deux solutions les plus simples sont les suivantes :
- Définir une clé primaire de substitution sur une nouvelle propriété indépendante de la clé étrangère qui pointe vers le propriétaire. Les valeurs contenues doivent être uniques pour tous les propriétaires (p. ex., si le Parent {1} a l’Enfant {1}, le Parent {2} ne peut pas avoir l’Enfant {1}), de sorte que la valeur n’a aucune signification inhérente. Étant donné que la clé étrangère ne fait pas partie de la clé primaire, ses valeurs peuvent être modifiées. Vous pouvez donc déplacer un enfant d’un parent à un autre, bien que cela aille généralement à l’encontre de la sémantique d’agrégation.
- Utiliser la clé étrangère et une propriété supplémentaire comme clé composite. La valeur de propriété supplémentaire doit alors seulement être unique pour un parent donné (donc si le Parent {1} a l’Enfant {1,1}, le Parent {2} peut toujours avoir l’Enfant {2,1}). En rendant la clé étrangère partie intégrante de la clé primaire, la relation entre le propriétaire et l’entité détenue devient immuable et reflète mieux la sémantique d’agrégation. C’est ce que fait EF Core par défaut.
Dans cet exemple, nous allons utiliser la classe Distributor
.
public class Distributor
{
public int Id { get; set; }
public ICollection<StreetAddress> ShippingCenters { get; set; }
}
Par défaut, la clé primaire utilisée pour le type détenu référencé via la propriété de navigation ShippingCenters
sera ("DistributorId", "Id")
, où "DistributorId"
est la clé étrangère et "Id"
est une valeur int
unique.
Pour configurer une autre clé primaire, appelez HasKey
.
modelBuilder.Entity<Distributor>().OwnsMany(
p => p.ShippingCenters, a =>
{
a.WithOwner().HasForeignKey("OwnerId");
a.Property<int>("Id");
a.HasKey("Id");
});
Le modèle ci-dessus est mappé au schéma de base de données suivant :
Mappage des types détenus avec fractionnement de table
Lorsque vous utilisez des bases de données relationnelles, les types détenus de référence par défaut sont mappés à la même table que le propriétaire. Cela nécessite le fractionnement de la table en deux : certaines colonnes sont utilisées pour stocker les données du propriétaire, et d’autres sont utilisées pour stocker les données de l’entité détenue. Il s’agit d’une fonctionnalité courante appelée fractionnement de table.
Par défaut, EF Core nomme les colonnes de la base de données pour les propriétés du type d’entité détenu selon le modèle Navigation_OwnedEntityProperty. Par conséquent, les propriétés StreetAddress
s’affichent dans la table 'Orders' avec les noms 'ShippingAddress_Street' et 'ShippingAddress_City'.
Vous pouvez utiliser la méthode HasColumnName
pour renommer ces colonnes.
modelBuilder.Entity<Order>().OwnsOne(
o => o.ShippingAddress,
sa =>
{
sa.Property(p => p.Street).HasColumnName("ShipsToStreet");
sa.Property(p => p.City).HasColumnName("ShipsToCity");
});
Remarque
La plupart des méthodes normales de configuration du type d’entité, comme Ignore, peuvent être appelées de la même façon.
Partage du même type .NET entre plusieurs types détenus
Comme un type d’entité détenu peut être du même type .NET qu’un autre type d’entité détenu, le type .NET peut ne pas suffire pour identifier un type détenu.
Dans ce cas, la propriété pointant du propriétaire à l’entité détenue devient la navigation de définition du type d’entité détenu. Du point de vue d’EF Core, la navigation de définition fait partie de l’identité du type, au même titre que le type .NET.
Par exemple, dans la classe suivante, ShippingAddress
et BillingAddress
sont tous deux du même type .NET : StreetAddress
.
public class OrderDetails
{
public DetailedOrder Order { get; set; }
public StreetAddress BillingAddress { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
Pour comprendre comment EF Core distingue les instances suivies de ces objets, il faut imaginer que la navigation de définition est devenue partie intégrante de la clé de l’instance, au même titre que la valeur de la clé du propriétaire et que le type .NET du type détenu.
Types détenus imbriqués
Dans cet exemple, OrderDetails
est le propriétaire de BillingAddress
et ShippingAddress
, qui sont tous deux des types StreetAddress
. OrderDetails
est alors détenu par le type DetailedOrder
.
public class DetailedOrder
{
public int Id { get; set; }
public OrderDetails OrderDetails { get; set; }
public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
Pending,
Shipped
}
Chaque navigation vers un type détenu définit un type d’entité distinct avec une configuration totalement indépendante.
En plus des types détenus imbriqués, un type détenu peut référencer une entité régulière qui peut être le propriétaire ou une entité distincte, tant que l’entité détenue est du côté dépendant. Cette capacité distingue les types d’entités détenus des types complexes dans EF6.
public class OrderDetails
{
public DetailedOrder Order { get; set; }
public StreetAddress BillingAddress { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
Configuration des types détenus
Il est possible de chaîner la méthode OwnsOne
dans un appel Fluent pour configurer ce modèle :
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);
});
Notez que l’appel WithOwner
utilisé pour définir la propriété de navigation pointe vers le propriétaire. Pour définir une navigation vers le type d’entité propriétaire ne faisant pas partie de la relation de propriété, WithOwner()
doit être appelé sans argument.
Il est également possible d’obtenir ce résultat en utilisant OwnedAttribute
sur à la fois OrderDetails
et StreetAddress
.
Notez en outre l’appel Navigation
. Les propriétés de navigation vers des types détenus peuvent être configurées plus en avant, comme pour les propriétés de navigation non détenues.
Le modèle ci-dessus est mappé au schéma de base de données suivant :
Stockage de types détenus dans des tables distinctes
Par ailleurs, et contrairement aux types complexes EF6, les types détenus peuvent être stockés dans une table distincte du propriétaire. Pour remplacer la convention qui mappe un type détenu à la même table que le propriétaire, vous pouvez simplement appeler ToTable
et fournir un nom de table différent. L’exemple suivant mappe OrderDetails
et ses deux adresses à une table distincte de DetailedOrder
:
modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od => { od.ToTable("OrderDetails"); });
Il est également possible d’utiliser l’attribut TableAttribute
pour effectuer cette opération, mais notez que cela échouera s’il existe plusieurs navigations vers le type détenu, car cela signifierait que plusieurs types d’entités sont mappés à la même table.
Interrogation des types détenus
Quand le propriétaire fait l’objet d’une interrogation, les types détenus sont inclus par défaut. Il n’est pas nécessaire d’utiliser la méthode Include
, même si les types détenus sont stockés dans une table distincte. En fonction du modèle décrit précédemment, la requête suivante obtient Order
, OrderDetails
et les deux types détenus StreetAddresses
depuis la base de données :
var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.OrderDetails.ShippingAddress.City}");
Limites
Certaines de ces limitations sont fondamentales pour le fonctionnement des types d’entités détenus, mais d’autres constituent des restrictions que nous pourrions supprimer dans les futures versions :
Restrictions de conception
- Vous ne pouvez pas créer de
DbSet<T>
pour un type détenu. - Vous ne pouvez pas appeler
Entity<T>()
avec un type détenu surModelBuilder
. - Les instances de types d’entités détenus ne peuvent pas être partagées par plusieurs propriétaires (il s’agit d’un scénario bien connu pour les objets de valeur qui ne peuvent pas être implémentés à l’aide de types d’entités détenus).
Lacunes actuelles
- Les types d’entités détenus ne peuvent pas avoir de hiérarchies d’héritage
Lacunes dans les précédentes versions
- Dans EF Core 2.x, les navigations de référence vers des types d’entités détenus ne peuvent pas être définies sur la valeur Null, sauf si elles sont explicitement mappées à une table distincte du propriétaire.
- Dans EF Core 3.x, les colonnes pour les types d’entités détenus mappés à la même table que le propriétaire sont toujours marquées comme pouvant accepter la valeur Null.