Diamo un'occhiata a semplici esempi di classe:
public class Book
{
[Key]
public string BookId { get; set; }
public List<BookPage> Pages { get; set; }
public string Text { get; set; }
}
public class BookPage
{
[Key]
public string BookPageId { get; set; }
public PageTitle PageTitle { get; set; }
public int Number { get; set; }
}
public class PageTitle
{
[Key]
public string PageTitleId { get; set; }
public string Title { get; set; }
}
Quindi, se voglio ottenere tutti i PageTitiles, se conoscessi solo il BookId, ho bisogno di scrivere alcuni include, come questo:
using (var dbContext = new BookContext())
{
var bookPages = dbContext
.Book
.Include(x => x.Pages)
.ThenInclude(x => x.PageTitle)//.ThenInclude(x => x.Select(y => y.PageTitle)) Shouldn't use in EF Core
.SingleOrDefault(x => x.BookId == "some example id")
.Pages
.Select(x => x.PageTitle);
}
E se voglio avere PageTitles connesso ad altri libri, devo riscrivere questo metodo di nuovo, e nulla è cambiato tranne il BookId! Questo è un modo molto inefficiente di lavorare con il database, in questo esempio ho 3 classi, ma se avessi centinaia di classi, annidate a livelli molto profondi, sarebbe molto lento e scomodo lavorare.
In che modo esattamente dovrei organizzare il lavoro con il mio database, per evitare molti Include e query ridondanti?
Problema 1: devo aggiungere un sacco di Includes
ogni volta.
Beh, non c'è un modo per aggirare questo dato che devi includere esplicitamente i dati correlati in EF, ma puoi facilmente creare un metodo di estensione per renderlo più pulito:
public static IQueryable<Book> GetBooksAndPages(this BookContext db)
{
return db.Book.Include(x => x.Pages);
}
public static IQueryable<Book> GetBooksAndPagesAndTitles(this BookContext db)
{
return GetBooksAndPages(db).ThenInclude(p => p.PageTitle)
}
Quindi puoi semplicemente fare:
var bookPages = dbContext
.GetBooksAndPagesAndTitles()
.SingleOrDefault(x => x.BookId == "some example id")
.Pages
.Select(x => x.PageTitle);
Problema 2: Devo scrivere questa query più volte per ID diversi.
Perché non limitarsi a bookId
in un metodo con un parametro bookId
?
public IEnumerable<PageTitle> GetPageTitlesForBook(BookContext dbContext, int bookId)
{
return dbContext
.GetBooksAndPagesAndTitles()
.SingleOrDefault(x => x.BookId == bookId)
.Pages
.Select(x => x.PageTitle);
}
In conclusione, se ti ritrovi a scrivere la stessa cosa molte volte, è un'occasione perfetta per rifattorizzare il tuo codice in metodi più piccoli che possono essere riutilizzati.
Nessuno degli esempi forniti richiede alcuna dichiarazione Includi. Se si utilizza una selezione alla fine della query e si sta ancora operando su un IQueryable come un DbSet, Entity Framework eseguirà la cosiddetta 'proiezione' e eseguirà la query includendo tutti i campi obbligatori per te automaticamente.
Ad esempio, il tuo codice originale:
using (var dbContext = new BookContext())
{
var bookPages = dbContext
.Book
.Include(x => x.Pages)
.ThenInclude(x => x.PageTitle)//.ThenInclude(x => x.Select(y => y.PageTitle)) Shouldn't use in EF Core
.SingleOrDefault(x => x.BookId == "some example id")
.Pages
.Select(x => x.PageTitle);
}
Puoi riscriverlo in questo modo:
using (var dbContext = new BookContext())
{
var bookPages = dbContext
.Book
.Where(x => x.BookId == "some example id")
.SelectMany(x => x.Pages.Select(y => y.PageTitle))
.ToList();
}
Ecco cosa farà Entity Framework per risolvere questo problema:
L'ultimo passaggio è cruciale se vuoi capire come Entity Framework fa quello che fa. Nel tuo esempio quando chiami SingleOrDefault
a Entity Framework di eseguire la query, motivo per cui hai bisogno degli include. Nel tuo esempio non hai effettivamente detto a Entity Framework che hai bisogno delle pagine quando esegui la query, quindi devi richiederle manualmente usando Include
.
Nell'esempio che ho pubblicato, è possibile vedere che nel momento in cui si esegue la query ( ToList
è ciò che attiva l'esecuzione della query) Entity Framework riconosce dall'espressione Select che avranno bisogno delle pagine e dei relativi titoli. Ancora meglio: questo significa che Entity Framework non includerà nemmeno le colonne non utilizzate nell'istruzione SELECT
che genera.
Consiglio vivamente di esaminare le proiezioni, sono probabilmente il modo migliore che conosco per rimuovere il requisito di includere continuamente le cose manualmente.