Phần 5: Razor Pages với EF Core trong ASP.NET Core - Tạo Data Model Phức Tạp
Hướng dẫn toàn diện này minh họa cách tạo data model (mô hình dữ liệu) phức tạp cho ứng dụng web Contoso University sử dụng Entity Framework Core và Razor Pages trong ASP.NET Core.
Tổng quan
Hướng dẫn này xây dựng dựa trên các hướng dẫn trước đã giới thiệu một data model cơ bản gồm ba entity (thực thể). Phần này mở rộng model bằng cách:
- Thêm nhiều entity và relationship (quan hệ) hơn
- Tùy chỉnh data model với các quy tắc định dạng, validation (xác thực), và ánh xạ database
- Triển khai các quan hệ many-to-many (nhiều-nhiều) với join table (bảng liên kết)
Các Entity chính
Entity Student
Entity Student bao gồm các thuộc tính với data annotation (chú thích dữ liệu) để validation và định dạng:
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public ICollection<Enrollment> Enrollments { get; set; }
}Giải thích các Attribute (thuộc tính) chính:
[DataType(DataType.Date)]- Chỉ định rằng chỉ nên hiển thị ngày, không hiển thị giờ[DisplayFormat]- Đặt rõ ràng định dạng ngày là yyyy-MM-dd[StringLength]- Giới hạn độ dài chuỗi và cung cấp validation[Column]- Ánh xạ tên thuộc tính sang tên cột database khác[Required]- Làm cho trường bắt buộc[Display]- Tùy chỉnh tên hiển thị trong UI
Entity Instructor
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public ICollection<Course> Courses { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}Navigation properties (thuộc tính điều hướng):
Courses- Collection đại diện cho nhiều khóa học mà instructor có thể dạyOfficeAssignment- Một office assignment (phân công văn phòng) duy nhất (quan hệ one-to-zero-or-one - một-đến-không-hoặc-một)
Entity OfficeAssignment
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}Đặc điểm chính:
- Sử dụng attribute
[Key]để xác địnhInstructorIDlà primary key (khóa chính) InstructorIDcũng đóng vai trò là foreign key (khóa ngoại) tới entityInstructor- Đại diện cho quan hệ one-to-zero-or-one (một instructor có thể không có office assignment)
Entity Course
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Title { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
public int DepartmentID { get; set; }
public Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<Instructor> Instructors { get; set; }
}Các Attribute đáng chú ý:
[DatabaseGenerated(DatabaseGeneratedOption.None)]- PK được cung cấp bởi ứng dụng, không được tự động tạo[Range(0, 5)]- Giới hạn giá trị credit- Foreign key và navigation properties cho các quan hệ
Entity Department
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}Attribute chính:
[Column(TypeName = "money")]- Ánh xạ thuộc tính decimal sang kiểu dữ liệu money của SQL Server
Entity Enrollment
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}Các quan hệ Many-to-Many
Hướng dẫn đề cập hai loại quan hệ many-to-many:
1. Student-to-Course (có payload - dữ liệu bổ sung)
Entity Enrollment đóng vai trò là join table (bảng liên kết) với dữ liệu bổ sung:
- Chứa primary key (
EnrollmentID) - Chứa thuộc tính
Grade(payload) - Kết nối students và courses
2. Instructor-to-Course (pure join table - bảng liên kết thuần túy)
Sử dụng join table để kết nối instructors với courses mà không có dữ liệu bổ sung.
Cấu hình Database Context
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable(nameof(Course))
.HasMany(c => c.Instructors)
.WithMany(i => i.Courses);
modelBuilder.Entity<Student>().ToTable(nameof(Student));
modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
}
}Data Annotations vs. Fluent API
Hướng dẫn giải thích hai cách tiếp cận để cấu hình model:
Data Annotations: Sử dụng trực tiếp trên các thuộc tính
- Chỉ validation (ví dụ:
MinimumLength) - Cấu hình EF Core (ví dụ:
[Column]) - Kết hợp (ví dụ:
[StringLength(50)])
Fluent API: Cấu hình trong OnModelCreating
- Bắt buộc đối với composite primary key (khóa chính tổ hợp)
- Có thể chỉ định hành vi cascade delete (xóa theo tầng)
- Dài hơn nhưng giữ cho các lớp entity sạch
Ví dụ Fluent API cho composite key:
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });Migrations
Hướng dẫn minh họa:
- Tạo migration để áp dụng thay đổi schema:
Add-Migration ColumnFirstName Update-Database
- Xử lý database hiện có với migrations
- Cách thay thế: drop và recreate database
Seeding Database
Phương thức DbInitializer.Initialize cung cấp dữ liệu mẫu:
- 8 sinh viên
- 5 giảng viên
- 4 khoa (departments)
- 7 khóa học
- Office assignments (phân công văn phòng)
- Course-instructor assignments (phân công khóa học-giảng viên)
- Student enrollments (đăng ký sinh viên) với điểm
Các điểm quan trọng
- Data Annotations cung cấp cách khai báo để chỉ định các quy tắc validation và định dạng
- Foreign Keys có thể tường minh hoặc ngầm định; thuộc tính FK tường minh cải thiện hiệu quả cập nhật
- Navigation Properties cho phép quan hệ giữa các entity
- Join Tables có thể có payload (dữ liệu bổ sung) như
Enrollment.Grade - Fluent API xử lý các cấu hình phức tạp mà attribute không thể biểu đạt
- Migrations theo dõi thay đổi schema và có thể được áp dụng hoặc hoàn tác
- Cascade Delete phải được cấu hình cẩn thận để tránh các phụ thuộc vòng tròn
Data model hoàn chỉnh đại diện cho database đại học thực tế với sinh viên, giảng viên, khóa học, khoa, và quản lý đăng ký học.