Hướng dẫn: Đọc dữ liệu liên quan - ASP.NET MVC với EF Core
Trong hướng dẫn trước, bạn đã hoàn thành mô hình dữ liệu School. Trong hướng dẫn này, bạn sẽ đọc và hiển thị dữ liệu liên quan -- đó là dữ liệu mà Entity Framework tải vào các thuộc tính navigation (điều hướng).
Trong hướng dẫn này, bạn:
- Tìm hiểu cách tải dữ liệu liên quan
- Tạo trang Courses
- Tạo trang Instructors
- Tìm hiểu về explicit loading (tải tường minh)
Điều kiện tiên quyết
Tìm hiểu cách tải dữ liệu liên quan
Có một số cách mà phần mềm Object-Relational Mapping (ORM - Ánh xạ Đối tượng-Quan hệ) như Entity Framework có thể tải dữ liệu liên quan vào các thuộc tính navigation của một entity:
- Eager loading (Tải háo hức): Khi entity được đọc, dữ liệu liên quan được lấy cùng với nó. Đ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. Bạn chỉ định eager loading trong Entity Framework Core bằng cách sử dụng các phương thức
IncludevàThenInclude.
Bạn có thể lấy một số dữ liệu trong các truy vấn riêng lẻ, và EF "sắp xếp" các thuộc tính navigation. Nghĩa là, EF tự động thêm các entity được lấy riêng lẻ vào nơi chúng thuộc về trong các thuộc tính navigation của các entity đã lấy trước đó. Đối với truy vấn lấy dữ liệu liên quan, bạn có thể sử dụng phương thức Load thay vì phương thức trả về danh sách hoặc đối tượng, như ToList hoặc Single.
- Explicit loading (Tải tường minh): Khi entity được đọc lần đầu, dữ liệu liên quan không được lấy. Bạn viết mã để lấy dữ liệu liên quan nếu cần. Giống như trường hợp eager loading với các truy vấn riêng lẻ, explicit loading dẫn đến nhiều truy vấn được gửi đến cơ sở dữ liệu. Sự khác biệt là với explicit loading, mã chỉ định các thuộc tính navigation cần tải. Trong Entity Framework Core 1.1, bạn có thể sử dụng phương thức
Loadđể thực hiện explicit loading. - Lazy loading (Tải lười biếng): Khi entity được đọc lần đầu, dữ liệu liên quan không được lấy. Tuy nhiên, lần đầu tiên bạn cố gắng truy cập thuộc tính navigation, dữ liệu cần thiết cho thuộc tính navigation đó sẽ tự động được lấy. Một truy vấn được gửi đến cơ sở dữ liệu mỗi khi bạn cố lấy dữ liệu từ thuộc tính navigation lần đầu tiên. Entity Framework Core 1.0 không hỗ trợ lazy loading.
Cân nhắc về hiệu năng
Nếu bạn biết mình cần dữ liệu liên quan cho mỗi entity được lấy, eager loading thường mang lại hiệu năng tốt nhất, vì một truy vấn duy nhất gửi đến cơ sở dữ liệu thường hiệu quả hơn các truy vấn riêng lẻ cho mỗi entity được lấy. Ví dụ, giả sử mỗi khoa có mười khóa học liên quan. Eager loading tất cả dữ liệu liên quan sẽ dẫn đến chỉ một truy vấn (join) duy nhất và một lần round trip đến cơ sở dữ liệu. Một truy vấn riêng cho các khóa học của mỗi khoa sẽ dẫn đến mười một lần round trip đến cơ sở dữ liệu.
Mặt khác, trong một số trường hợp, các truy vấn riêng lẻ có hiệu quả hơn. Eager loading tất cả dữ liệu liên quan trong một truy vấn có thể khiến một join rất phức tạp được tạo ra mà SQL Server không thể xử lý hiệu quả. Nếu hiệu năng quan trọng, tốt nhất là kiểm tra hiệu năng cả hai cách để đưa ra lựa chọn tốt nhất.
Tạo trang Courses
Entity Course bao gồm thuộc tính navigation chứa entity Department của khoa mà khóa học được phân công vào. Để hiển thị tên của khoa được phân công trong danh sách các khóa học, bạn cần lấy thuộc tính Name từ entity Department có trong thuộc tính navigation Course.Department.
Tạo một controller có tên CoursesController cho loại entity Course, sử dụng cùng các tùy chọn cho scaffolder (bộ tạo khung) MVC Controller with views, using Entity Framework như bạn đã làm trước đó cho StudentsController.
Mở CoursesController.cs và kiểm tra phương thức Index. Scaffolding tự động đã chỉ định eager loading cho thuộc tính navigation Department bằng cách sử dụng phương thức Include.
Thay thế phương thức Index bằng mã sau sử dụng tên thích hợp hơn cho IQueryable trả về các entity Course (courses thay vì schoolContext):
public async Task<IActionResult> Index()
{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}Mở Views/Courses/Index.cshtml và thay thế mã template bằng mã sau:
@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>Bạn đã thực hiện các thay đổi sau với mã đã được scaffolded:
- Thay đổi tiêu đề từ Index thành Courses.
- 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 khoa. Mã hiển thị thuộc tính
Namecủa entityDepartmentđược tải vào thuộc tính navigationDepartment.
Chạy ứng dụng và chọn tab Courses để xem danh sách với tên khoa.
Tạo trang Instructors
Trong phần này, bạn sẽ tạo một controller và view cho entity Instructor để hiển thị trang Instructors.
Trang này đọc và hiển thị dữ liệu liên quan theo các cách sau:
- Danh sách instructors (giảng viên) hiển thị dữ liệu liên quan từ entity
OfficeAssignment. Các entityInstructorvàOfficeAssignmentcó mối quan hệ một-đến-không-hoặc-một. Bạn sẽ sử dụng eager loading cho các entityOfficeAssignment. - Khi người dùng chọn một instructor, các entity
Courseliên quan được hiển thị. Các entityInstructorvàCoursecó mối quan hệ nhiều-đến-nhiều. Bạn sẽ sử dụng eager loading cho các entityCoursevà các entityDepartmentliên quan của chúng. - Khi người dùng chọn một khóa học, dữ liệu liên quan từ entity set
Enrollmentsđược hiển thị.
Tạo view model cho Instructor Index view
Trang Instructors hiển thị dữ liệu từ ba bảng khác nhau. Do đó, bạn sẽ tạo một view model bao gồm ba thuộc tính, mỗi thuộc tính chứa dữ liệu cho một bảng.
Trong thư mục SchoolViewModels, tạo InstructorIndexData.cs và thay thế mã hiện có bằng mã 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; }
}
}Tạo Instructor controller và views
Tạo Instructors controller với các action đọc/ghi EF.
Mở InstructorsController.cs và thêm câu lệnh using cho namespace ViewModels:
using ContosoUniversity.Models.SchoolViewModels;
Thay thế phương thức Index bằng mã sau để thực hiện eager loading dữ liệu liên quan và đưa nó vào view model:
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);
}Phương thức chấp nhận dữ liệu route tùy chọn (id) và tham số query string (courseID) cung cấp các giá trị ID của instructor được chọn và khóa học được chọn. Các tham số được cung cấp bởi các siêu liên kết Select trên trang.
Mã bắt đầu bằng cách tạo một instance của view model và đưa vào đó danh sách các instructor. Mã chỉ định eager loading cho các thuộc tính navigation Instructor.OfficeAssignment và Instructor.CourseAssignments. Trong thuộc tính CourseAssignments, thuộc tính Course được tải, và trong đó, các thuộc tính Enrollments và Department được tải, và trong mỗi entity Enrollment, thuộc tính Student được tải.
Mã sau đây thực thi khi một instructor được chọn. Instructor được chọn được lấy từ danh sách các instructor trong view model. Thuộc tính Courses của view model sau đó được tải với các entity Course từ thuộc tính navigation CourseAssignments của 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);
}Phương thức Where trả về một collection, nhưng trong trường hợp này, tiêu chí được truyền cho phương thức đó dẫn đến chỉ một entity Instructor duy nhất được trả về. Phương thức Single chuyển đổi collection thành một entity Instructor duy nhất, cho phép bạn truy cập thuộc tính CourseAssignments của entity đó. Thuộc tính CourseAssignments chứa các entity CourseAssignment, từ đó bạn chỉ muốn các entity Course liên quan.
Bạn sử dụng phương thức Single trên một collection khi bạn biết collection sẽ chỉ có một item. Phương thức Single ném ra ngoại lệ nếu collection được truyền cho nó là rỗng hoặc nếu có nhiều hơn một item.
Tiếp theo, nếu một khóa học được chọn, khóa học được chọn được lấy từ danh sách các khóa học trong view model. Sau đó, thuộc tính Enrollments của view model được tải với các entity Enrollment từ thuộc tính navigation Enrollments của khóa học đó.
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}Tracking vs no-tracking
Các truy vấn no-tracking (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 nhanh hơn để thực thi vì không cần thiết lập thông tin theo dõi thay đổi. Nếu các entity được lấy từ cơ sở dữ liệu không cần được cập nhật, thì truy vấn no-tracking có thể hoạt động tốt hơn truy vấn tracking (theo dõi).
Sửa đổi Instructor Index view
Trong Views/Instructors/Index.cshtml, thay thế mã template bằng mã sau:
@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>Bạn đã thực hiện các thay đổi sau với mã hiện có:
- Thay đổi class model thành
InstructorIndexData. - Thay đổi tiêu đề trang từ Index thành Instructors.
- 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 khóa học do mỗi instructor giảng dạy.
- Thêm mã điều kiện thêm class Bootstrap CSS vào phần tử
trcủa instructor được chọn. - Thêm siêu liên kết mới có nhãn Select trước các liên kết khác trong mỗi hàng.
Chạy ứng dụng và chọn tab Instructors. Trang hiển thị thuộc tính Location của các entity OfficeAssignment liên quan và ô bảng rỗng khi không có entity OfficeAssignment liên quan.
Trong file Views/Instructors/Index.cshtml, sau phần tử bảng đóng, thêm mã sau. Mã này hiển thị danh sách các khóa học liên quan đến instructor khi một instructor được chọn:
@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 = "table-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>
}Mã này đọc thuộc tính Courses của view model để hiển thị danh sách các khóa học. Nó cũng cung cấp siêu liên kết Select gửi ID của khóa học được chọn đến phương thức action Index.
Làm mới trang và chọn một instructor. Bây giờ bạn thấy một lưới hiển thị các khóa học được phân công cho instructor được chọn, và với mỗi khóa học, bạn thấy tên của khoa được phân công.
Sau khối mã bạn vừa thêm, thêm mã sau. Mã này hiển thị danh sách các sinh viên đăng ký vào khóa học khi khóa học đó được chọn:
@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>
}Mã này đọc thuộc tính Enrollments của view model để hiển thị danh sách các sinh viên đăng ký vào khóa học.
Làm mới trang lại và chọn một instructor. Sau đó chọn một khóa học để xem danh sách các sinh viên đã đăng ký và điểm của họ.
Về Explicit Loading
Khi bạn lấy danh sách các instructor trong InstructorsController.cs, bạn đã chỉ định eager loading cho thuộc tính navigation CourseAssignments.
Giả sử bạn mong đợi người dùng chỉ muốn xem enrollments trong instructor và khóa học được chọn một cách hiếm khi. Trong trường hợp đó, bạn có thể chỉ muốn tải dữ liệu enrollment nếu được yêu cầu. Để xem ví dụ về cách thực hiện explicit loading, thay thế phương thức Index bằng mã sau, mã này loại bỏ eager loading cho Enrollments và tải thuộc tính đó một cách tường minh:
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);
}Mã mới bỏ các lệnh gọi phương thức ThenInclude cho dữ liệu enrollment khỏi mã lấy các entity instructor. Nó cũng bỏ AsNoTracking. Nếu một instructor và khóa học được chọn, mã highlight lấy các entity Enrollment cho khóa học được chọn, và các entity Student cho mỗi Enrollment.
Chạy ứng dụng, đến trang Instructors Index và bạn sẽ thấy không có sự khác biệt trong những gì hiển thị trên trang, mặc dù bạn đã thay đổi cách dữ liệu được lấy.