Phần 6: Razor Pages với EF Core trong ASP.NET Core - Đọc Dữ Liệu Liên Quan
Hướng dẫn này minh họa cách đọc và hiển thị dữ liệu liên quan. Dữ liệu liên quan là dữ liệu mà EF Core tải vào navigation properties (thuộc tính điều hướng) của entity.
Eager, Explicit, và Lazy Loading
Có một số cách EF Core có thể tải dữ liệu liên quan vào navigation properties của entity:
- Eager loading (tải tham lam). Eager loading xảy ra khi một truy vấn cho một loại entity cũng tải các entity liên quan. Khi một entity được đọc, dữ liệu liên quan của nó được lấy. Điều này thường dẫn đến một truy vấn join duy nhất lấy tất cả dữ liệu cần thiết. EF Core sẽ phát ra nhiều truy vấn cho một số loại eager loading. Phát ra nhiều truy vấn có thể hiệu quả hơn một truy vấn đơn lớn. Eager loading được chỉ định bằng phương thức
IncludevàThenInclude.
Eager loading gửi nhiều truy vấn khi navigation collection được bao gồm:
- Một truy vấn cho truy vấn chính
- Một truy vấn cho mỗi collection "edge" trong load tree
- Separate queries with
Load(truy vấn riêng lẻ vớiLoad): Dữ liệu có thể được lấy trong các truy vấn riêng lẻ, và EF Core "fixes up" (sửa lại) navigation properties. "Fixes up" có nghĩa là EF Core tự động điền vào navigation properties. Truy vấn riêng lẻ vớiLoadgiống explicit loading hơn là eager loading.
Lưu ý: EF Core tự động sửa lại navigation properties cho bất kỳ entity nào đã được tải trước đó vào context instance. Ngay cả khi dữ liệu cho navigation property không được bao gồm rõ ràng, thuộc tính vẫn có thể được điền nếu một số hoặc tất cả các entity liên quan đã được tải trước đó.
- Explicit loading (tải rõ ràng). Khi entity được đọc lần đầu, dữ liệu liên quan không được lấy. Code phải được viết để lấy dữ liệu liên quan khi cần. Explicit loading với các truy vấn riêng lẻ dẫn đến nhiều truy vấn được gửi đến database. Với explicit loading, code chỉ định navigation properties cần được tải. Sử dụng phương thức
Loadđể thực hiện explicit loading. - Lazy loading (tải lười). Khi entity được đọc lần đầu, dữ liệu liên quan không được lấy. Lần đầu tiên navigation property được truy cập, dữ liệu cần thiết cho navigation property đó được tự động lấy. Một truy vấn được gửi đến database mỗi khi navigation property được truy cập lần đầu tiên. Lazy loading có thể ảnh hưởng đến hiệu năng, ví dụ khi lập trình viên sử dụng N+1 queries - tải parent và liệt kê các children.
Tạo các trang Course
Entity Course bao gồm navigation property chứa entity Department liên quan.
Để hiển thị tên department (khoa) được phân công cho course:
- Tải entity
Departmentliên quan vào navigation propertyCourse.Department. - Lấy tên từ thuộc tính
Namecủa entityDepartment.
Scaffold các trang Course
Visual Studio
- Làm theo hướng dẫn trong Scaffold Student pages với các ngoại lệ sau:
- Tạo thư mục Pages/Courses.
- Sử dụng
Coursecho model class. - Sử dụng context class hiện có thay vì tạo mới.
Visual Studio Code
- Tạo thư mục Pages/Courses.
- Chạy lệnh sau để scaffold các trang Course.
Trên Windows:
``dotnetcli dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages\Courses --referenceScriptLibraries ``
Trên Linux hoặc macOS:
``dotnetcli dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages/Courses --referenceScriptLibraries ``
- Mở
Pages/Courses/Index.cshtml.csvà kiểm tra phương thứcOnGetAsync. Scaffolding engine đã chỉ định eager loading cho navigation propertyDepartment. Phương thứcIncludechỉ định eager loading. - Chạy ứng dụng và chọn link Courses. Cột department hiển thị
DepartmentID, không hữu ích.
Hiển thị tên department
Cập nhật Pages/Courses/Index.cshtml.cs với code sau:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Courses
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public IList<Course> Courses { get; set; }
public async Task OnGetAsync()
{
Courses = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
}
}Code trên thay đổi thuộc tính Course thành Courses và thêm AsNoTracking.
No-tracking queries (truy vấn không theo dõi) hữu ích khi kết quả được sử dụng trong tình huống chỉ đọc. Chúng thường thực thi nhanh hơn vì không cần thiết lập thông tin theo dõi thay đổi.
Cập nhật Pages/Courses/Index.cshtml với code sau:
@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
ViewData["Title"] = "Courses";
}
<h1>Courses</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Courses[0].CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Courses)
{
<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-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>Những thay đổi được thực hiện:
- Đổi tên thuộc tính
CoursethànhCourses. - Thêm cột Number hiển thị giá trị thuộc tính
CourseID. - Thay đổi cột Department để hiển thị tên department. Code hiển thị thuộc tính
Namecủa entityDepartmentđược tải vào navigation propertyDepartment.
Chạy ứng dụng và chọn tab Courses để xem danh sách với tên department.
Tải dữ liệu liên quan với Select
Phương thức OnGetAsync tải dữ liệu liên quan bằng phương thức Include. Phương thức Select là phương án thay thế chỉ tải dữ liệu liên quan cần thiết. Với các item đơn lẻ như Department.Name, nó sử dụng SQL INNER JOIN. Với collections, nó sử dụng truy cập database khác, tương tự như toán tử Include trên collections.
Code sau tải dữ liệu liên quan bằng phương thức Select:
public IList<CourseViewModel> CourseVM { get; set; }
public async Task OnGetAsync()
{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}CourseViewModel:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}Tạo các trang Instructor
Phần này scaffold các trang Instructor và thêm các Courses và Enrollments liên quan vào trang Instructors Index.
Trang này đọc và hiển thị dữ liệu liên quan theo các cách sau:
- Danh sách instructor hiển thị dữ liệu liên quan từ entity
OfficeAssignment. EntityInstructorvàOfficeAssignmentcó quan hệ one-to-zero-or-one. Eager loading được sử dụng cho các entityOfficeAssignment. Eager loading thường hiệu quả hơn khi dữ liệu liên quan cần được hiển thị. - Khi người dùng chọn một instructor, các entity
Courseliên quan được hiển thị. EntityInstructorvàCoursecó quan hệ many-to-many. Eager loading được sử dụng cho các entityCoursevà các entityDepartmentliên quan của chúng. - Khi người dùng chọn một course, dữ liệu liên quan từ entity
Enrollmentsđược hiển thị.
Tạo view model
Tạo Models/SchoolViewModels/InstructorIndexData.cs với code sau:
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; }
}
}Scaffold các trang Instructor
Visual Studio
- Làm theo hướng dẫn trong Scaffold the student pages với các ngoại lệ sau:
- Tạo thư mục Pages/Instructors.
- Sử dụng
Instructorcho model class. - Sử dụng context class hiện có thay vì tạo mới.
Visual Studio Code
- Tạo thư mục Pages/Instructors.
- Chạy lệnh sau để scaffold các trang Instructor:
Trên Windows:
``dotnetcli dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages\Instructors --referenceScriptLibraries ``
Trên Linux hoặc macOS:
``dotnetcli dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages/Instructors --referenceScriptLibraries ``
Cập nhật Pages/Instructors/Index.cshtml.cs với code sau:
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels; // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public InstructorIndexData InstructorData { get; set; }
public int InstructorID { get; set; }
public int CourseID { get; set; }
public async Task OnGetAsync(int? id, int? courseID)
{
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.ThenInclude(c => c.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.Courses;
}
if (courseID != null)
{
CourseID = courseID.Value;
IEnumerable<Enrollment> Enrollments = await _context.Enrollments
.Where(x => x.CourseID == CourseID)
.Include(i=>i.Student)
.ToListAsync();
InstructorData.Enrollments = Enrollments;
}
}
}
}Phương thức OnGetAsync nhận route data tùy chọn cho ID của instructor được chọn.
Code chỉ định eager loading cho các navigation properties sau:
Instructor.OfficeAssignmentInstructor.CoursesCourse.Department
Khi một instructor được chọn (id != null), instructor được chọn được lấy từ danh sách instructors trong view model. Thuộc tính Courses của view model được tải với các entity Course từ navigation property Courses của instructor được chọn.
Phương thức Where trả về collection. Trong trường hợp này, filter chọn một entity duy nhất, vì vậy phương thức Single được gọi để chuyển đổi collection thành entity Instructor duy nhất.
Cập nhật trang Instructors Index
Cập nhật Pages/Instructors/Index.cshtml với code sau:
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-page="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.InstructorData.Instructors)
{
string selectedRow = "";
if (item.ID == Model.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.Courses)
{
@course.CourseID @: @course.Title <br />
}
}
</td>
<td>
<a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@if (Model.InstructorData.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.InstructorData.Courses)
{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
<a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
@if (Model.InstructorData.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.InstructorData.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}Code trên thực hiện các thay đổi sau:
- Cập nhật chỉ thị
pagethành@page "{id:int?}"."{id:int?}"là route template. Route template thay đổi query string số nguyên trong URL thành route data. Ví dụ, nhấp vào link Select cho instructor chỉ với chỉ thị@pagetạo ra URL như sau:https://localhost:5001/Instructors?id=2. Khi chỉ thị page là@page "{id:int?}", URL là:https://localhost:5001/Instructors/2 - Thêm cột Office hiển thị
item.OfficeAssignment.Locationchỉ khiitem.OfficeAssignmentkhông phải null. - Thêm cột Courses hiển thị các courses được dạy bởi mỗi instructor.
- Thêm code tự động thêm
class="table-success"vào phần tửtrcủa instructor và course được chọn. - Thêm hyperlink mới có nhãn Select.
- Thêm bảng courses cho Instructor được chọn.
- Thêm bảng student enrollments cho course được chọn.
Chạy ứng dụng và chọn tab Instructors. Trang hiển thị Location (văn phòng) từ entity OfficeAssignment liên quan. Nếu OfficeAssignment là null, ô bảng trống được hiển thị.
Nhấp vào link Select cho một instructor. Kiểu hàng thay đổi và các courses được phân công cho instructor đó được hiển thị.
Chọn một course để xem danh sách sinh viên đã đăng ký và điểm của họ.