Amorçage des données

L’amorçage des données est le processus de remplissage d’une base de données avec un ensemble initial de données.

Il existe plusieurs façons d’effectuer cette opération dans EF Core :

Options de configuration UseSeeding et méthodes UseAsyncSeeding

EF 9 a introduit les méthodes UseSeeding et UseAsyncSeeding, qui offrent un moyen pratique d’amorcer la base de données avec des données initiales. Ces méthodes visent à améliorer l’expérience d’utilisation de la logique d’initialisation personnalisée (expliquée ci-dessous). Elles fournissent un emplacement clair où tout le code d’amorçage des données peut être placé. De plus, le code à l’intérieur des méthodes UseSeeding et UseAsyncSeeding est protégé par le mécanisme de verrouillage de migration pour éviter les problèmes de concurrence.

Les nouvelles méthodes d’amorçage sont appelées dans le cadre de l’opération EnsureCreated, Migrate et de la commande dotnet ef database update, même s’il n’y a pas de modifications du modèle et qu’aucune migration n’a été appliquée.

Conseil

L’utilisation des méthodes UseSeeding et UseAsyncSeeding est le moyen recommandé pour amorcer la base de données avec des données initiales lors de l’utilisation d’EF Core.

Ces méthodes peuvent être configurées lors de l’étape de configuration des options. Voici un exemple :

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
        .UseSeeding((context, _) =>
        {
            var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });

Remarque

UseSeeding / UseAsyncSeedingsont appelés pendant EnsureCreated/EnsureCreatedAsync et après les migrations sont appliqués (par exemple,/MigrateMigrateAsync , dotnet ef database updateet les bundles de migration). Les outils et les bundles EF Core s’appuient actuellement sur le délégué synchrone. Par conséquent, implémentez UseSeeding toujours même si votre application utilise normalement des API asynchrones.

Comportement de déploiement

UseSeeding et UseAsyncSeeding s’exécute uniquement lorsque EF Core effectue une opération d’initialisation ou de migration de base de données. Choisissez un mécanisme de déploiement en conséquence :

Operation Délégué d’amorçage appelé
EnsureCreated ou Migrate UseSeeding
EnsureCreatedAsync ou MigrateAsync UseAsyncSeeding
dotnet ef database update ou Update-Database UseSeeding
Bundle de migration UseSeeding
Script SQL exécuté par un outil SQL externe None

Pour un déploiement automatisé qui doit s’exécuter UseSeeding, utilisez un bundle de migration ou un processus d’initialisation dédié. Les outils et bundles EF Core appellent le délégué synchrone. Par conséquent, implémentez UseSeeding toujours même si l’application utilise normalement des API asynchrones. Utilisez un script SQL lorsque l’exécution de la révision ou de l’administrateur de base de données est requise et que les données initiales sont représentées par les opérations de migration à la place. Consultez l’application des migrations pour les compromis.

L’amorçage s’exécute également après une rétrogradation de la migration. Si l’application prend en charge la rétrogradation vers une migration qui ne contient pas chaque table utilisée par le code d’amorçage, vérifiez que le schéma requis existe avant de l’interroger. Cela est particulièrement important lors de la restauration de toutes les migrations en ciblant 0.

Les applications Aspire peuvent coordonner l’exécution de la migration locale et publier des ensembles de migration ou des scripts avec l’intégration des migrations Aspire EF Core.

Logique d’initialisation personnalisée

Un moyen simple et puissant d’effectuer l’amorçage des données consiste à utiliser SaveChangesAsync avant que la logique d’application principale commence l’exécution. Il est recommandé d’utiliser les méthodes UseSeeding et UseAsyncSeeding à cette fin, cependant, parfois, l’utilisation de ces méthodes n’est pas une bonne solution. Un scénario d’exemple est lorsque l’amorçage nécessite l’utilisation de deux contextes différents dans une transaction unique. Voici un exemple de code effectuant une initialisation personnalisée directement dans l’application :

using (var context = new DataSeedingContext())
{
    await context.Database.EnsureCreatedAsync();

    var testBlog = await context.Blogs.FirstOrDefaultAsync(b => b.Url == "http://test.com");
    if (testBlog == null)
    {
        context.Blogs.Add(new Blog { Url = "http://test.com" });
        await context.SaveChangesAsync();
    }
}

Avertissement

Le code d’amorçage ne doit pas faire partie de l’exécution normale de l’application, car cela peut entraîner des problèmes d’accès concurrentiel lorsque plusieurs instances sont en cours d’exécution et nécessiterait également que l’application ait l’autorisation de modifier le schéma de la base de données.

Selon les contraintes de votre déploiement, le code d’initialisation peut être exécuté de différentes façons :

  • Exécution locale de l’application d’initialisation
  • Déploiement de l’application d’initialisation avec l’application principale, avec un appel de la routine d’initialisation et la désactivation ou suppression de l’application d’initialisation.

Cela peut généralement être automatisé à l’aide de profils de publication.

Données gérées par le modèle

Les données peuvent également être associées à un type d’entité dans le cadre de la configuration du modèle. Ainsi, les migrations EF Core peuvent automatiquement déterminer quelles opérations d'insertion, de mise à jour ou de suppression doivent être réalisées lors de la mise à niveau de la base de données vers une nouvelle version du modèle.

Avertissement

Les migrations ne considèrent que les modifications du modèle pour déterminer quelle opération doit être effectuée pour amener les données gérées à l’état souhaité. Ainsi, les modifications apportées aux données effectuées en dehors des migrations peuvent être perdues ou provoquer une erreur.

Par exemple, cela configurera des données gérées pour un Country dans OnModelCreating :

modelBuilder.Entity<Country>(b =>
{
    b.Property(x => x.Name).IsRequired();
    b.HasData(
        new Country { CountryId = 1, Name = "USA" },
        new Country { CountryId = 2, Name = "Canada" },
        new Country { CountryId = 3, Name = "Mexico" });
});

Pour ajouter des entités qui ont une relation, les valeurs de clé étrangère doivent être spécifiées :

modelBuilder.Entity<City>().HasData(
    new City { Id = 1, Name = "Seattle", LocatedInId = 1 },
    new City { Id = 2, Name = "Vancouver", LocatedInId = 2 },
    new City { Id = 3, Name = "Mexico City", LocatedInId = 3 },
    new City { Id = 4, Name = "Puebla", LocatedInId = 3 });

Lors de la gestion des données pour des navigations plusieurs-à-plusieurs, l’entité de jointure doit être configurée explicitement. Si le type d’entité a des propriétés dans un état de shadow (par exemple, l’entité de jointure LanguageCountry ci-dessous), une classe anonyme peut être utilisée pour fournir les valeurs :

modelBuilder.Entity<Language>(b =>
{
    b.HasData(
        new Language { Id = 1, Name = "English" },
        new Language { Id = 2, Name = "French" },
        new Language { Id = 3, Name = "Spanish" });

    b.HasMany(x => x.UsedIn)
        .WithMany(x => x.OfficialLanguages)
        .UsingEntity(
            "LanguageCountry",
            r => r.HasOne(typeof(Country)).WithMany().HasForeignKey("CountryId").HasPrincipalKey(nameof(Country.CountryId)),
            l => l.HasOne(typeof(Language)).WithMany().HasForeignKey("LanguageId").HasPrincipalKey(nameof(Language.Id)),
            je =>
            {
                je.HasKey("LanguageId", "CountryId");
                je.HasData(
                    new { LanguageId = 1, CountryId = 2 },
                    new { LanguageId = 2, CountryId = 2 },
                    new { LanguageId = 3, CountryId = 3 });
            });
});

Les types d’entités possédées peuvent être configurés de manière similaire :

modelBuilder.Entity<Language>().OwnsOne(p => p.Details).HasData(
    new { LanguageId = 1, Phonetic = false, Tonal = false, PhonemesCount = 44 },
    new { LanguageId = 2, Phonetic = false, Tonal = false, PhonemesCount = 36 },
    new { LanguageId = 3, Phonetic = true, Tonal = false, PhonemesCount = 24 });

Pour plus de contexte, consultez l’exemple de projet complet.

Une fois les données ajoutées au modèle, les migrations doivent être utilisées pour appliquer les modifications.

HasDatales modifications sont converties en InsertDataUpdateDataopérations , et DeleteData quand une migration est générée. L’appel Migrate n’inspecte pas indépendamment la configuration actuelle HasData . Après avoir modifié les données gérées par un modèle, ajoutez et déployez une nouvelle migration.

Conseil

Pour le déploiement automatisé, utilisez un bundle de migration. Utilisez un script SQL lorsqu’il doit être aperçu ou modifié avant l’exécution.

Alternativement, vous pouvez utiliser EnsureCreatedAsync pour créer une nouvelle base de données contenant les données gérées, par exemple pour une base de données de test ou lors de l’utilisation du fournisseur en mémoire ou de toute base de données non relationnelle. Notez que si la base de données existe déjà, EnsureCreatedAsync ne mettra à jour ni le schéma ni les données gérées dans la base de données. Pour les bases de données relationnelles, vous ne devez pas appeler EnsureCreatedAsync si vous envisagez d’utiliser des migrations.

Remarque

Le remplissage de la base de données à l’aide de la méthode HasData était autrefois appelé « amorçage de données ». Cette dénomination induit des attentes incorrectes, car la fonctionnalité présente un certain nombre de limitations et n’est appropriée que pour des types spécifiques de données. C’est pourquoi nous avons décidé de renommer cela en « données gérées par le modèle ». Les méthodes UseSeeding et UseAsyncSeeding doivent être utilisées pour l’amorçage général des données.

Limitations des données gérées par le modèle

Ce type de données est géré par des migrations et le script pour mettre à jour les données déjà présentes dans la base de données doit être généré sans se connecter à la base de données. Cela impose certaines restrictions :

  • La valeur de clé primaire doit être spécifiée même si elle est généralement générée par la base de données. Elle sera utilisée pour détecter les modifications de données entre les migrations.
  • Les données insérées précédemment seront supprimées si la clé primaire est modifiée de quelque manière que ce soit.

Par conséquent, cette fonctionnalité est la plus utile pour les données statiques qui ne sont pas censées changer en dehors des migrations et ne dépendent de rien d’autre dans la base de données, par exemple des codes ZIP.

Si votre scénario inclut l’un des éléments suivants, il est recommandé d’utiliser les méthodes UseSeeding et UseAsyncSeeding décrites dans la première section :

  • Données temporaires pour les tests
  • Données qui dépendent de l’état de la base de données
  • Données volumineuses (les données d’amorçage sont capturées dans les instantanés de migration, et les données volumineuses peuvent rapidement entraîner des fichiers volumineux et des performances dégradées).
  • Données nécessitant des valeurs de clé générées par la base de données, y compris les entités qui utilisent des clés alternatives comme identité
  • Données nécessitant une transformation personnalisée (qui n’est pas gérée par des conversions de valeurs), telles que le hachage de mot de passe
  • Données nécessitant des appels à l’API externe, telles que les rôles d’identité ASP.NET Core et la création d’utilisateurs
  • Données qui ne sont pas fixes ni déterministes, comme l’initialisation avec DateTime.Now.

Personnalisation manuelle de la migration

Lorsqu’une migration est ajoutée, les modifications apportées aux données spécifiées avec HasData sont transformées en appels vers InsertData(), UpdateData() et DeleteData(). Une façon de contourner certaines des limitations de HasData consiste à ajouter manuellement ces appels ou opérations personnalisées à la migration à la place.

migrationBuilder.InsertData(
    table: "Countries",
    columns: new[] { "CountryId", "Name" },
    values: new object[,]
    {
        { 1, "USA" },
        { 2, "Canada" },
        { 3, "Mexico" }
    });

migrationBuilder.InsertData(
    table: "Languages",
    columns: new[] { "Id", "Name", "Details_PhonemesCount", "Details_Phonetic", "Details_Tonal" },
    values: new object[,]
    {
        { 1, "English", 44, false, false },
        { 2, "French", 36, false, false },
        { 3, "Spanish", 24, true, false }
    });

migrationBuilder.InsertData(
    table: "Cites",
    columns: new[] { "Id", "LocatedInId", "Name" },
    values: new object[,]
    {
        { 1, 1, "Seattle" },
        { 2, 2, "Vancouver" },
        { 3, 3, "Mexico City" },
        { 4, 3, "Puebla" }
    });

migrationBuilder.InsertData(
    table: "LanguageCountry",
    columns: new[] { "CountryId", "LanguageId" },
    values: new object[,]
    {
        { 2, 1 },
        { 2, 2 },
        { 3, 3 }
    });

Ces opérations sont appropriées lorsque les valeurs et les clés sont corrigées lorsque la migration est écrite. Ils n’interrogent pas l’état actuel de la base de données. Consultez les opérations de données dans les migrations pour obtenir des exemples de InsertDataUpdateDataDeleteDatatransformations SQL spécifiques au fournisseur.