Seeding di database molti-a-molti in EFCore5 con ModelBuilder?
Esistono molte domande sul seeding delle relazioni molti-a-molti in Entity Framework. Tuttavia, la maggior parte di essi è estremamente vecchia e il comportamento molti-a-molti è cambiato in modo significativo in EFCore5 . I documenti ufficiali consigliano di ignorare OnModelCreatingl'implementazione ModelBuilder.Entity<>.HasData().
Tuttavia, con il nuovo comportamento molti-a-molti (senza mappature esplicite), non riesco a trovare un percorso chiaro per eseguire il seeding delle tabelle intermedie. Per utilizzare l'esempio di questo tutorial , la BookCategoriesclasse è ora implicita. Pertanto, non esiste alcun percorso per dichiarare esplicitamente i valori della tabella intermedia durante il seeding.
Ho anche provato semplicemente ad assegnare gli array, ad esempio:
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public ICollection<Category> Categories { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public ICollection<Book> Books { get; set; }
}
E poi al momento del seme:
Book book = new Book() { BookId = 1, Title = "Brave New World" }
Category category = new Category() { CategoryId = 1, CategoryName = "Dystopian" }
category.Books = new List<Book>() { book };
book.Categories = new List<Category>() { category };
modelBuilder.Entity<Book>().HasData(book);
modelBuilder.Entity<Category>().HasData(category);
... ma non ci sono voci create per BookCategoriesnella migrazione risultante. Questo era in qualche modo previsto, poiché questo articolo suggerisce che è necessario eseguire il seeding esplicito della tabella intermedia. Quello che voglio è qualcosa del genere:
modelBuilder.Entity<BookCategory>().HasData(
new BookCategory() { BookId = 1, CategoryId = 1 }
);
Tuttavia, ancora una volta, poiché non esiste una classe concreta da descrivere BookCategoriesin EFCore5, l'unico modo in cui posso pensare di eseguire il seeding della tabella è modificare manualmente la migrazione con MigrationBuilder.InsertDatacomandi aggiuntivi , il che vanifica piuttosto lo scopo del seeding dei dati tramite il codice dell'applicazione.
Risposte
Tuttavia, ancora una volta, poiché non esiste una classe concreta da descrivere
BookCategoriesin EFCore5
In realtà, come spiegato nel collegamento Novità , EF Core 5 consente di avere un'entità di join esplicita
public class BookCategory
{
public int BookId { get; set; }
public EBook Book { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
e configurare la relazione molti a molti per utilizzarla
modelBuilder.Entity<Book>()
.HasMany(left => left.Categories)
.WithMany(right => right.Books)
.UsingEntity<BookCategory>(
right => right.HasOne(e => e.Category).WithMany(),
left => left.HasOne(e => e.Book).WithMany().HasForeignKey(e => e.BookId),
join => join.ToTable("BookCategories")
);
In questo modo puoi utilizzare tutte le normali operazioni delle entità (query, rilevamento delle modifiche, seeding del modello di dati, ecc.)
modelBuilder.Entity<BookCategory>().HasData(
new BookCategory() { BookId = 1, CategoryId = 1 }
);
ancora con la nuova mappatura molti-a-molti salta navigazioni .
Questo è probabilmente l'approccio più semplice e indipendente dai tipi.
Nel caso in cui sia troppo, è anche possibile utilizzare l'entità join convenzionale, ma è necessario conoscere il nome del tipo di entità del dizionario condiviso , nonché i due nomi delle proprietà shadow . Che come vedrai per convenzione potrebbe non essere quello che ti aspetti.
Quindi, per convenzione, il nome dell'entità di join (e della tabella) è
{LeftEntityName}{RightEntityName}
e i nomi delle proprietà shadow (e delle colonne) sono
{LeftEntityNavigationPropertyName}{RightEntityKeyName}{RightEntityNavigationPropertyName}{LeftEntityKeyName}
La prima domanda sarebbe: qual è l'entità sinistra / destra? La risposta è (non ancora documentata): per convenzione l'entità a sinistra è quella il cui nome è meno in ordine alfabetico. Quindi con il tuo esempio Bookè a sinistra, Categoryè a destra, quindi l'entità di join e il nome della tabella sarebbero BookCategory.
Può essere modificato aggiungendo esplicito
modelBuilder.Entity<Category>()
.HasMany(left => left.Books)
.WithMany(right => right.Categories);
e ora lo sarebbe CategoryBook.
In entrambi i casi i nomi delle proprietà shadow (e delle colonne) sarebbero
CategoriesCategoryIdBooksBookId
Quindi né il nome della tabella, né i nomi di proprietà / colonna sono ciò che faresti normalmente.
E a parte i nomi di tabella / colonna del database, i nomi di entità e proprietà sono importanti perché ne avresti bisogno per le operazioni di entità, incluso il seeding dei dati in questione.
Detto questo, anche se non crei un'entità join esplicita, è meglio configurare fluentemente quella creata automaticamente dalla convenzione EF Core:
modelBuilder.Entity<Book>()
.HasMany(left => left.Categories)
.WithMany(right => right.Books)
.UsingEntity("BookCategory", typeof(Dictionary<string, object>),
right => right.HasOne(typeof(Category)).WithMany().HasForeignKey("CategoryId"),
left => left.HasOne(typeof(Book)).WithMany().HasForeignKey("BookId"),
join => join.ToTable("BookCategories")
);
Ora puoi utilizzare il nome dell'entità per accedere al file EntityTypeBuilder
modelBuilder.Entity("BookCategories")
e puoi iniziarlo in modo simile a entità normali con proprietà FK ombra con tipo anonimo
modelBuilder.Entity("BookCategory").HasData(
new { BookId = 1, CategoryId = 1 }
);
o per questa specifica entità di tipo bag di proprietà, anche con Dictionary<string, object>istanze
modelBuilder.Entity("BookCategory").HasData(
new Dictionary<string, object> { ["BookId"] = 1, ["CategoryId"] = 1 }
);
Aggiornare:
Le persone sembrano interpretare male i suddetti passaggi "extra" e li trovano ridondanti e "troppo", non necessari.
Non ho mai detto che siano obbligatorie. Se conosci l'entità join convenzionale ei nomi delle proprietà, vai direttamente all'ultimo passaggio e utilizza il tipo anonimo o Dictionary<string, object>.
Ho già spiegato gli svantaggi di prendere quella strada - perdere la sicurezza dei tipi C # e usare stringhe "magiche" fuori dal tuo controllo. Bisogna essere abbastanza intelligente per sapere l'esatto EF Nucleo convenzioni di denominazione e rendersi conto che se si rinomina di classe Bookper EBookil nuovo unirsi nome dell'entità / tavolo cambierà da "BookCategory" a "CategoryEBook", così come l'ordine delle proprietà PK / colonne, indici associati ecc.
Per quanto riguarda il problema concreto con il seeding dei dati. Se vuoi davvero generalizzarlo (tentativo OP nella propria risposta), fallo almeno correttamente usando il sistema di metadati EF Core piuttosto che la riflessione e le ipotesi. Ad esempio, quanto segue estrarrà questi nomi dai metadati di EF Core:
public static void HasJoinData<TFirst, TSecond>(
this ModelBuilder modelBuilder,
params (TFirst First, TSecond Second)[] data)
where TFirst : class where TSecond : class
=> modelBuilder.HasJoinData(data.AsEnumerable());
public static void HasJoinData<TFirst, TSecond>(
this ModelBuilder modelBuilder,
IEnumerable<(TFirst First, TSecond Second)> data)
where TFirst : class where TSecond : class
{
var firstEntityType = modelBuilder.Model.FindEntityType(typeof(TFirst));
var secondEntityType = modelBuilder.Model.FindEntityType(typeof(TSecond));
var firstToSecond = firstEntityType.GetSkipNavigations()
.Single(n => n.TargetEntityType == secondEntityType);
var joinEntityType = firstToSecond.JoinEntityType;
var firstProperty = firstToSecond.ForeignKey.Properties.Single();
var secondProperty = firstToSecond.Inverse.ForeignKey.Properties.Single();
var firstValueGetter = firstToSecond.ForeignKey.PrincipalKey.Properties.Single().GetGetter();
var secondValueGetter = firstToSecond.Inverse.ForeignKey.PrincipalKey.Properties.Single().GetGetter();
var seedData = data.Select(e => (object)new Dictionary<string, object>
{
[firstProperty.Name] = firstValueGetter.GetClrValue(e.First),
[secondProperty.Name] = secondValueGetter.GetClrValue(e.Second),
});
modelBuilder.Entity(joinEntityType.Name).HasData(seedData);
}
Anche qui non è necessario sapere quale tipo è "left" e quale è "right", né richiede una classe base o un'interfaccia speciali. Basta passare la sequenza di coppie di entità e verrà correttamente inizializzato l'entità join convenzionale, ad esempio con l'esempio OP, entrambi
modelBuilder.HasJoinData((book, category));
e
modelBuilder.HasJoinData((category, book));
farebbe.
Ho finito per trovare una soluzione generica a questo problema basata sulla risposta di Ivan (grazie!). Ora sono in grado di eseguire il seeding di tutte le mie tabelle M2M con questa sintassi:
// Add book1 and book2 to category1:
modelBuilder.HasM2MData(new [] { book1, book2 }, new [] { category1 });
Potrebbe non essere del tutto affidabile, ma dovrebbe funzionare con le mappature M2M convenzionali.
Fa alcune ipotesi:
T1&T2Eredita da alcuniModelBaseche forniscono unaIdproprietà.T1&T2Avere esattamente unaICollection<OtherType>proprietà.- Conosci l'ordine corretto (quale modello è
T1e quale èT2): questo può essere scoperto eseguendo prima la migrazione per le tabelle e ispezionando la migrazione. - Stai eseguendo EFCore5 RC2 o successivo (vedi questo problema ).
public static void HasM2MData<T1, T2>
(this ModelBuilder mb, T1[] t1s, T2[] t2s)
where T1 : ModelBase where T2 : ModelBase
{
string table = $"{typeof(T1).Name}{typeof(T2).Name}"; PropertyInfo t1Prop = GetM2MProperty<T1, T2>(); PropertyInfo t2Prop = GetM2MProperty<T2, T1>(); string t1Key = $"{t1Prop.Name}Id";
string t2Key = $"{t2Prop.Name}Id"; foreach (T1 t1 in t1s) { foreach (T2 t2 in t2s) { mb.Entity(table).HasData(new Dictionary<string, object>() { [t2Key] = t1.Id, [t1Key] = t2.Id }); } } } // Get a property on T1 which is assignable to type ICollection<T2>, representing the m2m relationship private static PropertyInfo GetM2MProperty<T1, T2>() { Type assignableType = typeof(ICollection<T2>); List<PropertyInfo> props = typeof(T1).GetProperties() .Where(pi => pi.PropertyType.IsAssignableTo(assignableType)) .ToList(); if (props.Count() != 1) { throw new SystemException( $"Expected {typeof(T1)} to have exactly one column of type {assignableType}; got: {props.Count()}");
}
return props.First();
}
Nella migrazione, vediamo qualcosa di simile:
migrationBuilder.InsertData(
table: "BookCategory",
columns: new[] { "BooksId", "CategoriesId" },
values: new object[,]
{
{ "book1", "category1" },
{ "book2", "category1" }
});