c# - net - entity framework mvc 5
¿Cuál es la mejor práctica para múltiples "Incluir"-s en Entity Framework? (4)
Digamos que tenemos cuatro entidades en el modelo de datos: Categorías, Libros, Autores y BookPages. También suponga que las categorías-libros, libros-autores y libros-las relaciones de las páginas de libro son uno a muchos.
Si se recupera una instancia de entidad de categoría de la base de datos, incluidos "Libros", "Libros, páginas de libros" y "Libros, autores", esto se convertirá en un problema grave de rendimiento. Además, si no se incluyen, se producirá la excepción "La referencia del objeto no está configurada para una instancia de un objeto".
¿Cuál es la mejor práctica para usar múltiples llamadas al método Include?
- Escriba un solo método GetCategoryById e incluya todos los elementos dentro (problema de rendimiento)
- Escriba un solo método GetCategoryById y envíe una lista de relaciones para incluir (quizás, pero todavía no parece lo suficientemente elegante)
- Escribir métodos como GetCategoryByIdWithBooks, GetCategoryByIdWithBooksAndBooksPages y GetCategoryByIdWithBooksAndAuthors (no es práctico)
EDITAR : Por segunda opción quise decir algo como esto:
public static Category GetCategoryById(ModelEntities db, int categoryId, params string[] includeFields)
{
var categories = db.Categories;
foreach (string includeField in includeFields)
{
categories = categories.Include(includeField);
}
return categories.SingleOrDefault(i => i.CategoryId == categoryId);
}
Cuando llamamos necesitamos un código como este:
Category theCategory1 = CategoryHelper.GetCategoryById(db, 5, "Books");
Category theCategory2 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Pages");
Category theCategory3 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Authors");
Category theCategory4 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Pages", "Books.Authors");
¿Hay desventajas claras de este enfoque?
Escriba un solo método GetCategoryById y envíe una lista de relaciones para incluir (quizás, pero todavía no parece lo suficientemente elegante)
Escribir métodos como GetCategoryByIdWithBooks, GetCategoryByIdWithBooksAndBooksPages y GetCategoryByIdWithBooksAndAuthors (no es práctico)
Una combinación de estos dos es actualmente mi enfoque. Sé qué propiedades quiero incluir para cada contexto, así que prefiero codificarlas a mano (como usted mismo dijo, la carga lenta no siempre es una opción, y si lo es, repetirá el mismo Include()
repetitivo Include()
como la sintaxis al mapear desde modelos de datos a DTO''s).
Esta separación hace que piense más acerca de los conjuntos de datos que desea exponer, dado el código de acceso a datos, como este generalmente se oculta debajo de un servicio.
Al utilizar una clase base que contiene un método virtual, puede sobrescribir para ejecutar el Include()
s requerido:
using System.Data.Entity;
public class DataAccessBase<T>
{
// For example redirect this to a DbContext.Set<T>().
public IQueryable<T> DataSet { get; private set; }
public IQueryable<T> Include(Func<IQueryable<T>, IQueryable<T>> include = null)
{
if (include == null)
{
// If omitted, apply the default Include() method
// (will call overridden Include() when it exists)
include = Include;
}
return include(DataSet);
}
public virtual IQueryable<T> Include(IQueryable<T> entities)
{
// provide optional entities.Include(f => f.Foo) that must be included for all entities
return entities;
}
}
A continuación, puede crear instancias y utilizar esta clase tal como está, o ampliarla:
using System.Data.Entity;
public class BookAccess : DataAccessBase<Book>
{
// Overridden to specify Include()s to be run for each book
public override IQueryable<Book> Include(IQueryable<Book> entities)
{
return base.Include(entities)
.Include(e => e.Author);
}
// A separate Include()-method
private IQueryable<Book> IncludePages(IQueryable<Book> entities)
{
return entities.Include(e => e.Pages);
}
// Access this method from the outside to retrieve all pages from each book
public IEnumerable<Book> GetBooksWithPages()
{
var books = Include(IncludePages);
}
}
Ahora puede instanciar un BookAccess
y llamar a métodos en él:
var bookAccess = new BookAccess();
var allBooksWithoutNavigationProperties = bookAccess.DataSet;
var allBooksWithAuthors = bookAccess.Include();
var allBooksWithAuthorsAndPages = bookAccess.GetBooksWithPages();
En su caso, es posible que desee crear pares de métodos IncludePages
y GetBooksWithPages
separado para cada vista de su colección. O simplemente escríbalo como un método, el método IncludePages
existe para la reutilización.
Puede encadenar estos métodos todo lo que quiera, ya que cada uno de ellos (así como el método de extensión Include()
Entity Framework) devuelve otro IQueryable<T>
.
Algunas de las consideraciones de rendimiento son específicas del conector ADO.Net. Me gustaría tener en cuenta una vista de la base de datos o un procedimiento almacenado como una copia de seguridad si no está obteniendo el rendimiento necesario.
En primer lugar, tenga en cuenta que los DbContext
(y ObjectContext
) no son seguros para subprocesos.
Si le preocupa la clarividencia sobre el rendimiento, entonces la primera opción es la más simple.
Por otro lado, si le preocupa el rendimiento y está dispuesto a deshacerse del objeto de contexto después de obtener los datos, puede consultar los datos con múltiples tareas simultáneas (hilos) cada uno utilizando su propio objeto de contexto.
Si necesita un contexto para rastrear los cambios en los datos, tiene el camino directo de una sola consulta para agregar todos los elementos al contexto, o puede usar el método Adjuntar para ''reconstruir'' el estado original, y luego cambiar y salvar.
Esto último dice algo como:
using(var dbContext = new DbContext())
{
var categoryToChange = new Categories()
{
// set properties to original data
};
dbContext.Categories.Attach(categoryToChange);
// set changed properties
dbContext.SaveChanges();
}
Desafortunadamente no hay una mejor práctica para cumplir con todas las situaciones.
En el primer enfoque db, digamos que crea BookStore.edmx y agrega la entidad Category y Book y genera contexto como public partial class BookStoreContext : DbContext
entonces es una buena práctica simple si puede agregar una clase parcial como esta:
public partial class BookStoreContext
{
public IQueryable<Category> GetCategoriesWithBooks()
{
return Categories.Include(c => c.Books);
}
public IQueryable<Category> GetCategoriesWith(params string[] includeFields)
{
var categories = Categories.AsQueryable();
foreach (string includeField in includeFields)
{
categories = categories.Include(includeField);
}
return categories;
}
// Just another example
public IQueryable<Category> GetBooksWithAllDetails()
{
return Books
.Include(c => c.Books.Authors)
.Include(c => c.Books.Pages);
}
// yet another complex example
public IQueryable<Category> GetNewBooks(/*...*/)
{
// probably you can pass sort by, tags filter etc in the parameter.
}
}
Entonces puedes usarlo así:
var category1 = db.CategoriesWithBooks()
.Where(c => c.Id = 5).SingleOrDefault();
var category2 = db.CategoriesWith("Books.Pages", "Books.Authors")
.Where(c => c.Id = 5).SingleOrDefault(); // custom include
Nota:
- Puede leer algunos de los simples (muchos complicados por ahí) del modelo de repositorio para extender
IDbSet<Category> Categories
al grupo comúnInclude
yWhere
lugar de usarIDbSet<Category> Categories
estático. Entonces puede tenerIQueryable<Category> db.Categories.WithBooks()
- No debe incluir todas las entidades secundarias en
GetCategoryById
porque no se explica por sí mismo en el nombre del método y eso causará problemas de rendimiento si el usuario de este método no es el hermano de Entites. - Aunque no incluya todos, si usa la carga diferida, aún puede tener un posible problema de rendimiento N + 1
- Si tiene 1000 de
Books
mejor, coloque en la página su carga algo así comodb.Books.Where(b => b.CategoryId = categoryId).Skip(skip).Take(take).ToList()
o incluso mejor, agregue el método arriba para ser asídb.GetBooksByCategoryId(categoryId, skip, take)
Yo mismo prefiero cargar entidades explícitamente ya que ''sabré'' qué está cargado actualmente pero la carga lenta solo es útil si tiene entidades de carga condicional cargadas y debería usarse dentro de un pequeño ámbito de contexto de db, de lo contrario no puedo controlar el hit de db y qué tan grande es el resultado
Como @Colin mencionó en los comentarios, debe usar la palabra clave virtual al definir las propiedades de navegación para que funcionen con carga diferida. Suponiendo que está utilizando Code-First, su clase Book debería verse más o menos así:
public class Book
{
public int BookID { get; set; }
//Whatever other information about the Book...
public virtual Category Category { get; set; }
public virtual List<Author> Authors { get; set; }
public virtual List<BookPage> BookPages { get; set; }
}
Si no se utiliza la palabra clave virtual, la clase de proxy creada por EF no podrá cargar de forma perezosa las entidades / entidades relacionadas.
Por supuesto, si está creando un nuevo libro, no podrá realizar la carga diferida y lanzará NullReferenceException si intenta iterar sobre las BookPages. Es por eso que deberías hacer una de estas dos cosas:
- define un constructor
Book()
que incluyeBookPages = new List<BookPage>();
(lo mismo para losAuthors
) o - asegúrese de que la ÚNICA vez que tenga "
new Book()
" en su código sea cuando esté creando una nueva entrada que está guardando inmediatamente en la base de datos y luego descartando sin intentar obtener nada de ella.
Personalmente prefiero la segunda opción, pero sé que muchos otros prefieren la primera.
<EDIT>
Encontré una tercera opción, que es usar el método Create
de la clase DbSet<>
. Esto significa que llamaría a myContext.Books.Create()
lugar de new Book()
. Consulte este Q + A para obtener más información: Ramificaciones de DbSet.Crear frente a una nueva entidad () </EDIT>
Ahora, la otra forma en que se puede romper la carga lenta es cuando se apaga. (Supongo que ModelEntities
es el nombre de su clase DbContext
.) Para desactivarlo, debe establecer ModelEntities.Configuration.LazyLoadingEnabled = false;
Bastante auto explicativo, ¿no?
La conclusión es que no debería necesitar usar Include()
todas partes. Realmente pretende ser más un medio de optimización que un requisito para que funcione tu código. Usar Include()
excesivamente da como resultado un rendimiento muy bajo porque termina obteniendo mucho más de lo que realmente necesita de la base de datos, porque Include()
siempre incluirá todos los registros relacionados. Digamos que estás cargando una Categoría y hay 1000 Libros que pertenecen a esa Categoría. No se puede filtrar para incluir solo buscar los libros escritos por John Smith cuando se usa la función Include()
. Sin embargo, puedes (con la carga lenta habilitada) solo hacer lo siguiente:
Category cat = ModelEntities.Categorys.Find(1);
var books = cat.Books.Where(b => b.Authors.Any(a => a.Name == "John Smith"));
Esto realmente dará como resultado que se devuelvan menos registros de la base de datos y es mucho más simple de entender.
¡Espero que ayude! ;)