Truy vấn với EF Core
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Database đã có bảng. Giờ API cần đọc, lọc, thêm, sửa, xoá sản phẩm thật. Với EF Core, bạn viết LINQ như với list trong bộ nhớ, còn EF Core dịch nó sang SQL và chạy trong database.
Khái niệm
🔍 Truy vấn trên DbSet: LINQ viết trên DbSet không chạy trong bộ nhớ, mà được EF Core dịch sang câu SQL và chạy trong database.
💾 SaveChangesAsync: method ghi mọi thay đổi (thêm, sửa, xoá) mà DbContext đang theo dõi xuống database trong một lần.
| Việc | Code |
|---|---|
| Đọc hết | await _db.Products.ToListAsync() |
| Lọc | await _db.Products.Where(...).ToListAsync() |
| Tìm theo khoá | await _db.Products.FindAsync(id) |
| Thêm | _db.Products.Add(p) rồi SaveChangesAsync() |
| Sửa | đổi property rồi SaveChangesAsync() |
| Xoá | _db.Products.Remove(p) rồi SaveChangesAsync() |
Ví dụ
Phần đọc ghi database nằm trong DbProductStore, sau interface IProductStore
như bài Dependency injection. Controller không biết có EF Core:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
public interface IProductStore
{
Task<List<Product>> InStockAsync();
Task<bool> SetStockAsync(int id, int stock);
}
public class DbProductStore : IProductStore
{
private readonly ShopDbContext _db;
public DbProductStore(ShopDbContext db)
{
_db = db;
}
public async Task<List<Product>> InStockAsync() =>
await _db.Products
.Where(p => p.Stock > 0)
.ToListAsync();
public async Task<bool> SetStockAsync(
int id, int stock)
{
var product = await _db.Products.FindAsync(id);
if (product == null)
{
return false;
}
product.Stock = stock;
await _db.SaveChangesAsync();
return true;
}
}
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly IProductStore _store;
public ProductsController(IProductStore store)
{
_store = store;
}
[HttpGet("in-stock")]
public async Task<List<Product>> InStock() =>
await _store.InStockAsync();
[HttpPut("{id}/stock")]
public async Task<IActionResult> SetStock(
int id, int stock)
{
if (!await _store.SetStockAsync(id, stock))
{
return NotFound();
}
return NoContent();
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
public int Stock { get; set; }
}
public class ShopDbContext : DbContext
{
public ShopDbContext(
DbContextOptions<ShopDbContext> options)
: base(options)
{
}
public DbSet<Product> Products => Set<Product>();
}- Đăng ký trong
Program.cs:builder.Services.AddScoped<IProductStore, DbProductStore>();. Phải làScopedvìDbContextlàScoped, đúng lỗi "Singleton phụ thuộc Scoped" của bài Dependency injection. Where(...).ToListAsync(): điều kiện lọc chạy trong database, chỉ các dòng khớp mới được gửi về.FindAsync(id)tìm theo khoá chính, không có thì trảnull.- Sửa: chỉ cần đổi property rồi gọi
SaveChangesAsync.DbContexttự biết object nào đã đổi và sinh câuUPDATE. SaveChangesAsyncgói mọi câuINSERT,UPDATE,DELETEvào một transaction như bài Transaction của khoá SQL: một câu lỗi thì không câu nào được lưu.- Mọi method đọc ghi database đều có bản
Async, dùng kèmawait. - Giá trị lấy từ biến C# được EF Core gửi bằng bind variable, như bài SQL injection của khoá SQL, nên truy vấn LINQ không bị chèn SQL.
Thử ngay
Bảng PRODUCTS vẫn còn trống. Trong VS Code, kết nối Oracle bằng user
shopapi, mật khẩu shopapi_pw, service name FREEPDB1, rồi thêm ba sản
phẩm mẫu mà các bài sau dùng lại:
INSERT INTO PRODUCTS (NAME, PRICE, STOCK)
VALUES ('Bút bi', 5000, 120);
INSERT INTO PRODUCTS (NAME, PRICE, STOCK)
VALUES ('Vở', 12000, 0);
INSERT INTO PRODUCTS (NAME, PRICE, STOCK)
VALUES ('Thước', 7000, 50);
COMMIT;Để xem câu SQL mà EF Core sinh ra, thêm vào mục Logging:LogLevel của
appsettings.Development.json:
"Microsoft.EntityFrameworkCore.Database.Command": "Information"Chạy server, gọi curl -i http://localhost:5000/api/products/in-stock rồi
xem cửa sổ đang chạy server.
Đoán trước khi chạy: điều kiện Stock > 0 được lọc trong C# sau khi đọc
hết bảng, hay nằm ngay trong câu SQL?
Xem kết quả
SELECT "p"."ID", "p"."NAME", "p"."PRICE", "p"."STOCK"
FROM "PRODUCTS" "p"
WHERE "p"."STOCK" > 0Nằm trong câu SQL. EF Core dịch lambda p => p.Stock > 0 thành
WHERE "p"."STOCK" > 0, nên Oracle chỉ trả về các dòng còn hàng. Response
là 200 kèm hai sản phẩm Bút bi và Thước, không có Vở.
Lỗi hay gặp
Gọi ToListAsync() trước khi lọc. Cả bảng được đọc về bộ nhớ rồi mới lọc.
Bảng có một triệu dòng thì cả một triệu dòng bị đọc vào bộ nhớ.
// SAI — đọc hết bảng rồi mới lọc trong C#
var all = await _db.Products.ToListAsync();
var inStock = all.Where(p => p.Stock > 0).ToList();// ĐÚNG — lọc trong database
var inStock = await _db.Products
.Where(p => p.Stock > 0)
.ToListAsync();Quên SaveChangesAsync(). Add, Remove hay đổi property chỉ được ghi
nhận trong DbContext. Không gọi SaveChangesAsync thì không có gì được ghi xuống
database.
Tóm tắt
- LINQ trên
DbSetđược dịch sang SQL và chạy trong database. - Lọc bằng
Wheretrước, gọiToListAsyncsau cùng. - Thêm, sửa, xoá xong phải gọi
SaveChangesAsyncmới được ghi. - Dùng bản
Asynckèmawaitcho mọi thao tác với database.
Tự kiểm tra
0/3 câuCode: var p = await _db.Products.FindAsync(3); p.Price = 9000m; return Ok(); Giá trong database có đổi không?
Cần 10 đơn hàng mới nhất. Cách nào chỉ lấy đúng 10 dòng từ database?
Vì sao dùng ToListAsync thay vì ToList khi đọc database trong controller?