Hướng dẫn: Tìm hiểu về các tình huống nâng cao - ASP.NET MVC với EF Core
Trong hướng dẫn trước, bạn đã triển khai TPH inheritance. Hướng dẫn này giới thiệu một số chủ đề hữu ích khi bạn vượt qua những kiến thức cơ bản trong phát triển ứng dụng web ASP.NET Core sử dụng Entity Framework Core.
Trong hướng dẫn này, bạn sẽ:
- Thực hiện raw SQL queries (truy vấn SQL thuần)
- Gọi truy vấn để trả về entities
- Gọi truy vấn để trả về các loại khác
- Gọi truy vấn cập nhật
- Kiểm tra SQL queries
- Tạo abstraction layer (lớp trừu tượng)
- Tìm hiểu về Automatic change detection (phát hiện thay đổi tự động)
- Tìm hiểu về source code và kế hoạch phát triển của EF Core
- Tìm hiểu cách sử dụng dynamic LINQ để đơn giản hóa code
Điều kiện tiên quyết
Thực hiện Raw SQL Queries
Một trong những lợi thế của việc sử dụng Entity Framework là nó tránh gắn kết code của bạn quá chặt với một phương thức lưu trữ dữ liệu cụ thể. Nó thực hiện điều này bằng cách tạo các SQL queries và commands cho bạn. Nhưng có những tình huống ngoại lệ khi bạn cần chạy các SQL queries cụ thể mà bạn đã tạo thủ công.
Trong EF Core 1.0, bạn có các lựa chọn sau:
- Sử dụng phương thức
DbSet.FromSqlcho các truy vấn trả về kiểu entity. Các đối tượng được trả về phải thuộc loại mà đối tượngDbSetmong đợi, và chúng được tự động theo dõi (tracked) bởi database context trừ khi bạn tắt tính năng theo dõi. - Sử dụng
Database.ExecuteSqlCommandcho các lệnh không phải truy vấn.
Nếu bạn cần chạy truy vấn trả về các loại không phải entity, bạn có thể sử dụng ADO.NET với kết nối cơ sở dữ liệu được cung cấp bởi EF.
Như thường lệ khi thực thi các lệnh SQL trong ứng dụng web, bạn phải thực hiện các biện pháp phòng ngừa để bảo vệ trang của bạn khỏi các cuộc tấn công SQL injection (tiêm nhiễm SQL). Một cách để làm điều đó là sử dụng parameterized queries (truy vấn có tham số) để đảm bảo rằng các chuỗi do trang web gửi không thể được diễn giải là lệnh SQL.
Gọi truy vấn trả về Entities
Lớp DbSet<TEntity> cung cấp một phương thức bạn có thể sử dụng để thực thi truy vấn trả về thực thể kiểu TEntity. Để xem cách hoạt động, bạn sẽ thay đổi code trong phương thức Details của Department controller.
Trong DepartmentsController.cs, trong phương thức Details, thay thế code lấy bộ phận bằng một lời gọi phương thức FromSql:
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync();
if (department == null)
{
return NotFound();
}
return View(department);
}Gọi truy vấn trả về các loại khác
Trước đó, bạn đã tạo lưới thống kê student cho trang About, hiển thị số lượng student cho mỗi ngày đăng ký. Bạn lấy dữ liệu từ Students entity set (_context.Students) và dùng LINQ để chiếu kết quả thành danh sách các đối tượng view model EnrollmentDateGroup. Giả sử bạn muốn viết SQL trực tiếp thay vì dùng LINQ. Để làm điều đó, bạn cần chạy SQL query trả về thứ gì đó ngoài entity objects. Trong EF Core 1.0, một cách là viết code ADO.NET và lấy kết nối cơ sở dữ liệu từ EF.
Trong HomeController.cs, thay thế phương thức About bằng code sau:
public async Task<ActionResult> About()
{
List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}Thêm câu lệnh using:
using System.Data.Common;
Gọi truy vấn cập nhật
Giả sử quản trị viên Contoso University muốn thực hiện các thay đổi toàn cục trong cơ sở dữ liệu, chẳng hạn như thay đổi số lượng tín chỉ cho mỗi khóa học. Trong phần này bạn sẽ triển khai một trang web cho phép người dùng chỉ định một hệ số để thay đổi số lượng tín chỉ cho tất cả các khóa học, và bạn sẽ thực hiện thay đổi bằng cách thực thi một câu lệnh SQL UPDATE.
Trong CoursesController.cs, thêm các phương thức UpdateCourseCredits cho HttpGet và HttpPost:
public IActionResult UpdateCourseCredits()
{
return View();
}[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
if (multiplier != null)
{
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
}
return View();
}Trong Solution Explorer, nhấp chuột phải vào thư mục Views/Courses, sau đó nhấp Add > New Item. Trong hộp thoại Add New Item, nhấp ASP.NET Core trong phần Installed ở ngăn trái, nhấp Razor View, và đặt tên view mới là UpdateCourseCredits.cshtml.
Trong Views/Courses/UpdateCourseCredits.cshtml, thay thế code template bằng code sau:
@{
ViewBag.Title = "UpdateCourseCredits";
}
<h2>Update Course Credits</h2>
@if (ViewData["RowsAffected"] == null)
{
<form asp-action="UpdateCourseCredits">
<div class="form-actions no-color">
<p>
Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
</p>
<p>
<input type="submit" value="Update" class="btn btn-default" />
</p>
</div>
</form>
}
@if (ViewData["RowsAffected"] != null)
{
<p>
Number of rows updated: @ViewData["RowsAffected"]
</p>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>Chạy phương thức UpdateCourseCredits bằng cách chọn tab Courses, sau đó thêm "/UpdateCourseCredits" vào cuối URL trong thanh địa chỉ của trình duyệt.
Kiểm tra SQL Queries
Đôi khi hữu ích khi có thể xem các SQL queries thực tế được gửi đến cơ sở dữ liệu. Chức năng logging tích hợp sẵn của ASP.NET Core được EF Core tự động sử dụng để ghi các log chứa SQL cho các truy vấn và cập nhật.
Mở StudentsController.cs và trong phương thức Details, đặt breakpoint tại câu lệnh if (student == null).
Chạy ứng dụng ở chế độ debug và vào trang Details cho một student.
Đi đến cửa sổ Output hiển thị debug output, bạn sẽ thấy truy vấn:
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30'] SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName], [s].[EnrollmentDate] FROM [Person] AS [s] WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0) ORDER BY [s].[ID]
Bạn sẽ nhận thấy một điều có thể làm bạn ngạc nhiên: SQL chọn tối đa 2 hàng (TOP(2)) từ bảng Person. Phương thức SingleOrDefaultAsync không resolve thành 1 hàng trên máy chủ. Lý do là:
- Nếu truy vấn trả về nhiều hàng, phương thức trả về null.
- Để xác định liệu truy vấn có trả về nhiều hàng không, EF phải kiểm tra xem nó có trả về ít nhất 2 hàng không.
Tạo Abstraction Layer (lớp trừu tượng)
Nhiều nhà phát triển viết code để triển khai các pattern repository và unit of work như một wrapper (lớp bọc) xung quanh code làm việc với Entity Framework. Các pattern này nhằm tạo ra một abstraction layer giữa data access layer (lớp truy cập dữ liệu) và business logic layer (lớp logic nghiệp vụ) của ứng dụng.
Tuy nhiên, viết thêm code để triển khai các pattern này không phải lúc nào cũng là lựa chọn tốt nhất cho các ứng dụng sử dụng EF, vì nhiều lý do:
- Lớp context EF tự nó đã cách ly code của bạn khỏi code cụ thể của data-store.
- Lớp context EF có thể đóng vai trò là lớp unit-of-work cho các cập nhật cơ sở dữ liệu mà bạn thực hiện bằng EF.
- EF bao gồm các tính năng để triển khai TDD (Test-Driven Development) mà không cần viết code repository.
Entity Framework Core triển khai in-memory database provider (trình cung cấp cơ sở dữ liệu trong bộ nhớ) có thể được sử dụng để kiểm thử.
Automatic Change Detection (phát hiện thay đổi tự động)
Entity Framework xác định cách một thực thể đã thay đổi (và do đó cần gửi những cập nhật nào đến cơ sở dữ liệu) bằng cách so sánh các giá trị hiện tại của thực thể với các giá trị gốc.
Một số phương thức gây ra automatic change detection:
- DbContext.SaveChanges
- DbContext.Entry
- ChangeTracker.Entries
Nếu bạn đang theo dõi số lượng lớn thực thể và gọi một trong các phương thức này nhiều lần trong vòng lặp, bạn có thể nhận được cải thiện hiệu suất đáng kể bằng cách tạm thời tắt automatic change detection:
_context.ChangeTracker.AutoDetectChangesEnabled = false;
Source code và kế hoạch phát triển EF Core
Source code của Entity Framework Core tại https://github.com/dotnet/efcore. Repository EF Core chứa các bản build hàng đêm, theo dõi vấn đề (issue tracking), thông số tính năng, ghi chú cuộc họp thiết kế và lộ trình (roadmap) phát triển trong tương lai.
Reverse Engineer từ cơ sở dữ liệu hiện có
Để đảo ngược thiết kế (reverse engineer) mô hình dữ liệu bao gồm các lớp entity từ cơ sở dữ liệu hiện có, sử dụng lệnh scaffold-dbcontext.
Sử dụng Dynamic LINQ để đơn giản hóa code
Hướng dẫn thứ ba trong loạt này trình bày cách viết code LINQ bằng cách hard-coding (mã hóa cứng) tên cột trong câu lệnh switch. Với hai cột để lựa chọn, điều này hoạt động tốt, nhưng nếu bạn có nhiều cột, code có thể trở nên dài dòng.
Để giải quyết vấn đề đó, bạn có thể sử dụng phương thức EF.Property để chỉ định tên thuộc tính dưới dạng chuỗi. Để thử cách tiếp cận này, hãy thay thế phương thức Index trong StudentsController bằng code sau:
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] =
String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}
bool descending = false;
if (sortOrder.EndsWith("_desc"))
{
sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
descending = true;
}
if (descending)
{
students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
}
else
{
students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
}
int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
pageNumber ?? 1, pageSize));
}Xử lý các lỗi thường gặp
ContosoUniversity.dll được sử dụng bởi một tiến trình khác
Thông báo lỗi:
Cannot open '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll' because it is being used by another process.
Giải pháp: Dừng site trong IIS Express.
Migration được scaffold mà không có code trong phương thức Up và Down
Nguyên nhân có thể: Các lệnh EF CLI không tự động đóng và lưu code files. Nếu bạn có các thay đổi chưa lưu khi chạy lệnh migrations add, EF sẽ không tìm thấy thay đổi của bạn.
Giải pháp: Chạy lệnh migrations remove, lưu các thay đổi code và chạy lại lệnh migrations add.
Lỗi trong khi chạy database update
Nếu bạn gặp lỗi migration không thể giải quyết, bạn có thể thay đổi tên cơ sở dữ liệu trong connection string hoặc xóa cơ sở dữ liệu.
Để xóa cơ sở dữ liệu bằng CLI:
dotnet ef database drop