Compartir vía


Tutorial: Leer datos relacionados con EF en una aplicación ASP.NET MVC

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.

Screenshot that shows the Courses page with a list of courses.

Instructors_index_page_with_instructor_and_course_selected

Descargar el proyecto completado

En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones de ASP.NET MVC 5 con Code First de Entity Framework 6 y Visual Studio 2012. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial de la serie.

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

Requisitos previos

Hay varias formas de que Entity Framework cargue datos relacionados en las propiedades de navegación de una entidad:

  • Carga diferida. 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. Esto da como resultado varias consultas enviadas a la base de datos: una para la propia entidad y otra cada vez que se deben recuperar los datos relacionados de la entidad. La DbContext clase habilita la carga diferida de forma predeterminada.

    Lazy_loading_example

  • 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. Especifique la carga diligente mediante el método Include.

    Eager_loading_example

  • Carga explícita. Esto es similar a la carga diferida, salvo que recupera explícitamente los datos relacionados en el código; no se produce automáticamente cuando se accede a una propiedad de navegación. Los datos relacionados se cargan manualmente obteniendo la entrada del administrador de estado de objetos para una entidad y llamando al método Collection.Load para colecciones o al método Reference.Load para las propiedades que contienen una sola entidad. (En el ejemplo siguiente, si desea cargar la propiedad de navegación Administrador, reemplazaría Collection(x => x.Courses) por Reference(x => x.Administrator)). Normalmente, usaría la carga explícita solo cuando haya desactivado la carga diferida.

    Explicit_loading_example

Dado que no recuperan inmediatamente los valores de propiedad, la carga diferida y la carga explícita también se conocen como cargadiferida.

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, en los ejemplos anteriores, supongamos que cada departamento tiene diez cursos relacionados. El ejemplo de carga ansiosa daría lugar a una única consulta (de unión) y un único viaje de ida y vuelta a la base de datos. Los ejemplos de carga diferida y carga explícita generarían once consultas y once 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 la carga diferida es más eficaz. La carga diligente puede hacer que se genere una combinación muy compleja, que SQL Server no puede procesar de forma eficaz. 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.

La carga diferida puede enmascarar el código que provoca problemas de rendimiento. Por ejemplo, el código que no especifica la carga diligente o explícita, pero procesa un gran volumen de entidades y usa varias propiedades de navegación en cada iteración podría ser muy ineficaz (debido a muchos recorridos de ida y vuelta a la base de datos). Una aplicación que funciona bien en el desarrollo mediante un servidor SQL Server local podría tener problemas de rendimiento al moverse a Azure SQL Database debido al aumento de la latencia y la carga diferida. La generación de perfiles de las consultas de base de datos con una carga de prueba realista le ayudará a determinar si la carga diferida es adecuada. Para obtener más información, consulte Demystifying Entity Framework Strategies: Loading Related Data and Using the Entity Framework to Reduce Network Latency to SQL Azure (Estrategias de Entity Framework: carga de datos relacionados y uso de Entity Framework para reducir la latencia de red a SQL Azure).

Deshabilitar la carga diferida antes de la serialización

Si deja habilitada la carga diferida durante la serialización, puede terminar consultando significativamente más datos de los previstos. La serialización generalmente funciona accediendo a cada propiedad en una instancia de un tipo. El acceso a propiedades desencadena la carga diferida y esas entidades cargadas diferidas se serializan. A continuación, el proceso de serialización accede a cada propiedad de las entidades cargadas de forma diferida, lo que puede provocar una carga y serialización aún más diferida. Para evitar esta reacción en cadena de desencadenamiento, desactive la carga diferida antes de serializar una entidad.

La serialización también puede ser complicada por las clases de proxy que usa Entity Framework, como se explica en el tutorialEscenarios avanzados.

Una manera de evitar problemas de serialización es serializar objetos de transferencia de datos (DTO) en lugar de objetos de entidad, como se muestra en el tutorial Uso de API web con Entity Framework.

Si no usa DTO, puede deshabilitar la carga diferida y evitar problemas de proxy deshabilitando la creación de proxy.

Estas son algunas otras maneras de deshabilitar la cargadiferida:

  • Para propiedades de navegación específicas, omita la palabra clave virtual al declarar la propiedad.

  • Para todas las propiedades de navegación, establezca LazyLoadingEnabled en false, coloque el código siguiente en el constructor de la clase de contexto:

    this.Configuration.LazyLoadingEnabled = false;
    

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 llamado CourseController (no CoursesController) para el tipo de entidad Course, utilizando las mismas opciones para el controlador MVC 5 con vistas, utilizando el andamiaje de Entity Framework que realizó antes para el controlador Student:

Configuración Valor
Clase de modelo Seleccione Curso (ContosoUniversity.Models).
Clase de contexto de datos Seleccione SchoolContext (ContosoUniversity.DAL).
Nombre del controlador Escriba CourseController. De nuevo, no CoursesController con un s. Cuando seleccionó Course (ContosoUniversity.Models),el valor nombre del controlador se rellena automáticamente. Tiene que cambiar el valor.

Deje los demás valores predeterminados y agregue el controlador.

Abra Controllers\CourseController.cs y examine el Index método :

public ActionResult Index()
{
    var courses = db.Courses.Include(c => c.Department);
    return View(courses.ToList());
}

El scaffolding automático ha especificado la carga diligente para la propiedad de navegación Department mediante el método Include.

Abra Views/Courses/Index.cshtml y reemplace el código de plantilla con el código siguiente. Se resaltan los cambios:

@model IEnumerable<ContosoUniversity.Models.Course>

@{
    ViewBag.Title = "Courses";
}

<h2>Courses</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.CourseID)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Credits)
        </th>
        <th>
            Department
        </th>
        <th></th>
    </tr>

@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>
            @Html.ActionLink("Edit", "Edit", new { id=item.CourseID }) |
            @Html.ActionLink("Details", "Details", new { id=item.CourseID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.CourseID })
        </td>
    </tr>
}

</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.
  • Movió la columna Departamento al lado derecho y cambió su encabezado. El scaffolder eligió correctamente mostrar la propiedad Name de la entidad Department, pero aquí en la página Curso, el encabezado de columna debe ser Departamento en lugar de Nombre.

Observe que para la columna Departamento, el código con scaffolding muestra la propiedad Name de la entidad Department que se carga en la propiedad de Department navegación:

<td>
    @Html.DisplayFor(modelItem => item.Department.Name)
</td>

Ejecute la página (seleccione la pestaña Cursos en la página principal de Contoso University) para ver la lista con 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 entidades Instructor y OfficeAssignment se encuentran en una relación de uno a cero o uno. Usará la carga diligente para las entidades OfficeAssignment. 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 entidades Instructor y Course se encuentran en una relación de varios a varios. Usará la carga diligente para las entidades Course y sus entidades Department 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 entidades Course y Enrollment se encuentran en una relación uno a varios. Agregará carga explícita para Enrollment las entidades y sus entidades relacionadas Student. (La carga explícita no es necesaria porque la carga diferida está habilitada, pero esto muestra cómo realizar la carga explícita).

Crear un modelo de vista para la vista de índice de instructores

La página de Instructores muestra 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 ViewModels, cree InstructorIndexData.cs y sustituya el código existente por el siguiente código:

using System.Collections.Generic;
using ContosoUniversity.Models;

namespace ContosoUniversity.ViewModels
{
    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 InstructorController controlador (no InstructorsController) con la acción de lectura y escritura de EF:

Configuración Valor
Clase de modelo Seleccione Instructor (ContosoUniversity.Models).
Clase de contexto de datos Seleccione SchoolContext (ContosoUniversity.DAL).
Nombre del controlador Escriba InstructorController. De nuevo, no InstructorsController con un s. Cuando seleccionó Course (ContosoUniversity.Models),el valor nombre del controlador se rellena automáticamente. Tiene que cambiar el valor.

Deje los demás valores predeterminados y agregue el controlador.

Abra Controllers\InstructorController.cs y agregue una using instrucción para el ViewModels espacio de nombres:

using ContosoUniversity.ViewModels;

El código con scaffolding en el Index método especifica la carga diligente solo para la propiedad de OfficeAssignment navegación:

public ActionResult Index()
{
    var instructors = db.Instructors.Include(i => i.OfficeAssignment);
    return View(instructors.ToList());
}

Reemplace el Index método por el código siguiente para cargar datos relacionados adicionales y colocarlos en el modelo de vista:

public ActionResult Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses.Select(c => c.Department))
        .OrderBy(i => i.LastName);

    if (id != null)
    {
        ViewBag.InstructorID = id.Value;
        viewModel.Courses = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single().Courses;
    }

    if (courseID != null)
    {
        ViewBag.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 de identificador del instructor seleccionado y el curso seleccionado, y pasa todos los datos necesarios a la vista. 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.Courses.

var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
    .Include(i => i.OfficeAssignment)
    .Include(i => i.Courses.Select(c => c.Department))
     .OrderBy(i => i.LastName);

El segundo método Include carga Courses y, para cada Curso que se carga, realiza una carga diligente para la propiedad de navegación Course.Department.

.Include(i => i.Courses.Select(c => c.Department))

Como se mencionó anteriormente, la carga diligente no es necesaria, pero se realiza para mejorar el rendimiento. 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 la carga ansiosa es mejor que la carga perezosa sólo si la página se muestra más a menudo con un curso seleccionado que sin él.

Si se seleccionó un identificador de 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 Courses de ese instructor.

if (id != null)
{
    ViewBag.InstructorID = id.Value;
    viewModel.Courses = viewModel.Instructors.Where(i => i.ID == id.Value).Single().Courses;
}

El método Where devuelve una colección, pero en este caso los criterios pasados a ese método hacen que sólo se devuelva una única entidad Instructor. El método Single convierte la colección en una única entidad Instructor, lo que proporciona acceso a la propiedad Courses de esa entidad.

Se utiliza el método Único en una colección cuando se sabe que la colección sólo tendrá un 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 null), 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)
{
    ViewBag.CourseID = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}

Modificar la vista de índice de instructores

En Views/Instructors/Index.cshtml, reemplace el código de plantilla con el código siguiente. Se resaltan los cambios:

@model ContosoUniversity.ViewModels.InstructorIndexData

@{
    ViewBag.Title = "Instructors";
}

<h2>Instructors</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>Last Name</th>
        <th>First Name</th>
        <th>Hire Date</th>
        <th>Office</th>
        <th></th>
    </tr>

    @foreach (var item in Model.Instructors)
    {
        string selectedRow = "";
        if (item.ID == ViewBag.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>
                @Html.ActionLink("Select", "Index", new { id = item.ID }) |
                @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
                @Html.ActionLink("Details", "Details", new { id = item.ID }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.ID })
            </td>
        </tr>
    }

    </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 si item.OfficeAssignment no es NULL. (Dado que se trata de una relación de uno a cero o de uno a uno, es posible que no exista una entidad OfficeAssignment relacionada.)

    <td> 
        @if (item.OfficeAssignment != null) 
        { 
            @item.OfficeAssignment.Location  
        } 
    </td>
    
  • Ha agregado código que agrega dinámicamente class="success" al elemento tr del instructor seleccionado. Esto establece el color de fondo de la fila seleccionada mediante una clase de arranque.

    string selectedRow = ""; 
    if (item.InstructorID == ViewBag.InstructorID) 
    { 
        selectedRow = "success"; 
    } 
    <tr class="@selectedRow" valign="top">
    
  • Se ha agregado un ActionLink 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.

Ejecute la aplicación y seleccione la pestaña Instructors. La página muestra la Location propiedad de las entidades relacionadas OfficeAssignment y una celda de tabla vacía cuando no hay ninguna entidad relacionada OfficeAssignment.

En el archivo Views/Instructors/Index.cshtml, después del elemento de tabla de cierre table (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 == ViewBag.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 ID 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.

Actualice la página y seleccione un instructor. Después, seleccione un curso para ver la lista de los estudiantes inscritos y sus calificaciones.

Agregar carga explícita

Abra InstructorController.cs y examine cómo obtiene el Index método la lista de inscripciones de un curso seleccionado:

if (courseID != null)
{
    ViewBag.CourseID = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}

Cuando recuperó la lista de instructores, especificó la carga diligente para la Courses propiedad de navegación y para la Department propiedad de cada curso. A continuación, coloca la Courses colección en el modelo de vista y ahora tiene acceso a la Enrollments propiedad de navegación desde una entidad de esa colección. Dado que no especificó la carga diligente para la propiedad Course.Enrollments de navegación, los datos de esa propiedad aparecen en la página como resultado de la carga diferida.

Si deshabilitó la carga diferida sin cambiar el código de ninguna otra manera, la propiedad Enrollments sería null independientemente de cuántas inscripciones tenía realmente el curso. En ese caso, para cargar la propiedad Enrollments, tendría que especificar la carga diligente o la carga explícita. Ya has visto cómo hacer una carga diligente. Para ver un ejemplo de carga explícita, reemplace el Index método por el código siguiente, que carga explícitamente la Enrollments propiedad. Los cambios de código aparecen resaltados.

public ActionResult Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();

    viewModel.Instructors = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses.Select(c => c.Department))
        .OrderBy(i => i.LastName);

    if (id != null)
    {
        ViewBag.InstructorID = id.Value;
        viewModel.Courses = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single().Courses;
    }
    
    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        // Lazy loading
        //viewModel.Enrollments = viewModel.Courses.Where(
        //    x => x.CourseID == courseID).Single().Enrollments;
        // Explicit loading
        var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
        db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            db.Entry(enrollment).Reference(x => x.Student).Load();
        }

        viewModel.Enrollments = selectedCourse.Enrollments;
    }

    return View(viewModel);
}

Después de obtener la entidad Course seleccionada, el nuevo código carga explícitamente la propiedad de navegación del curso Enrollments:

db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();

A continuación, carga explícitamente la entidad Student relacionada de cada entidad Enrollment:

db.Entry(enrollment).Reference(x => x.Student).Load();

Tenga en cuenta que usa el método Collection para cargar una propiedad de colección, pero para una propiedad que contiene solo una entidad, se usa el método Reference.

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

Descargar el proyecto completado

Recursos adicionales

Puede encontrar enlaces a otros recursos de Entity Framework en el ASP.NET Acceso a datos: recursos recomendados.

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

Pase al artículo siguiente para obtener información sobre cómo actualizar datos relacionados.