Partager via


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

La méthode UseSeeding est appelée à partir de la méthode EnsureCreated, et la méthode UseAsyncSeeding est appelée à partir de la méthode EnsureCreatedAsync. Lorsque cette fonctionnalité est utilisée, il est recommandé de mettre en œuvre les méthodes UseSeeding et UseAsyncSeeding avec une logique similaire, même si le code utilisant EF est asynchrone. Les outils EF Core reposent actuellement sur la version synchrone de la méthode et n’amorceront pas correctement la base de données si la méthode UseSeeding n’est pas implémentée.

Logique d’initialisation personnalisée

Un moyen simple et puissant d’effectuer l’amorçage des données consiste à utiliser DbContext.SaveChanges() 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())
{
    context.Database.EnsureCreated();

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

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. Ensuite, les migrations EF Core peuvent automatiquement calculer quelles opérations d’insertion, de mise à jour ou de suppression doivent être appliquées lors de la mise à jour 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.

Conseil

Si vous devez appliquer des migrations dans le cadre d’un déploiement automatisé, vous pouvez créer un script SQL qui peut être prévisualisé avant l’exécution.

Alternativement, vous pouvez utiliser context.Database.EnsureCreated() 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à, EnsureCreated() 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 EnsureCreated() 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 à une API externe, telles que les rôles d’identité et la création d’utilisateurs ASP.NET Core
  • Données qui ne sont pas fixes et déterministes, telles que l’amorçage à 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 }
    });