Compartir a través de


Propagación de datos

La siembra de datos es el proceso de llenar una base de datos con un conjunto inicial de datos.

Hay varias maneras de hacerlo en EF Core:

Opciones de configuración UseSeeding y métodos UseAsyncSeeding

EF 9 introdujo UseSeeding y métodos UseAsyncSeeding, que proporcionan una manera conveniente de propagar la base de datos con datos iniciales. El objetivo de estos métodos es mejorar la experiencia de uso de la lógica de inicialización personalizada (que se explica más adelante). Proporcionan una ubicación clara donde se puede colocar todo el código de propagación de datos. Además, el código dentro de estos métodos UseSeeding y UseAsyncSeeding está protegido por el mecanismo de bloqueo de migración para evitar problemas de simultaneidad.

Los nuevos métodos de propagación son llamados como parte de la operación de EnsureCreated, comando Migrate y dotnet ef database update, incluso si no hay cambios en el modelo y no se aplicaron migraciones.

Sugerencia

El uso de UseSeeding y UseAsyncSeeding es la forma recomendada de propagar la base de datos con datos iniciales cuando se trabaja con EF Core.

Estos métodos se pueden configurar en el paso de configuración de opciones. Este es un ejemplo:

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);
            }
        });

Nota:

UseSeeding se llama desde el método EnsureCreated y UseAsyncSeeding se llama desde el método EnsureCreatedAsync. Cuando se utiliza esta función, se recomienda implementar ambos métodos UseSeeding y UseAsyncSeeding utilizando una lógica similar, incluso si el código que utiliza EF es asíncrono. Actualmente, las herramientas de EF Core se basan en la versión sincrónica del método y no propagarán la base de datos correctamente si no se implementa el método UseSeeding.

Lógica de inicialización personalizada

Una manera sencilla y eficaz de realizar la propagación de datos es usar DbContext.SaveChanges() antes de que la lógica de aplicación principal comience la ejecución. Se recomienda utilizar UseSeeding y UseAsyncSeeding para ese propósito, sin embargo a veces utilizar estos métodos no es una buena solución. Un escenario de ejemplo es cuando la propagación requiere el uso de dos contextos diferentes en una transacción. A continuación se muestra un ejemplo de código que realiza la inicialización personalizada en la aplicación directamente:

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();
    }
}

Advertencia

El código de propagación no debe formar parte de la ejecución normal de la aplicación, ya que esto puede provocar problemas de simultaneidad cuando se ejecutan varias instancias y también requeriría que la aplicación tenga permiso para modificar el esquema de la base de datos.

En función de las restricciones de la implementación, el código de inicialización se puede ejecutar de maneras diferentes:

  • Ejecución de la aplicación de inicialización localmente
  • Implementar la aplicación de inicialización con la aplicación principal, invocando la rutina de inicialización y deshabilitando o quitando la aplicación de inicialización.

Normalmente, esto se puede automatizar mediante perfiles de publicación.

Datos administrados por el modelo

Los datos también pueden asociarse a un tipo de entidad como parte de la configuración del modelo. Las migraciones de EF Core pueden luego calcular automáticamente las operaciones de inserción, actualización y eliminación que hay que aplicar al actualizar la base de datos a una nueva versión del modelo.

Advertencia

Las migraciones solo tienen en cuenta los cambios del modelo a la hora de determinar qué operación debe realizarse para que los datos administrados pasen al estado deseado. Por lo tanto, los cambios realizados fuera de las migraciones pueden perderse o provocar un error.

Como ejemplo, esto configurará los datos administrados para un Country en 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" });
});

Para agregar entidades que tengan una relación, se deben especificar los valores de clave externa:

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 });

Cuando se administran datos para navegaciones muchos-a-muchos, la entidad de combinación necesita ser configurada explícitamente. Si el tipo de entidad tiene alguna propiedad en estado sombra (por ejemplo, la entidad de combinación LanguageCountry de abajo), se puede utilizar una clase anónima para proporcionar los valores:

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 });
            });
});

Los tipos de entidad propios pueden configurarse de forma similar:

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 });

Vea el proyecto de ejemplo completo para obtener más contexto.

Una vez agregados los datos al modelo, se deben usar migraciones para aplicar los cambios.

Sugerencia

Si necesita aplicar migraciones como parte de una implementación automatizada, puede crear un script SQL que se pueda obtener una vista previa antes de la ejecución.

Alternativamente, se puede utilizar context.Database.EnsureCreated() para crear una nueva base de datos que contenga los datos administrados, por ejemplo para una base de datos de prueba o cuando se utiliza el proveedor en memoria o cualquier base de datos no relacional. Tenga en cuenta que si la base de datos ya existe, EnsureCreated() no se actualizará ni el esquema ni los datos administrados en la base de datos. En el caso de las bases de datos relacionales, no debe llamar EnsureCreated() si planea usar migraciones.

Nota:

Rellenar la base de datos utilizando el método HasData solía denominarse "propagación de datos". Esta denominación crea expectativas erróneas, ya que la función tiene una serie de limitaciones y solo es apropiada para determinados tipos de datos. Por eso hemos decidido cambiarle el nombre por el de "datos administrados por modelos". Los métodos UseSeeding y UseAsyncSeeding deben usarse para la propagación de datos de uso general.

Limitaciones de los datos administrados por modelos

Este tipo de datos se administra mediante migraciones y el script para actualizar los datos que ya están en la base de datos debe generarse sin conectarse a la base de datos. Esto impone algunas restricciones:

  • El valor de clave principal debe especificarse incluso si normalmente lo genera la base de datos. Se usará para detectar cambios de datos entre migraciones.
  • Los datos insertados previamente se eliminarán si la clave primaria se modifica de algún modo.

Por lo tanto, esta característica es más útil para los datos estáticos que no se espera que cambien fuera de las migraciones y no dependan de nada más en la base de datos, por ejemplo, códigos postales.

Si su escenario incluye alguno de los siguientes casos, se recomienda utilizar los métodos UseSeeding y UseAsyncSeeding descritos en la primera sección:

  • Datos temporales para pruebas
  • Datos que dependen del estado de la base de datos
  • Los datos que son grandes (los datos de inicialización se capturan en instantáneas de migración y los datos de gran tamaño pueden provocar rápidamente archivos enormes y un rendimiento degradado).
  • Datos que necesitan que la base de datos genere valores clave, incluidas las entidades que usan claves alternativas como identidad
  • Datos que requieren transformación personalizada (que no se manipula mediante conversiones de valor), como algunos hash de contraseñas
  • Datos que requieren llamadas a una API externa, como ASP.NET roles de Core Identity y la creación de usuarios
  • Datos que no son fijos y deterministas, como la propagación a DateTime.Now.

Personalización manual de la migración

Cuando se agrega una migración, los cambios en los datos especificados con HasData se transforman en llamadas a InsertData(), UpdateData(), y DeleteData(). Una manera de solucionar algunas de las limitaciones de HasData es agregar manualmente estas llamadas o operaciones personalizadas a la migración en su lugar.

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 }
    });