Partager via


Configuration du modèle avec le fournisseur EF Core Azure Cosmos DB

Conteneurs et types d'entités

Dans Azure Cosmos DB, les documents JSON sont stockés dans des conteneurs. Contrairement aux tables des bases de données relationnelles, les conteneurs de Azure Cosmos DB peuvent contenir des documents de formes différentes - un conteneur n’impose pas un schéma uniforme à ses documents. Cependant, diverses options de configuration sont définies au niveau du conteneur et affectent donc tous les documents qu'il contient. Consultez la documentation de Azure Cosmos DB sur les conteneurs pour plus d’informations.

Par défaut, EF mappe tous les types d'entités vers le même conteneur ; il s'agit généralement d'une bonne valeur par défaut en termes de performances et de prix. Le conteneur par défaut est nommé d'après le type de contexte .NET (OrderContext dans ce cas). Pour modifier le nom du conteneur par défaut, utilisez HasDefaultContainer :

modelBuilder.HasDefaultContainer("Store");

Pour mapper un type d’entité à un autre conteneur, utilisez ToContainer :

modelBuilder.Entity<Order>().ToContainer("Orders");

Avant de mapper les types d’entités à différents conteneurs, assurez-vous de comprendre les implications potentielles en termes de performances et de prix (par exemple, en ce qui concerne le débit dédié et partagé) ; consultez la documentation de Azure Cosmos DB pour en savoir plus.

ID et clés

Azure Cosmos DB exige que tous les documents aient une propriété JSON id qui les identifie de manière unique. Comme les autres fournisseurs EF, le fournisseur EF Azure Cosmos DB tentera de trouver une propriété nommée Id ou <type name>Id, et configurera cette propriété comme la clé de votre type d’entité, en la mappant à la propriété JSON id. Vous pouvez configurer n'importe quelle propriété pour qu'elle devienne la propriété clé en utilisant HasKey voir la documentation générale de EF sur les clés pour plus d'informations.

Les développeurs qui arrivent à Cosmos Azure DB depuis d’autres bases de données s’attendent parfois à ce que la propriété key (Id) soit générée automatiquement. Par exemple, sur SQL Server, EF configure les propriétés des clés numériques comme des colonnes IDENTITY, où des valeurs auto-incrémentées sont générées dans la base de données. En revanche, Azure Cosmos DB ne prend pas en charge la génération automatique des propriétés, et les propriétés de clé doivent donc être définies explicitement. Si vous insérez un type d'entité dont la propriété clé n'est pas définie, la valeur par défaut du CLR pour cette propriété sera simplement insérée (par exemple, 0 pour int), et une deuxième insertion échouera ; EF émet un problème si vous essayez de faire cela.

Si vous souhaitez utiliser un GUID comme propriété de clé, vous pouvez configurer EF pour qu'il génère des valeurs uniques et aléatoires au niveau du client :

modelBuilder.Entity<Session>().Property(b => b.Id).HasValueGenerator<GuidValueGenerator>();

Clés de partition

Azure Cosmos DB utilise le partitionnement pour réaliser une mise à l'échelle horizontale ; une modélisation appropriée et une sélection minutieuse de la clé de partition sont essentielles pour obtenir de bonnes performances et maintenir les coûts à un niveau bas. Il est fortement recommandé de lire la documentation de Azure Cosmos DB sur le partitionnement et de planifier votre stratégie de partitionnement à l’avance.

Pour configurer la clé de partitionnement avec EF, appelez HasPartitionKey, en lui transmettant une propriété ordinaire de votre type d’entité :

modelBuilder.Entity<Order>().HasPartitionKey(o => o.PartitionKey);

Toute propriété peut être transformée en clé de partition à condition d'être convertie en chaîne de caractères. Une fois configurée, la propriété de clé de partition doit toujours avoir une valeur non nulle ; essayer d'insérer un nouveau type d'entité avec une propriété de clé de partition non définie entraînera une erreur.

Notez que Azure Cosmos DB permet à deux documents ayant la même propriété id d’exister dans un conteneur, à condition qu’ils soient dans des partitions différentes ; cela signifie que pour identifier de manière unique un document dans un conteneur, les propriétés id et la clé de partition doivent toutes deux être fournies. Pour cette raison, la notion interne d'EF de clé primaire d'entité contient ces deux éléments par convention, contrairement aux bases de données relationnelles, par exemple, où il n'y a pas de concept de clé de partition. Cela signifie par exemple que FindAsync nécessite des propriétés de clé et de clé de partition (voir d'autres documents), et qu'une requête doit les spécifier dans sa clause Where pour bénéficier d'un point reads efficace et rentable.

Notez que la clé de partition est définie au niveau du conteneur. Cela signifie notamment qu’il est impossible pour plusieurs types d’entités dans le même conteneur d’avoir des propriétés de clé de partition différentes. Si vous devez définir des clés de partition différentes, mappez les types d'entités concernés dans des conteneurs différents.

Clés de partition hiérarchiques

Azure Cosmos DB supporte également les clés de partition hiérarchiques afin d’optimiser encore plus la distribution des données; consultez la documentation pour plus de détails. EF 9.0 a ajouté la prise en charge des clés de partition hiérarchiques ; pour les configurer, il suffit de transmettre jusqu'à trois propriétés à HasPartitionKey :

modelBuilder.Entity<Order>().HasPartitionKey(o => new { e.TenantId, e.UserId, e.SessionId });

Avec une telle clé de partition hiérarchique, les requêtes peuvent facilement être envoyées uniquement à un sous-ensemble pertinent de sous-partitions. Par exemple, si vous demandez les commandes d'un locataire spécifique, ces requêtes ne seront exécutées que dans les sous-partitions de ce locataire.

Si vous ne configurez pas de clé de partition avec EF, un journal d'avertissement s'affichera au démarrage ; EF Core créera des conteneurs avec la clé de partition fixée à __partitionKey, et ne fournira aucune valeur pour cette clé lors de l'insertion d'éléments. Lorsqu’aucune clé de partition n’est définie, votre conteneur sera limité à 20 Go de données, ce qui correspond à la capacité maximale de stockage d’une seule partition logique. Bien que cela puisse fonctionner pour des applications de développement ou de test de petite taille, il est fortement déconseillé de déployer une application en production sans une stratégie de clé de partition bien configurée.

Une fois que les propriétés de vos clés de partition sont correctement configurées, vous pouvez leur attribuer des valeurs dans les requêtes; voir Requête avec des clés de partition pour plus d’informations.

Discriminateurs

Étant donné que plusieurs types d'entités peuvent être mappés dans le même conteneur, EF Core ajoute toujours un discriminateur $type à tous les documents JSON que vous enregistrez (cette propriété était appelée Discriminator avant EF 9.0) ; cela permet à EF de reconnaître les documents chargés à partir de la base de données et de matérialiser le bon type .NET. Les développeurs issus de bases de données relationnelles connaissent peut-être les discriminateurs dans le contexte de l’héritage de tables par hiérarchie (TPH) ; dans Azure Cosmos DB, les discriminateurs sont utilisés non seulement dans les scénarios de mappage de l’héritage, mais aussi parce qu’un même conteneur peut contenir des types de documents complètement différents.

Le nom et les valeurs de la propriété du discriminateur peuvent être configurés à l'aide des API EF standard, voir ces documents pour plus d'informations. Si vous mappez un seul type d'entité à un conteneur, que vous êtes certain de ne jamais en mapper un autre et que vous souhaitez vous débarrasser de la propriété discriminator, appelez HasNoDiscriminator :

modelBuilder.Entity<Order>().HasNoDiscriminator();

Étant donné qu’un même conteneur peut contenir des entités de types différents et que la propriété JSON id doit être unique au sein d’une partition de conteneur, vous ne pouvez pas avoir la même valeur id pour des entités de types différents dans la même partition de conteneur. Comparez cela aux bases de données relationnelles, où chaque type d'entité est mappé sur une table différente, et possède donc son propre espace de clés. Il est donc de votre responsabilité de garantir l'unicité id des documents que vous insérez dans un conteneur. Si vous devez avoir différents types d'entités avec les mêmes valeurs de clé primaire, vous pouvez demander à EF d'insérer automatiquement le discriminant dans la propriété id comme suit :

modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();

Bien que cela puisse faciliter le travail avec les valeurs id, cela peut rendre plus difficile l'interopérabilité avec les applications externes qui travaillent avec vos documents, car elles doivent maintenant connaître le format concaténé id de EF, ainsi que les valeurs du discriminateur, qui sont par défaut dérivées de vos types .NET. Notez que c'était le comportement par défaut avant EF 9.0.

Une option supplémentaire consiste à demander à EF d'insérer uniquement le discriminateur racine, qui est le discriminateur du type d'entité racine de la hiérarchie, dans la propriété id :

modelBuilder.Entity<Session>().HasRootDiscriminatorInJsonId();

Cette méthode est similaire, mais elle permet à EF d'utiliser des lectures de points efficaces dans un plus grand nombre de scénarios. Si vous devez insérer un discriminant dans la propriété id, envisagez d'insérer le discriminant racine pour de meilleures performances.

Débit approvisionné

Si vous utilisez EF Core pour créer la base de données ou les conteneurs Azure Cosmos DB, vous pouvez configurer le débit provisionné pour la base de données en appelant CosmosModelBuilderExtensions.HasAutoscaleThroughput ou CosmosModelBuilderExtensions.HasManualThroughput. Par exemple :

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

Pour configurer le débit provisionné pour un appel de conteneur CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput ou CosmosEntityTypeBuilderExtensions.HasManualThroughput. Par exemple :

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

Durée de vie

Les types d'entités dans le modèle Azure Cosmos DB peuvent être configurés avec un temps de vie par défaut. Par exemple :

modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);

Ou, pour le magasin analytique :

modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);

La durée de vie des entités individuelles peut être définie à l’aide d’une propriété mappée à « ttl » dans le document JSON. Par exemple :

modelBuilder.Entity<Village>()
    .HasDefaultTimeToLive(3600)
    .Property(e => e.TimeToLive)
    .ToJsonProperty("ttl");

Remarque

Une durée de vie par défaut doit être configurée sur le type d’entité pour que le « ttl » ait un effet. Pour plus d’informations, consultez Durée de vie (TTL) dans Azure Cosmos DB.

La propriété de durée de vie est ensuite définie avant l’enregistrement de l’entité. Par exemple :

var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();

La propriété de durée de vie peut être une propriété d’ombre pour éviter de polluer l’entité de domaine avec des problèmes de base de données. Par exemple :

modelBuilder.Entity<Hamlet>()
    .HasDefaultTimeToLive(3600)
    .Property<int>("TimeToLive")
    .ToJsonProperty("ttl");

La propriété de temps de vie de l’ombre est ensuite définie par l’accès à l’entité suivie. Par exemple :

var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();

Entités incorporées

Remarque

Les types d’entités associés sont configurés comme appartenant par défaut. Pour empêcher cela pour un appel ModelBuilder.Entity de type d’entité spécifique.

Pour Azure Cosmos DB, les entités détenues sont incorporées dans le même élément que le propriétaire. Pour changer un nom de propriété, utilisez ToJsonProperty :

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

Avec cette configuration, la commande dans l’exemple ci-dessus est stockée comme suit :

{
    "Id": 1,
    "PartitionKey": "1",
    "TrackingNumber": null,
    "id": "1",
    "Address": {
        "ShipsToCity": "London",
        "ShipsToStreet": "221 B Baker St"
    },
    "_rid": "6QEKAM+BOOABAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKAM+BOOA=/docs/6QEKAM+BOOABAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-692e763901d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163674
}

Les collections d’entités détenues sont également incorporées. Pour l’exemple suivant, nous allons utiliser la classe Distributor avec la collection StreetAddress :

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

Les entités détenues n’ont pas besoin de fournir des valeurs de clés explicites pour être stockées :

var distributor = new Distributor
{
    Id = 1,
    ShippingCenters = new HashSet<StreetAddress>
    {
        new StreetAddress { City = "Phoenix", Street = "500 S 48th Street" },
        new StreetAddress { City = "Anaheim", Street = "5650 Dolly Ave" }
    }
};

using (var context = new OrderContext())
{
    context.Add(distributor);

    await context.SaveChangesAsync();
}

Elles sont conservées de cette façon :

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        },
        {
            "City": "Anaheim",
            "Street": "5650 Dolly Ave"
        }
    ],
    "_rid": "6QEKANzISj0BAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKANzISj0=/docs/6QEKANzISj0BAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-7b2b439701d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163705
}

En interne, EF Core doit toujours avoir des valeurs de clé uniques pour toutes les entités suivies. La clé primaire créée par défaut pour les collections de types détenus se compose des propriétés de clé étrangère qui pointent vers le propriétaire et d’une propriété int correspondant à l’index dans le tableau JSON. Pour récupérer ces valeurs, vous pouvez utiliser l’API d’entrée :

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers: {firstDistributor.ShippingCenters.Count}");

    var addressEntry = context.Entry(firstDistributor.ShippingCenters.First());
    var addressPKProperties = addressEntry.Metadata.FindPrimaryKey().Properties;

    Console.WriteLine(
        $"First shipping center PK: ({addressEntry.Property(addressPKProperties[0].Name).CurrentValue}, {addressEntry.Property(addressPKProperties[1].Name).CurrentValue})");
    Console.WriteLine();
}

Conseil

Quand cela est nécessaire, la clé primaire par défaut pour les types d’entités détenus peut être changée, mais les valeurs de clé doivent être fournies explicitement.

Collection de types primitifs

Les collections de types primitifs pris en charge, telles que string et int, sont découvertes et mappées automatiquement. Les collections prises en charge sont tous les types qui implémentent IReadOnlyList<T> ou IReadOnlyDictionary<TKey,TValue>. Par exemple, prenons le type d'entité :

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

Les champs IList et IDictionary peuvent être remplis et conservés dans la base de données :

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
await context.SaveChangesAsync();

Cela entraîne le document JSON suivant :

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

Ces collections peuvent ensuite être mises à jour, de nouveau de la manière normale :

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

await context.SaveChangesAsync();

Limites :

  • Seuls les dictionnaires avec des clés de type chaîne sont pris en charge.
  • La prise en charge des requêtes dans les collections primitives a été ajoutée dans EF Core 9.0.

Accès concurrentiel optimiste avec les eTags

Pour configurer un type d’entité afin d’utiliser un appel concurrentiel optimiste UseETagConcurrency. Cet appel crée une propriété _etag dans un état d’ombre et la définit comme jeton d’accès concurrentiel.

modelBuilder.Entity<Order>()
    .UseETagConcurrency();

Pour faciliter la résolution des erreurs d’accès concurrentiel, vous pouvez mapper l’eTag à une propriété CLR à l’aide de IsETagConcurrency.

modelBuilder.Entity<Distributor>()
    .Property(d => d.ETag)
    .IsETagConcurrency();