Tutorial: Lectura de datos relacionados: ASP.NET MVC con EF Core
En el tutorial anterior, completó el modelo de datos School. En este tutorial podrá leer y mostrar datos relacionados, es decir, los datos que Entity Framework carga en propiedades de navegación.
En las ilustraciones siguientes se muestran las páginas con las que va a trabajar.
En este tutorial ha:
- Obtiene información sobre cómo cargar datos relacionados
- Crea una página de cursos
- Crea una página de instructores
- Obtiene información sobre la carga explícita
Requisitos previos
Obtiene información sobre cómo cargar datos relacionados
Existen varias formas para que el software de asignación relacional de objetos (ORM) como Entity Framework pueda cargar datos relacionados en las propiedades de navegación de una entidad:
Carga diligente: cuando se lee la entidad, junto a ella se recuperan datos relacionados. Esto normalmente da como resultado una única consulta de combinación en la que se recuperan todos los datos que se necesitan. En Entity Framework Core, la carga diligente se especifica mediante los métodos
Include
yThenInclude
.Puede recuperar algunos de los datos en distintas consultas y EF "corregirá" las propiedades de navegación. Es decir, EF agrega automáticamente las entidades recuperadas por separado que pertenecen a propiedades de navegación de entidades recuperadas previamente. Para la consulta que recupera los datos relacionados, puede usar el método
Load
en lugar de un método que devuelva una lista o un objeto, comoToList
oSingle
.Carga explícita: cuando la entidad se lee por primera vez, no se recuperan datos relacionados. Se escribe código que recupera los datos relacionados si son necesarios. Como en el caso de la carga diligente con consultas independientes, la carga explícita da como resultado varias consultas que se envían a la base de datos. La diferencia es que con la carga explícita el código especifica las propiedades de navegación que se van a cargar. En Entity Framework Core 1.1 se puede usar el método
Load
para realizar la carga explícita. Por ejemplo:Carga aplazada: cuando la entidad se lee por primera vez, no se recuperan datos relacionados. Pero la primera vez que intente obtener acceso a una propiedad de navegación, se recuperan automáticamente los datos necesarios para esa propiedad de navegación. Cada vez que intente obtener datos de una propiedad de navegación por primera vez, se envía una consulta a la base de datos. Entity Framework Core 1.0 no admite la carga diferida.
Consideraciones sobre el rendimiento
Si sabe que necesita datos relacionados para cada entidad que se recupere, la carga diligente suele ofrecer el mejor rendimiento, dado que una sola consulta que se envía a la base de datos normalmente es más eficaz que consultas independientes para cada entidad recuperada. Por ejemplo, suponga que cada departamento tiene diez cursos relacionados. Con la carga diligente de todos los datos relacionados se crearía una única consulta sencilla (de combinación) y un único recorrido de ida y vuelta a la base de datos. Una consulta independiente para los cursos de cada departamento crearía 11 recorridos de ida y vuelta a la base de datos. Los recorridos de ida y vuelta adicionales a la base de datos afectan especialmente de forma negativa al rendimiento cuando la latencia es alta.
Por otro lado, en algunos escenarios, las consultas independientes son más eficaces. Es posible que la carga diligente de todos los datos relacionados en una consulta genere una combinación muy compleja que SQL Server no pueda procesar eficazmente. O bien, si necesita tener acceso a las propiedades de navegación de una entidad solo para un subconjunto de un conjunto de las entidades que está procesando, es posible que las consultas independientes den mejores resultados porque la carga diligente de todo el contenido por adelantado recuperaría más datos de los que necesita. Si el rendimiento es crítico, es mejor probarlo de ambas formas para elegir la mejor opción.
Crea una página de cursos
La entidad Course
incluye una propiedad de navegación que contiene la entidad Department
del departamento al que se asigna el curso. Para mostrar el nombre del departamento asignado en una lista de cursos, tendrá que obtener la propiedad Name
de la entidad Department
que se encuentra en la propiedad de navegación Course.Department
.
Cree un controlador denominado CoursesController
para el tipo de entidad Course
, con las mismas opciones para el proveedor de scaffolding Controlador de MVC con vistas que usan Entity Framework que ha usado antes para StudentsController
, como se muestra en la ilustración siguiente:
Abra CoursesController.cs
y examine el método Index
. El scaffolding automático ha especificado la carga diligente para la propiedad de navegación Department
mediante el método Include
.
Reemplace el método Index
con el siguiente código, en el que se usa un nombre más adecuado para la IQueryable
que devuelve las entidades Course (courses
en lugar de schoolContext
):
public async Task<IActionResult> Index()
{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}
Abra Views/Courses/Index.cshtml
y reemplace el código de plantilla por el código siguiente. Se resaltan los cambios:
@model IEnumerable<ContosoUniversity.Models.Course>
@{
ViewData["Title"] = "Courses";
}
<h2>Courses</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Ha realizado los cambios siguientes en el código con scaffolding:
Ha cambiado el título de Index a Courses.
Ha agregado una columna Number en la que se muestra el valor de propiedad
CourseID
. De forma predeterminada, las claves principales no tienen scaffolding porque normalmente no tienen sentido para los usuarios finales. Pero en este caso, la clave principal es significativa y quiere mostrarla.Ha cambiado la columna Department para mostrar el nombre del departamento. El código muestra la propiedad
Name
de la entidadDepartment
que se carga en la propiedad de navegaciónDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Ejecute la aplicación y haga clic en la pestaña Courses para ver la lista con los nombres de departamento.
Crea una página de instructores
En esta sección, creará un controlador y una vista para la entidad Instructor
con el fin de mostrar la página Instructors:
En esta página se leen y muestran los datos relacionados de las maneras siguientes:
En la lista de instructores se muestran datos relacionados de la entidad
OfficeAssignment
. Las entidadesInstructor
yOfficeAssignment
se encuentran en una relación de uno a cero o uno. Usará la carga diligente para las entidadesOfficeAssignment
. Como se explicó anteriormente, la carga diligente normalmente es más eficaz cuando se necesitan los datos relacionados para todas las filas recuperadas de la tabla principal. En este caso, quiere mostrar las asignaciones de oficina para todos los instructores que se muestran.Cuando el usuario selecciona un instructor, se muestran las entidades
Course
relacionadas. Las entidadesInstructor
yCourse
se encuentran en una relación de varios a varios. Usará la carga diligente para las entidadesCourse
y sus entidadesDepartment
relacionadas. En este caso, es posible que las consultas independientes sean más eficaces porque necesita cursos solo para el instructor seleccionado. Pero en este ejemplo se muestra cómo usar la carga diligente para propiedades de navegación dentro de entidades que, a su vez, se encuentran en propiedades de navegación.Cuando el usuario selecciona un curso, se muestran los datos relacionados del conjunto de entidades
Enrollments
. Las entidadesCourse
yEnrollment
se encuentran en una relación uno a varios. Usará consultas independientes para las entidadesEnrollment
y sus entidadesStudent
relacionadas.
Crear un modelo de vista para la vista de índice de instructores
En la página Instructors se muestran datos de tres tablas diferentes. Por tanto, creará un modelo de vista que incluye tres propiedades, cada una con los datos de una de las tablas.
En la carpeta SchoolViewModels, cree InstructorIndexData.cs
y reemplace el código existente con el código siguiente:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
Crear el controlador y las vistas de Instructor
Cree un controlador Instructors con acciones de lectura y escritura de EF como se muestra en la ilustración siguiente:
Abra InstructorsController.cs
y agregue una instrucción using para el espacio de nombres ViewModels:
using ContosoUniversity.Models.SchoolViewModels;
Reemplace el método Index con el código siguiente para realizar la carga diligente de los datos relacionados y colocarlos en el modelo de vista.
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
return View(viewModel);
}
El método acepta datos de ruta opcionales (id
) y un parámetro de cadena de consulta (courseID
) que proporcionan los valores ID del instructor y el curso seleccionados. Los parámetros se proporcionan mediante los hipervínculos Select de la página.
El código comienza creando una instancia del modelo de vista y coloca en ella la lista de instructores. El código especifica la carga diligente para Instructor.OfficeAssignment
y las propiedades de navegación de Instructor.CourseAssignments
. Dentro de la propiedad CourseAssignments
se carga la propiedad Course
y dentro de esta se cargan las propiedades Enrollments
y Department
, y dentro de cada entidad Enrollment
se carga la propiedad Student
.
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
Como la vista siempre necesita la entidad OfficeAssignment
, resulta más eficaz capturarla en la misma consulta. Las entidades Course son necesarias cuando se selecciona un instructor en la página web, por lo que una sola consulta es más adecuada que varias solo si la página se muestra con más frecuencia con un curso seleccionado que sin él.
El código repite CourseAssignments
y Course
porque se necesitan dos propiedades de Course
. En la primera cadena de llamadas ThenInclude
se obtiene CourseAssignment.Course
, Course.Enrollments
y Enrollment.Student
.
Aquí puede obtener más información sobre cómo incluir varios niveles de datos relacionados.
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
En ese punto del código, otro elemento ThenInclude
sería para las propiedades de navegación de Student
, lo que no es necesario. Pero la llamada a Include
se inicia con las propiedades de Instructor
, por lo que tendrá que volver a pasar por la cadena, especificando esta vez Course.Department
en lugar de Course.Enrollments
.
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
El código siguiente se ejecuta cuando se ha seleccionado un instructor. El instructor seleccionado se recupera de la lista de instructores del modelo de vista. Después, se carga la propiedad Courses
del modelo de vista con las entidades Course
de la propiedad de navegación CourseAssignments
de ese instructor.
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
El método Where
devuelve una colección, pero en este caso los criterios que se pasan a ese método dan como resultado que solo se devuelva una entidad Instructor. El método Single
convierte la colección en una única entidad Instructor
, lo que proporciona acceso a la propiedad CourseAssignments
de esa entidad. La propiedad CourseAssignments
contiene entidades CourseAssignment
, de las que solo quiere las entidades Course
relacionadas.
El método Single
se usa en una colección cuando se sabe que la colección tendrá un único elemento. El método Single
inicia una excepción si la colección que se pasa está vacía o si hay más de un elemento. Una alternativa es SingleOrDefault
, que devuelve una valor predeterminado (NULL, en este caso) si la colección está vacía. Pero en este caso, eso seguiría iniciando una excepción (al tratar de buscar una propiedad Courses
en una referencia nula), y el mensaje de excepción indicaría con menos claridad la causa del problema. Cuando se llama al método Single
, también se puede pasar la condición Where en lugar de llamar al método Where
por separado:
.Single(i => i.ID == id.Value)
En lugar de:
.Where(i => i.ID == id.Value).Single()
A continuación, si se ha seleccionado un curso, se recupera de la lista de cursos en el modelo de vista. Después, se carga la propiedad Enrollments
del modelo de vista con las entidades Enrollment de la propiedad de navegación Enrollments
de ese curso.
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
Diferencias entre seguimiento y no seguimiento
Las consultas de no seguimiento son útiles cuando los resultados se usan en un escenario de solo lectura. Su ejecución suele ser más rápida porque no es necesario configurar la información de seguimiento de cambios. Si no es necesario actualizar las entidades recuperadas de la base de datos, es probable que una consulta de no seguimiento funcione mejor que una consulta de seguimiento.
En algunos casos, una consulta de seguimiento es más eficaz que una consulta de no seguimiento. Para obtener más información, vea Consultas de seguimiento frente a consultas de no seguimiento.
Modificar la vista de índice de instructores
En Views/Instructors/Index.cshtml
, reemplace el código de plantilla por el código siguiente. Los cambios aparecen resaltados.
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @course.Course.Title <br />
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @course.Course.Title <br />
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Ha realizado los cambios siguientes en el código existente:
Ha cambiado la clase de modelo por
InstructorIndexData
.Ha cambiado el título de la página de Index a Instructors.
Se ha agregado una columna Office en la que se muestra
item.OfficeAssignment.Location
solo siitem.OfficeAssignment
no es NULL. (Dado que se trata de una relación de uno a cero o uno, es posible que no haya una entidad OfficeAssignment relacionada).@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
Se ha agregado una columna Courses en la que se muestran los cursos que imparte cada instructor. Para obtener más información, vea la sección Transición de línea explícita del artículo sobre la sintaxis de Razor.
Se ha agregado código que agrega condicionalmente una clase CSS de Bootstrap al elemento
tr
del instructor seleccionado. Esta clase establece el color de fondo de la fila seleccionada.Se ha agregado un hipervínculo nuevo con la etiqueta Select inmediatamente antes de los otros vínculos de cada fila, lo que hace que el identificador del instructor seleccionado se envíe al método
Index
.<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Ejecute la aplicación y haga clic en la pestaña Instructors. En la página se muestra la propiedad Location de las entidades OfficeAssignment relacionadas y una celda de tabla vacía cuando no hay ninguna entidad OfficeAssignment relacionada.
En el archivo Views/Instructors/Index.cshtml
, después del elemento de tabla de cierre (situado al final del archivo), agregue el código siguiente. Este código muestra una lista de cursos relacionados con un instructor cuando se selecciona un instructor.
@if (Model.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.Courses)
{
string selectedRow = "";
if (item.CourseID == (int?)ViewData["CourseID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
Este código lee la propiedad Courses
del modelo de vista para mostrar una lista de cursos. También proporciona un hipervínculo Select que envía el identificador del curso seleccionado al método de acción Index
.
Actualice la página y seleccione un instructor. Ahora verá una cuadrícula en la que se muestran los cursos asignados al instructor seleccionado, y para cada curso, el nombre del departamento asignado.
Después del bloque de código que se acaba de agregar, agregue el código siguiente. Esto muestra una lista de los estudiantes que están inscritos en un curso cuando se selecciona ese curso.
@if (Model.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
Este código lee la propiedad Enrollments
del modelo de vista para mostrar una lista de los alumnos inscritos en el curso.
Vuelva a actualizar la página y seleccione un instructor. Después, seleccione un curso para ver la lista de los estudiantes inscritos y sus calificaciones.
Acerca de la carga explícita
Cuando se recuperó la lista de instructores en InstructorsController.cs
, se especificó la carga diligente de la propiedad de navegación CourseAssignments
.
Suponga que esperaba que los usuarios rara vez quisieran ver las inscripciones en un instructor y curso seleccionados. En ese caso, es posible que quiera cargar los datos de inscripción solo si se solicitan. Para ver un ejemplo de cómo realizar la carga explícita, reemplace el método Index
con el código siguiente, que quita la carga diligente de Enrollments
y carga esa propiedad de forma explícita. Los cambios de código aparecen resaltados.
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}
El nuevo código quita las llamadas al método ThenInclude
para los datos de inscripción del código que recupera las entidades Instructor. También quita AsNoTracking
. Si se seleccionan un instructor y un curso, el código resaltado recupera las entidades Enrollment
para el curso seleccionado y las entidades Student
de cada instancia de Enrollment
.
Ejecute la aplicación, vaya a la página de índice de instructores ahora y no verá ninguna diferencia en lo que se muestra en la página, aunque haya cambiado la forma en que se recuperan los datos.
Obtención del código
Descargue o vea la aplicación completa.
Pasos siguientes
En este tutorial ha:
- Obtenido información sobre cómo cargar datos relacionados
- Creado una página de cursos
- Creado una página de instructores
- Obtenido información sobre la carga explícita
Pase al tutorial siguiente para obtener información sobre cómo actualizar datos relacionados.