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 de propagación de datos (
UseSeeding
) - Lógica de inicialización personalizada
- Datos administrados por el modelo (
HasData
) - Personalización manual de la migración
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 }
});