Quan hệ một-nhiều
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
Một đơn hàng có nhiều dòng hàng: 2 bút, 3 vở. Hai loại dữ liệu này nằm ở hai bảng, nối với nhau qua mã đơn hàng. Bài này khai báo quan hệ đó bằng EF Core và lấy đơn hàng kèm các dòng hàng của nó.
Khái niệm
Quan hệ một-nhiều và khoá ngoại đã học ở bài Khoá ngoại của khoá SQL. Bài này khai báo chúng bằng class.
🧲 Navigation property: property trỏ tới object liên quan, như Order.Lines, để đi từ đơn hàng tới các dòng hàng.
Ví dụ
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public List<OrderLine> Lines { get; set; } =
new List<OrderLine>();
}
public class OrderLine
{
public int Id { get; set; }
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
public class ShopDbContext : DbContext
{
public ShopDbContext(
DbContextOptions<ShopDbContext> options)
: base(options)
{
}
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderLine> OrderLines =>
Set<OrderLine>();
}
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly ShopDbContext _db;
public OrdersController(ShopDbContext db)
{
_db = db;
}
[HttpGet("{id}")]
public async Task<ActionResult<Order>> GetById(
int id)
{
var order = await _db.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null)
{
return NotFound();
}
return Ok(order);
}
}Order.Lineslà navigation property, cho biết một đơn có nhiều dòng.OrderLine.OrderIdlà khoá ngoại. EF Core tự nhận ra nhờ tên theo mẫuTênClass+Id.- Hai class tương ứng với bảng
orders,order_linescủa khoá SQL. Đơn chỉ lưuCustomerId, dòng hàng lưuProductIdvàUnitPricelà giá lúc bán, đúng như bài Chuẩn hoá. - Migration tạo hai bảng
ORDERSvàORDER_LINES, nối với nhau qua cộtORDER_ID, và tự tạo index cho cột này. Bài Index của khoá SQL đã nói Oracle không tự tạo index cho khoá ngoại. Include(o => o.Lines)bảo EF Core đọc luôn các dòng hàng cùng đơn hàng. Câu SQL được sinh ra là mộtLEFT JOINtừORDERSsangORDER_LINES.
Thử ngay
Tạo migration, cập nhật database, rồi thêm action sau vào OrdersController
để tạo một đơn mẫu có hai dòng hàng:
using Microsoft.AspNetCore.Mvc;
// Thêm action này vào trong OrdersController
[HttpPost("sample")]
public async Task<IActionResult> CreateSample()
{
var order = new Order { CustomerId = 1 };
order.Lines.Add(new OrderLine
{
ProductId = 1, Quantity = 2, UnitPrice = 5000m
});
order.Lines.Add(new OrderLine
{
ProductId = 2, Quantity = 3, UnitPrice = 12000m
});
_db.Orders.Add(order);
await _db.SaveChangesAsync();
return Ok(order.Id);
}Chạy server, tạo đơn mẫu rồi đọc lại:
curl -i -X POST http://localhost:5000/api/orders/sample
curl -i http://localhost:5000/api/orders/1Rồi xoá dòng .Include(o => o.Lines) và gọi lại.
Đoán trước khi chạy: bỏ Include thì lines trong response là null,
mảng rỗng, hay vẫn đủ hai dòng?
Xem kết quả
POST sample: 200, body là 1
Có Include: 200 {"id":1,"customerId":1,"lines":[{...},{...}]}
Bỏ Include: 200 {"id":1,"customerId":1,"lines":[]}Mảng rỗng. Không có Include, EF Core chỉ đọc bảng ORDERS. Lines giữ giá
trị khởi tạo là list rỗng, dù trong database vẫn có hai dòng hàng.
Lỗi hay gặp
Quên Include. Không báo lỗi gì, chỉ âm thầm trả về dữ liệu thiếu. Hễ cần
dữ liệu liên quan thì phải Include nó.
Navigation hai chiều khi trả JSON. Thêm Order vào OrderLine để đi
ngược lại, rồi trả thẳng entity ra API. Order chứa Lines, mỗi dòng lại
chứa Order, JSON lặp vô tận và báo lỗi.
// SAI — trả thẳng entity có vòng lặp Order ↔ OrderLine
public class LineWithOrder
{
public int Id { get; set; }
public Order? Order { get; set; }
}Trả dữ liệu qua DTO như bài DTO, chỉ chọn những trường cần gửi, vòng lặp sẽ không xảy ra.
// ĐÚNG — DTO chỉ giữ OrderId, không trỏ ngược
public class LineResponse
{
public int Id { get; set; }
public int OrderId { get; set; }
}Tóm tắt
- Một-nhiều: một đơn hàng có nhiều dòng hàng.
- Khai báo bằng navigation property (
List<OrderLine> Lines) và khoá ngoại (OrderId). - Muốn đọc dữ liệu liên quan thì dùng
Include. - Trả dữ liệu có quan hệ ra API qua DTO để tránh vòng lặp JSON.
Tự kiểm tra
0/3 câuClass Comment có property PostId, class Post có List<Comment> Comments. EF Core hiểu PostId là gì?
Tạo Post mới, thêm hai Comment vào post.Comments (chưa gán PostId), rồi gọi Add(post) và SaveChangesAsync một lần. PostId của hai Comment là gì?
Trả thẳng entity Order có Lines, mà mỗi OrderLine lại có Order, thì chuyện gì xảy ra?