Compartilhar via


Tutorial: Ler dados relacionados com o EF em um aplicativo MVC do ASP.NET

No tutorial anterior, você concluiu o modelo de dados Escola. Neste tutorial, você lerá e exibirá dados relacionados, ou seja, dados que o Entity Framework carrega nas propriedades de navegação.

As ilustrações a seguir mostram as páginas com as quais você trabalhará.

Captura de tela que mostra a página Cursos com uma lista de cursos.

Instructors_index_page_with_instructor_and_course_selected

Download do projeto concluído

O aplicativo Web de exemplo da Contoso University demonstra como criar aplicativos MVC 5 ASP.NET usando o Entity Framework 6 Code First e o Visual Studio. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial da série.

Neste tutorial, você:

  • Aprender a carregar entidades relacionadas
  • Criar uma página Cursos
  • Criar uma página Instrutores

Pré-requisitos

Há várias maneiras pelas quais o Entity Framework pode carregar dados relacionados nas propriedades de navegação de uma entidade:

  • Carregamento lento. Quando a entidade é lida pela primeira vez, os dados relacionados não são recuperados. No entanto, na primeira vez que você tenta acessar uma propriedade de navegação, os dados necessários para essa propriedade de navegação são recuperados automaticamente. Isso resulta em várias consultas enviadas ao banco de dados — uma para a própria entidade e outra sempre que os dados relacionados à entidade devem ser recuperados. A DbContext classe habilita o carregamento lento por padrão.

    Lazy_loading_example

  • Carregamento adiantado. Quando a entidade é lida, os dados relacionados são recuperados com ela. Normalmente, isso resulta em uma única consulta de junção que recupera todos os dados necessários. Você especifica o carregamento adiantado usando o Include método.

    Eager_loading_example

  • Carregamento explícito. Isso é semelhante ao carregamento lento, exceto que você recupera explicitamente os dados relacionados no código; Isso não acontece automaticamente quando você acessa uma propriedade de navegação. Você carrega os dados relacionados manualmente obtendo a entrada do gerenciador de estado do objeto para uma entidade e chamando o método Collection.Load para coleções ou o método Reference.Load para propriedades que contêm uma única entidade. (No exemplo a seguir, se você quisesse carregar a propriedade de navegação Administrador, substituiria Collection(x => x.Courses) por Reference(x => x.Administrator).) Normalmente, você usaria o carregamento explícito somente quando desativasse o carregamento lento.

    Explicit_loading_example

Como eles não recuperam imediatamente os valores de propriedade, o carregamento lento e o carregamento explícito também são conhecidos como carregamento adiado.

Considerações sobre o desempenho

Se você sabe que precisa de dados relacionados para cada entidade recuperada, o carregamento adiantado costuma oferecer o melhor desempenho, porque uma única consulta enviada para o banco de dados é geralmente mais eficiente do que consultas separadas para cada entidade recuperada. Por exemplo, nos exemplos acima, suponha que cada departamento tenha dez cursos relacionados. O exemplo de carregamento ansioso resultaria em apenas uma única consulta (junção) e uma única viagem de ida e volta para o banco de dados. Os exemplos de carregamento lento e carregamento explícito resultariam em onze consultas e onze viagens de ida e volta ao banco de dados. As viagens de ida e volta extras para o banco de dados são especialmente prejudiciais ao desempenho quando a latência é alta.

Por outro lado, em alguns cenários, o carregamento lento é mais eficiente. O carregamento adiantado pode fazer com que uma junção muito complexa seja gerada, que o SQL Server não pode processar com eficiência. Ou, se você precisar acessar as propriedades de navegação de uma entidade apenas para um subconjunto de um conjunto das entidades que você está processando, o carregamento lento poderá ter um desempenho melhor porque o carregamento adiantado recuperaria mais dados do que o necessário. Se o desempenho for crítico, será melhor testar o desempenho das duas maneiras para fazer a melhor escolha.

O carregamento lento pode mascarar o código que causa problemas de desempenho. Por exemplo, o código que não especifica o carregamento ansioso ou explícito, mas processa um grande volume de entidades e usa várias propriedades de navegação em cada iteração, pode ser muito ineficiente (devido a muitas viagens de ida e volta ao banco de dados). Um aplicativo que tem um bom desempenho no desenvolvimento usando um SQL Server local pode ter problemas de desempenho quando movido para o Banco de Dados SQL do Azure devido ao aumento da latência e ao carregamento lento. A criação de perfil das consultas de banco de dados com uma carga de teste realista ajudará você a determinar se o carregamento lento é apropriado. Para obter mais informações, consulte Desmistificando estratégias do Entity Framework: carregando dados relacionados e usando o Entity Framework para reduzir a latência de rede para o SQL Azure.

Desabilitar o carregamento lento antes da serialização

Se você deixar o carregamento lento habilitado durante a serialização, poderá acabar consultando significativamente mais dados do que o pretendido. A serialização geralmente funciona acessando cada propriedade em uma instância de um tipo. O acesso à propriedade dispara o carregamento lento e essas entidades carregadas lentamente são serializadas. Em seguida, o processo de serialização acessa cada propriedade das entidades carregadas lentamente, potencialmente causando ainda mais carregamento e serialização lentos. Para evitar essa reação em cadeia descontrolada, desative o carregamento lento antes de serializar uma entidade.

A serialização também pode ser complicada pelas classes de proxy que o Entity Framework usa, conforme explicado no tutorial Cenários Avançados.

Uma maneira de evitar problemas de serialização é serializar DTOs (objetos de transferência de dados) em vez de objetos de entidade, conforme mostrado no tutorial Usando a API Web com o Entity Framework .

Se você não usar DTOs, poderá desabilitar o carregamento lento e evitar problemas de proxy desabilitando a criação de proxy.

Aqui estão algumas outras maneiras de desativar o carregamento lento:

  • Para propriedades de navegação específicas, omita a virtual palavra-chave ao declarar a propriedade.

  • Para todas as propriedades de navegação, defina LazyLoadingEnabled como false, coloque o seguinte código no construtor da classe de contexto:

    this.Configuration.LazyLoadingEnabled = false;
    

Criar uma página Cursos

A entidade Course inclui uma propriedade de navegação que contém a entidade Department do departamento ao qual o curso é atribuído. Para exibir o nome do departamento atribuído em uma lista de cursos, você precisa obter a Name propriedade da Department entidade que está na Course.Department propriedade de navegação.

Crie um controlador chamado CourseController (não CoursesController) para o Course tipo de entidade, usando as mesmas opções para o Controlador MVC 5 com exibições, usando o scaffolder do Entity Framework que você fez anteriormente para o Student controlador:

Configuração Valor
Classe de modelo Selecione Curso (ContosoUniversity.Models).
Classe de contexto de dados Selecione SchoolContext (ContosoUniversity.DAL).
Nome do controlador Digite CourseController. Novamente, não CoursesController com um s. Quando você selecionou Curso (ContosoUniversity.Models), o valor do nome do controlador foi preenchido automaticamente. Você tem que mudar o valor.

Deixe os outros valores padrão e adicione o controlador.

Abra Controllers\CourseController.cs e observe o Index método:

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

O scaffolding automático especificou o carregamento adiantado para a propriedade de navegação Department usando o método Include.

Abra Views\Course\Index.cshtml e substitua o código do modelo pelo código a seguir. As alterações são realçadas:

@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>

Você fez as seguintes alterações no código gerado por scaffolding:

  • O cabeçalho mudou de Índice para Cursos.
  • Adicionou uma coluna Número que mostra o valor da propriedade CourseID. Por padrão, as chaves primárias não são scaffolded porque normalmente não têm sentido para os usuários finais. No entanto, nesse caso, a chave primária é significativa e você deseja mostrá-la.
  • Moveu a coluna Departamento para o lado direito e mudou seu título. O scaffolder escolheu corretamente exibir a Name propriedade da entidade, mas aqui na página Curso o título da coluna deve ser Departamento em vez de Department Nome.

Observe que, para a coluna Department, o código scaffolded exibe a Name Department propriedade da entidade carregada na Department propriedade de navegação:

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

Execute a página (selecione a guia Cursos na home page da Contoso University) para ver a lista com nomes de departamento.

Criar uma página Instrutores

Nesta seção, você criará um controlador e uma exibição para a Instructor entidade para exibir a página Instrutores. Essa página lê e exibe dados relacionados das seguintes maneiras:

  • A lista de instrutores exibe dados relacionados da entidade OfficeAssignment. As entidades Instructor e OfficeAssignment estão em uma relação um para zero ou um. Você usará o carregamento adiantado para as entidades OfficeAssignment. Conforme explicado anteriormente, o carregamento adiantado é geralmente mais eficiente quando você precisa dos dados relacionados para todas as linhas recuperadas da tabela primária. Nesse caso, você deseja exibir atribuições de escritório para todos os instrutores exibidos.
  • Quando o usuário seleciona um instrutor, as entidades Course relacionadas são exibidas. As entidades Instructor e Course estão em uma relação muitos para muitos. O carregamento adiantado é usado para as entidades Course e suas entidades Department relacionadas. Nesse caso, o carregamento lento pode ser mais eficiente porque você precisa de cursos apenas para o instrutor selecionado. No entanto, este exemplo mostra como usar o carregamento adiantado para propriedades de navegação em entidades que estão nas propriedades de navegação.
  • Quando o usuário seleciona um curso, os dados relacionados do conjunto de entidades Enrollments são exibidos. As entidades Course e Enrollment estão em uma relação um-para-muitos. Você adicionará carregamento explícito para Enrollment entidades e suas entidades relacionadas Student . (O carregamento explícito não é necessário porque o carregamento lento está habilitado, mas isso mostra como fazer o carregamento explícito.)

Criar um modelo de exibição para a exibição de índice do instrutor

A página Instrutores mostra três tabelas diferentes. Portanto, você criará um modelo de exibição que inclui três propriedades, cada uma contendo os dados de uma das tabelas.

Na pasta ViewModels, crie InstructorIndexData.cs e substitua o código existente pelo seguinte 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; }
    }
}

Criar o controlador do instrutor e as exibições

Crie um controlador (não InstructorsController) com a InstructorController ação de leitura/gravação do EF:

Configuração Valor
Classe de modelo Selecione Instrutor (ContosoUniversity.Models).
Classe de contexto de dados Selecione SchoolContext (ContosoUniversity.DAL).
Nome do controlador Digite InstructorController. Novamente, não InstructorsController com um s. Quando você selecionou Curso (ContosoUniversity.Models), o valor do nome do controlador foi preenchido automaticamente. Você tem que mudar o valor.

Deixe os outros valores padrão e adicione o controlador.

Abra Controllers\InstructorController.cs e adicione uma using instrução para o ViewModels namespace:

using ContosoUniversity.ViewModels;

O código scaffolded no Index método especifica o carregamento ansioso apenas para a OfficeAssignment propriedade de navegação:

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

Substitua o Index método pelo seguinte código para carregar dados relacionados adicionais e colocá-los no modelo de exibição:

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);
}

O método aceita dados de rota opcionais (id) e um parâmetro de string de consulta (courseID) que fornecem os valores de ID do instrutor selecionado e do curso selecionado e transmite todos os dados necessários para a exibição. Os parâmetros são fornecidos pelos hiperlinks Selecionar na página.

O código começa com a criação de uma instância do modelo de exibição e colocando-a na lista de instrutores. O código especifica o carregamento ansioso para a Instructor.OfficeAssignment propriedade e a Instructor.Courses propriedade de navegação.

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

O segundo Include método carrega Courses e, para cada Course carregado, ele faz o carregamento ansioso para a Course.Department propriedade de navegação.

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

Como mencionado anteriormente, o carregamento ansioso não é necessário, mas é feito para melhorar o desempenho. Como a exibição sempre exige a entidade OfficeAssignment, é mais eficiente buscar isso na mesma consulta. Course As entidades são necessárias quando um instrutor é selecionado na página da Web, portanto, o carregamento ansioso é melhor do que o carregamento lento somente se a página for exibida com mais frequência com um curso selecionado do que sem.

Se uma ID de instrutor tiver sido selecionada, o instrutor selecionado será recuperado da lista de instrutores no modelo de exibição. Em seguida, a propriedade Courses do modelo de exibição é carregada com as entidades Course da propriedade de navegação Courses desse instrutor.

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

O Where método retorna uma coleção, mas, nesse caso, os critérios passados para esse método resultam em apenas uma única Instructor entidade sendo retornada. O método Single converte a coleção em uma única entidade Instructor, que fornece acesso à propriedade Courses dessa entidade.

Você usa o método Single em uma coleção quando sabe que a coleção terá apenas um item. O Single método gerará uma exceção se a coleção passada para ele estiver vazia ou se houver mais de um item. Uma alternativa é SingleOrDefault, que retorna um valor padrão (null nesse caso) se a coleção estiver vazia. No entanto, nesse caso, isso ainda resultaria em uma exceção (de tentar encontrar uma Courses propriedade em uma null referência) e a mensagem de exceção indicaria menos claramente a causa do problema. Ao chamar o Single método, você também pode passar a Where condição em vez de chamar o Where método separadamente:

.Single(i => i.ID == id.Value)

Em vez de:

.Where(I => i.ID == id.Value).Single()

Em seguida, se um curso foi selecionado, o curso selecionado é recuperado na lista de cursos no modelo de exibição. Em seguida, a propriedade do modelo de Enrollments exibição é carregada com as Enrollment entidades da Enrollments propriedade de navegação desse curso.

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

Modificar a exibição do índice do instrutor

Em Views\Instructor\Index.cshtml, substitua o código do modelo pelo código a seguir. As alterações são realçadas:

@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>

Você fez as seguintes alterações no código existente:

  • Alterou a classe de modelo para InstructorIndexData.

  • Alterou o título de página de Índice para Instrutores.

  • Adicionada uma coluna do Office que é exibida item.OfficeAssignment.Location somente se item.OfficeAssignment não for nula. (Como essa é uma relação de um para zero ou um, pode não haver uma entidade relacionada OfficeAssignment .)

    <td> 
        @if (item.OfficeAssignment != null) 
        { 
            @item.OfficeAssignment.Location  
        } 
    </td>
    
  • Adicionado código que será adicionado class="success" dinamicamente ao tr elemento do instrutor selecionado. Isso define uma cor da tela de fundo para a linha selecionada usando uma classe Bootstrap.

    string selectedRow = ""; 
    if (item.InstructorID == ViewBag.InstructorID) 
    { 
        selectedRow = "success"; 
    } 
    <tr class="@selectedRow" valign="top">
    
  • Adicionado um novo ActionLink rotulado Selecionar imediatamente antes dos outros links em cada linha, o que faz com que a ID do instrutor selecionado seja enviada para o Index método.

Execute o aplicativo e selecione a guia Instrutores . A página exibe a Location propriedade de entidades relacionadas OfficeAssignment e uma célula de tabela vazia quando não há nenhuma entidade relacionada OfficeAssignment .

No arquivo Views\Instructor\Index.cshtml, após o elemento de fechamento table (no final do arquivo), adicione o código a seguir. Esse código exibe uma lista de cursos relacionados a um instrutor quando um instrutor é selecionado.

@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>
}

Esse código lê a propriedade Courses do modelo de exibição para exibir uma lista de cursos. Ele também fornece um Select hiperlink que envia o ID do curso selecionado para o Index método de ação.

Execute a página e selecione um instrutor. Agora, você verá uma grade que exibe os cursos atribuídos ao instrutor selecionado, e para cada curso, verá o nome do departamento atribuído.

Após o bloco de código que você acabou de adicionar, adicione o código a seguir. Isso exibe uma lista dos alunos que estão registrados em um curso quando esse curso é selecionado.

@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>
}

Esse código lê a propriedade Enrollments do modelo de exibição para exibir uma lista dos alunos matriculados no curso.

Execute a página e selecione um instrutor. Em seguida, selecione um curso para ver a lista de alunos registrados e suas notas.

Adicionando carregamento explícito

Abra InstructorController.cs e veja como o Index método obtém a lista de matrículas de um curso selecionado:

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

Ao recuperar a lista de instrutores, você especificou o carregamento ansioso para a Courses propriedade de navegação e para a Department propriedade de cada curso. Em seguida, você coloca a Courses coleção no modelo de exibição e agora está acessando a Enrollments propriedade de navegação de uma entidade nessa coleção. Como você não especificou o carregamento ansioso para a Course.Enrollments propriedade de navegação, os dados dessa propriedade estão aparecendo na página como resultado do carregamento lento.

Se você desabilitasse o carregamento lento sem alterar o código de qualquer outra forma, a Enrollments propriedade seria nula, independentemente de quantas matrículas o curso realmente tivesse. Nesse caso, para carregar a Enrollments propriedade, você teria que especificar o carregamento ansioso ou o carregamento explícito. Você já viu como fazer o carregamento ansioso. Para ver um exemplo de carregamento explícito, substitua o Index método pelo código a seguir, que carrega explicitamente a Enrollments propriedade. O código alterado é destacado.

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);
}

Depois de obter a entidade selecionada Course , o novo código carrega explicitamente a propriedade de navegação desse curso Enrollments :

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

Em seguida, ele carrega explicitamente a entidade relacionada de Student cada Enrollment entidade:

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

Observe que você usa o Collection método para carregar uma propriedade de coleção, mas para uma propriedade que contém apenas uma entidade, você usa o Reference método.

Execute a página Índice do instrutor agora e você não verá nenhuma diferença no que é exibido na página, embora tenha alterado a forma como os dados são recuperados.

Obter o código

Download do projeto concluído

Recursos adicionais

Links para outros recursos do Entity Framework podem ser encontrados no ASP.NET Acesso a Dados – Recursos Recomendados.

Próximas etapas

Neste tutorial, você:

  • Aprendeu a carregar dados relacionados
  • Criou uma página Cursos
  • Criou uma página Instrutores

Vá para o próximo artigo para aprender a atualizar dados relacionados.