Entity Framework Code First toàn tập

Code First là gì?

Entity Framework đã giới thiệu cách tiếp cận Code First trong Entity Framework 4.1. Code First chủ yếu hữu ích trong Thiết kế hướng domain.

Trong cách tiếp cận Code First, bạn tập trung vào miền của ứng dụng và bắt đầu tạo các lớp cho thực thể miền thay vì thiết kế cơ sở dữ liệu trước rồi tạo các lớp khớp với thiết kế cơ sở dữ liệu của bạn. Hình dưới đây minh họa cách tiếp cận Code First.

Code First trong Entity Framework

Như bạn có thể thấy trong hình trên, Entity Framework API sẽ tạo cơ sở dữ liệu dựa trên cấu hình và các lớp miền của bạn. Điều này có nghĩa là bạn cần bắt đầu viết code trước bằng C# hoặc VB.NET và sau đó Entity Framework (EF) sẽ tạo cơ sở dữ liệu từ code của bạn.

Quy trình làm việc với Code First

Hình dưới đây minh họa quy trình phát triển Code First.

Quy trình làm việc với Code First

Quy trình phát triển theo cách tiếp cận Code First sẽ là: Tạo hoặc sửa đổi các lớp miền -> cấu hình các lớp miền này bằng các thuộc tính chú thích dữ liệu hoặc Fluent API -> Tạo hoặc cập nhật lược đồ cơ sở dữ liệu bằng automated migration hoặc code-based migration.

Các quy ước trong Code First

Các quy ước là các bộ quy tắc mặc định tự động cấu hình một mô hình khái niệm dựa trên các lớp miền của bạn khi làm việc với cách tiếp cận Code First.

Như bạn đã thấy trong ví dụ ở trên, Entity Framework API đã cấu hình các khóa chính, khóa ngoại, các mối quan hệ, kiểu dữ liệu của cột, v.v. từ các lớp miền mà không cần cấu hình bổ sung.

Điều này là do các quy ước của Entity Framework Code First. Nếu các lớp miền của bạn tuân theo các quy ước thì lược đồ cơ sở dữ liệu sẽ được cấu hình dựa trên các quy ước này.

Các quy ước trong Entity Framework 6.x Code First được định nghĩa trong namespace System.Data.Entity.ModelConfiguration.Conventions .

Bảng sau liệt kê các quy ước mặc định của Code First:

Quy ước Miêu tả
Lược đồ Theo mặc định, EF tạo tất cả các đối tượng DB vào lược đồ dbo .
Tên bảng <Tên lớp thực thể> + 's'
EF sẽ tạo bảng DB với tên lớp thực thể thêm 's' ở cuối, ví dụ: lớp Student sẽ ánh xạ tới bảng Students.
Tên khóa chính 1) Id
2) <Tên lớp thực thể> + "Id" (không phân biệt chữ hoa chữ thường)

EF sẽ tạo cột khóa chính cho thuộc tính có tên Id hoặc <Tên lớp thực thể> + "Id" (không phân biệt chữ hoa chữ thường).
Tên khóa ngoại Theo mặc định, EF sẽ tìm thuộc tính khóa ngoại có cùng tên với tên khóa chính của thực thể chính.
Nếu thuộc tính khóa ngoại không tồn tại, thì EF sẽ tạo cột khóa ngoại trong bảng Db với <Tên thuộc tính điều hướng> + "_" + <Tên thuộc tính khóa chính của thực thể chính>
ví dụ: EF sẽ tạo cột khóa ngoại Grade_GradeId trong bảng Students nếu thực thể Student không chứa thuộc tính khóa ngoại cho Grade.
Cột Null EF tạo một cột null cho tất cả các thuộc tính kiểu tham chiếu và các thuộc tính kiểu nguyên thủy nullable, ví dụ: string, Nullable <int>, Student, Grade (tất cả các thuộc tính kiểu lớp)
Cột Not Null EF tạo các cột Not Null cho các thuộc tính khóa chính và các thuộc tính kiểu giá trị không nullable, ví dụ: int, float, binary, datetime, v.v.
Thứ tự cột EF sẽ tạo các cột theo cùng thứ tự như các thuộc tính trong một lớp thực thể. Tuy nhiên, các cột khóa chính sẽ được di chuyển lên đầu tiên.
Ánh xạ thuộc tính vào DB Theo mặc định, tất cả các thuộc tính sẽ ánh xạ tới cơ sở dữ liệu. Sử dụng thuộc tính [NotMapped] để loại trừ thuộc tính hoặc lớp không ánh xạ vào DB.
Cascade delete Được bật theo mặc định cho tất cả các loại mối quan hệ.

Bảng sau liệt kê kiểu dữ liệu C# được ánh xạ với kiểu dữ liệu SQL Server.

Kiểu dữ liệu C# Ánh xạ tới kiểu dữ liệu SQL Server
int int
string nvarchar(Max)
decimal decimal(18,2)
float real
byte[] varbinary(Max)
datetime datetime
bool bit
byte tinyint
short smallint
long bigint
double float
char Không ánh xạ
sbyte Không ánh xạ (ném ra exception)
object Không ánh xạ

Hình dưới đây minh họa ánh xạ quy ước với cơ sở dữ liệu.

Các quy ước trong Code First

Quy ước về mối quan hệ

Entity Framework 6 tạo mối quan hệ một-nhiều bằng cách sử dụng thuộc tính điều hướng theo quy ước mặc định. Phần này sẽ được trình bày chi tiết trong bài viết khác.

Lưu ý: Entity Framework 6 không có các quy ước mặc định cho các mối quan hệ một-một và nhiều-nhiều. Bạn cần cấu hình chúng bằng Fluent API hoặc DataAnnotation.

Quy ước kiểu phức tạp

Code First tạo kiểu phức tạp cho lớp không bao gồm thuộc tính khóa và khóa chính không được đăng ký bằng thuộc tính DataAnnotation hoặc Fluent API.

Phần này trình bày tổng quan về các quy ước trong Code First. Các quy ước này có thể được ghi đè bằng các sử dụng thuộc tính DataAnnotation hoặc Fluent API.

Khởi tạo cơ sở dữ liệu trong Code First

Ở phần này, chúng ta sẽ tìm hiểu cách Entity Framework quyết định tên cơ sở dữ liệu và máy chủ trong khi khởi tạo cơ sở dữ liệu theo cách tiếp cận Code First.

Hình dưới đây cho thấy một quy trình khởi tạo cơ sở dữ liệu, dựa trên tham số được truyền trong phương thức khởi tạo cơ sở của lớp Context được kế thừa từ lớp DbContext:

Khởi tạo cơ sở dữ liệu trong Entity Framework

Tham số khởi tạo cơ sở dữ liệu

Theo hình trên, phương thức khởi tạo cơ sở của lớp Context có thể có tham số sau:

  1. Không có tham số.
  2. Tên cơ sở dữ liệu.
  3. Tên chuỗi kết nối.

Không có tham số

Nếu bạn không chỉ định tham số trong phương thức khởi tạo cơ sở của lớp Context thì nó sẽ tạo cơ sở dữ liệu trong máy chủ SQLEXPRESS cục bộ của bạn với tên khớp với {Namespace}. {Tên lớp Context}.

Ví dụ, Entity Framework (EF) sẽ tạo một cơ sở dữ liệu có tên SchoolDataLayer.Context cho lớp Context sau:

namespace SchoolDataLayer
{
    public class Context: DbContext 
    {
        public Context(): base()
        { }
    }
}

Tên cơ sở dữ liệu

Bạn cũng có thể chỉ định tên cơ sở dữ liệu làm tham số trong phương thức khởi tạo cơ sở của lớp Context.

Nếu bạn chỉ định tham số tên cơ sở dữ liệu, thì Code First sẽ tạo cơ sở dữ liệu với tên bạn đã chỉ định trong phương thức khởi tạo cơ sở trong máy chủ cơ sở dữ liệu SQLEXPRESS cục bộ.

Ví dụ, Code First sẽ tạo cơ sở dữ liệu có tên MySchoolDB cho lớp Context sau.

namespace SchoolDataLayer
{
    public class Context: DbContext 
    {
        public Context(): base("MySchoolDB") 
        { }
    }
}

Tên chuỗi kết nối

Bạn cũng có thể chỉ định chuỗi kết nối trong app.config hoặc web.config và chỉ định tên chuỗi kết nối bắt đầu bằng "name =" trong phương thức khởi tạo cơ sở của lớp Context.

Hãy xem xét ví dụ sau nơi chúng ta truyền tham số name=SchoolDBConnectionString trong phương thức khởi tạo cơ sở.

namespace SchoolDataLayer
{
    public class Context: DbContext 
    {
        public SchoolDBContext() : base("name=SchoolDBConnectionString") 
        {
        }
    }
}

App.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="SchoolDBConnectionString" 
             connectionString="Data Source=.;Initial Catalog=SchoolDB;Integrated Security=true" 
             providerName="System.Data.SqlClient"/>
    </connectionStrings>
</configuration>

Trong lớp Context ở trên, chúng tôi chỉ định tên chuỗi kết nối làm tham số. Xin lưu ý rằng tên chuỗi kết nối phải bắt đầu bằng "name =", nếu không, nó sẽ coi nó là tên cơ sở dữ liệu.

Tên cơ sở dữ liệu trong chuỗi kết nối trong App.config là SchoolDB. Entity Framework (EF) sẽ tạo cơ sở dữ liệu SchoolDB mới hoặc sử dụng cơ sở dữ liệu SchoolDB hiện có trong SQL Server cục bộ.

Đảm bảo rằng bạn có thuộc tính providerName="System.Data.SqlClient" cho cơ sở dữ liệu SQL Server trong chuỗi kết nối.

Chiến lược khởi tạo cơ sở dữ liệu

Bạn đã tạo cơ sở dữ liệu sau khi chạy ứng dụng Code First lần đầu tiên, nhưng lần thứ hai trở đi thì sao? Nó sẽ tạo ra một cơ sở dữ liệu mới mỗi khi bạn chạy ứng dụng?

Còn môi trường Production thì sao? Làm thế nào để bạn cập nhật cơ sở dữ liệu khi bạn thay đổi mô hình miền của bạn?

Để xử lý các kịch bản này, bạn phải sử dụng một trong các chiến lược khởi tạo cơ sở dữ liệu.

Có bốn chiến lược khởi tạo cơ sở dữ liệu khác nhau trong Code First:

  1. CreateDatabaseIfNotExists: Đây là trình khởi tạo mặc định. Như tên gọi, nó sẽ tạo cơ sở dữ liệu nếu không tồn tại theo cấu hình. Tuy nhiên, nếu bạn thay đổi lớp mô hình và sau đó chạy ứng dụng với trình khởi tạo này, thì nó sẽ đưa ra một ngoại lệ.
  2. DropCreateDatabaseIfModelChanges: Trình khởi tạo này xóa cơ sở dữ liệu hiện có và tạo cơ sở dữ liệu mới, nếu các lớp mô hình của bạn (các lớp thực thể) đã bị thay đổi. Vì vậy, bạn không phải lo lắng về việc duy trì lược đồ cơ sở dữ liệu của mình, khi các lớp mô hình của bạn thay đổi.
  3. DropCreateDatabaseAlways: Như tên cho thấy, trình khởi tạo này sẽ xóa cơ sở dữ liệu hiện có mỗi khi bạn chạy ứng dụng, bất kể các lớp mô hình của bạn có thay đổi hay không. Điều này sẽ hữu ích khi bạn muốn có một cơ sở dữ liệu mới mỗi khi bạn chạy ứng dụng, ví dụ như khi bạn đang phát triển ứng dụng.
  4. Trình khởi tạo cơ sở dữ liệu tùy chỉnh: Bạn cũng có thể tạo trình khởi tạo tùy chỉnh của riêng mình, nếu các cách trên không đáp ứng yêu cầu của bạn hoặc bạn muốn thực hiện một số quy trình khác để khởi tạo cơ sở dữ liệu bằng trình khởi tạo ở trên.

Xem thêm về khởi tạo cơ sở dữ liệu trong Code First tại đây:

Khởi tạo cơ sở dữ liệu trong Entity Framework | Comdy
Cách Code First tự động khởi tạo cơ sở dữ liệu trong Entity Framework. Các chiến lược khởi tạo cơ sở dữ liệu trong EF.

Chiến lược kế thừa trong Code First

Bạn có thể thiết kế các lớp thực thể của mình bằng cách sử dụng tính kế thừa trong C#.

Trong lập trình hướng đối tượng, các lớp có mối quan hệ "has a" và "is a", trong khi  đó mô hình quan hệ dựa trên SQL chỉ có mối quan hệ "has a" giữa các bảng.

Hệ quản trị cơ sở dữ liệu SQL không hỗ trợ mối quan hệ "is a". Vì vậy, làm thế nào bạn có thể ánh xạ các lớp thực thể có mối quan hệ "is a" vào cơ sở dữ liệu quan hệ?

Dưới đây là ba cách tiếp cận khác nhau để ánh xạ kế thừa vào cơ sở dữ liệu trong Code First:

  • Table per Hierarchy (TPH): Cách tiếp cận này đề nghị tạo một bảng chung cho toàn bộ các lớp trong phân cấp kế thừa. Bảng này có một cột để phân biệt giữa các lớp con. Đây là một chiến lược ánh xạ kế thừa mặc định trong Entity Framework.
  • Table per Type (TPT): Cách tiếp cận này đề nghị tạo mỗi bảng cho từng lớp trong phân cấp kế thừa (tạo bảng cho cả lớp cha và lớp con).
  • Table per Concrete Class (TPC): Cách tiếp cận này đề nghị tạo mỗi bảng cho từng lớp con trong phân cấp kế thừa, nhưng không tạo bảng cho lớp cha. Vì vậy các thuộc tính của lớp cha sẽ là một phần của mỗi bảng của lớp con.

Xem chi tiết ba cách tiếp cạn để anh xạ kế thừa vào cơ sở dữ liệu trong Code First:

Chiến lược kế thừa trong Entity Framework | Comdy
Chiến lược kế thừa trong EF Code First là gì? Có những chiến lược kế thừa nào trong EF Code First.

Cấu hình các lớp trong Entity Framework

Code First xây dựng mô hình khái niệm từ các lớp thực thể của bạn bằng cách sử dụng các quy ước mặc định.

EF 6 Code First tận dụng một mẫu lập trình được gọi là quy ước về cấu hình. Tuy nhiên, bạn có thể ghi đè các quy ước này bằng cách cấu hình các lớp thực thể của bạn để cung cấp cho EF thông tin cần thiết.

Có hai cách để cấu hình các lớp thực thể của bạn:

  1. Attribute chú thích dữ liệu.
  2. Fluent API.

Attribute chú thích dữ liệu

Chú thích dữ liệu là một cấu hình dựa trên attribute đơn giản, bạn có thể áp dụng cho các lớp và thuộc tính của nó.

Các attribute này không chỉ dành riêng cho EF mà còn được sử dụng trong ASP.NET Web Form và ASP.NET MVC. Do đó, chúng được đặt trong một namespace riêng biệt là System.ComponentModel.DataAnnotations.

Ví dụ sau đây minh họa việc sử dụng một số attribute chú thích dữ liệu:

[Table("StudentInfo")]
public class Student
{
    public Student() { }
        
    [Key]
    public int SID { get; set; }

    [Column("Name", TypeName="ntext")]
    [MaxLength(20)]
    public string StudentName { get; set; }

    [NotMapped]
    public int? Age { get; set; }        
        
    public int StdId { get; set; }

    [ForeignKey("StdId")]
    public virtual Standard Standard { get; set; }
}
Lưu ý: Attribute chú thích dữ liệu không hỗ trợ tất cả các tùy chọn cấu hình cho Entity Framework. Vì vậy, bạn có thể sử dụng Fluent API, nó cung cấp tất cả các tùy chọn cấu hình cho EF.

Tìm hiểu chi tiết về attribute chú thích dữ liệu trong Entity Framework tại bài viết này:

Attribute chú thích dữ liệu trong Entity Framework | Comdy
Attribute chú thích dữ liệu trong Entity Framework là gì? Có những loại attribute chú thích dữ liệu nào?

Fluent API

Một cách khác để định cấu hình các lớp là bằng cách sử dụng Entity Framework Fluent API. Fluent API dựa trên mẫu thiết kế Fluent API (hay còn gọi là giao diện thông thạo) trong đó kết quả được tạo thành từ một chuỗi phương thức.

Cấu hình Fluent API có thể được áp dụng khi EF xây dựng mô hình từ các lớp thực thể của bạn. Bạn có thể thêm các cấu hình Fluent API bằng cách ghi đè phương thức OnModelCreating của lớp DbContext trong Entity Framework 6.x, như được trình bày bên dưới:

public class SchoolDBContext: DbContext 
{
    public SchoolDBContext(): base("SchoolDBConnectionString") 
    {
    }

    public DbSet<Student> Students { get; set; }
    public DbSet<Standard> Standards { get; set; }
    public DbSet<StudentAddress> StudentAddress { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //Configure domain classes using modelBuilder here..
    }
}

Bạn có thể sử dụng tham số modelBuilder, một đối tượng của lớp DbModelBuilder để cấu hình các lớp thực thể của bạn. DbModelBuilder được gọi là Fluent API vì bạn có thể gọi các phương thức khác nhau trong một chuỗi phương thức.

Tìm hiểu chi tiết về Fluent API trong Entity Framework tại bài viết này:

Fluent API trong Entity Framework | Comdy
Fluent API trong Entity Framework là gì? Làm sao để khai báo và sử dụng Fluent API trong Entity Framework?

Cấu hình mối quan hệ trong Code First

Trong cơ sở dữ liệu quan hệ, một mối quan hệ là một liên kết tồn tại giữa các bảng của cơ sở dữ liệu quan hệ thông qua các khóa ngoại.

Khóa ngoại (Foreign Key) là một cột hoặc tổ hợp các cột được sử dụng để thiết lập và thực thi một liên kết giữa dữ liệu trong hai bảng.

Có ba loại mối quan hệ giữa các bảng và chúng khác nhau tùy thuộc vào cách định nghĩa các cột liên quan.

  • Mối quan hệ một-nhiều
  • Mối quan hệ nhiều-nhiều
  • Mối quan hệ một-một

Để hiểu rõ cách cấu hình các mối quan hệ giữa các thực thể trong Entity Framwork Code First bạn có thể xem bài viết:

Cấu hình mối quan hệ trong Entity Framework | Comdy
Hướng dẫn cấu hình các mối quan hệ một-một, một-nhiều, nhiều-nhiều trong Entity Framework Code First.

Migration trong Code First

Entity Framework Code First có các chiến lược khởi tạo cơ sở dữ liệu khác nhau như CreateDatabaseIfNotExists, DropCreateDatabaseIfModelChanges, và DropCreateDatabaseAlways.

Tuy nhiên, có một số vấn đề với các chiến lược này, ví dụ: nếu bạn đã có dữ liệu (trừ seed data - dữ liệu ban đầu được thêm bằng code) hoặc stored procedures, triggers, v.v. trong cơ sở dữ liệu của bạn.

Các chiến lược này được sử dụng để loại bỏ toàn bộ cơ sở dữ liệu và tạo lại nó, do đó bạn sẽ mất dữ liệu và các đối tượng DB khác.

Entity Framework đã giới thiệu một công cụ migration tự động cập nhật lược đồ cơ sở dữ liệu khi mô hình của bạn thay đổi mà không mất bất kỳ dữ liệu hiện có hoặc các đối tượng cơ sở dữ liệu khác. Nó sử dụng một trình khởi tạo cơ sở dữ liệu mới gọi là MigrateDatabaseToLatestVersion.

Có hai loại Migration:

  1. Migration tự động.
  2. Migration dựa trên mã.

Để hiểu rõ về hai loại Migration trong Code First bạn có thể tham khảo bài viết sau:

Migration trong Entity Framework | Comdy
Migration trong Entity Framework là gì? Làm sao để cấu hình sử dụng Migration trong EF Code First.

Tạo Context và lớp thực thể từ database trong Code First

Ở phần này, bạn sẽ tìm hiểu cách tạo Context và các lớp thực thể từ cơ sở dữ liệu có sẵn, sử dụng phương pháp tiếp cận Code First.

Entity Framework cung cấp một cách đơn giản để sử dụng Code First cho cơ sở dữ liệu có sẵn. Nó sẽ tạo các lớp thực thể cho tất cả các bảng và view trong cơ sở dữ liệu hiện tại của bạn và cấu hình chúng với các attribute chú thích dữ liệu và Fluent API.

Để sử dụng Code First cho cơ sở dữ liệu có sẵn, nhấp chuột phải vào dự án của bạn trong Visual Studio -> Add -> New Item..

Tạo Context và lớp thực thể từ database trong Code First

Chọn ADO.NET Entity Data Model trong hộp thoại Add New Item và đặt tên cho mô hình (đây sẽ là tên lớp Context) rồi nhấp vào nút Add.

Tạo Context và lớp thực thể từ database trong Code First

Điều này sẽ mở trình hướng dẫn Entity Data Model như dưới đây. Chọn Code First from database và nhấn Next.

Tạo Context và lớp thực thể từ database trong Code First

Bây giờ, chọn kết nối dữ liệu cho cơ sở dữ liệu hiện có. Tạo kết nối mới cho cơ sở dữ liệu của bạn nếu danh sách thả xuống không bao gồm kết nối đến cơ sở dữ liệu hiện tại của bạn. Nhấn Next để tiếp tục.

Tạo Context và lớp thực thể từ database trong Code First

Bây giờ, chọn các bảng và view mà bạn muốn tạo các lớp thực thể và nhấp vào Finish.

Tạo Context và lớp thực thể từ database trong Code First

Điều này sẽ tạo ra tất cả các lớp thực thể cho các bảng và view trong cơ sở dữ liệu của bạn như được hiển thị bên dưới.

Tạo Context và lớp thực thể từ database trong Code First

Nó cũng sẽ tạo lớp Context sau sử dụng Fluent API để cấu hình các lớp thực thể theo cơ sở dữ liệu của bạn.


namespace EFDemo
{
    using System;
    using System.Data.Entity;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Linq;

    public partial class SchoolContext : DbContext
    {
        public SchoolContext()
            : base("name=SchoolContext2")
        {
        }

        public virtual DbSet<Course> Courses { get; set; }
        public virtual DbSet<Standard> Standards { get; set; }
        public virtual DbSet<Student> Students { get; set; }
        public virtual DbSet<StudentAddress> StudentAddresses { get; set; }
        public virtual DbSet<Teacher> Teachers { get; set; }
        public virtual DbSet<View_StudentCourse> View_StudentCourse { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>()
                .Property(e => e.CourseName)
                .IsUnicode(false);

            modelBuilder.Entity<Course>()
                .HasMany(e => e.Students)
                .WithMany(e => e.Courses)
                .Map(m => m.ToTable("StudentCourse").MapLeftKey("CourseId").MapRightKey("StudentId"));

            modelBuilder.Entity<Standard>()
                .Property(e => e.StandardName)
                .IsUnicode(false);

            modelBuilder.Entity<Standard>()
                .Property(e => e.Description)
                .IsUnicode(false);

            modelBuilder.Entity<Standard>()
                .HasMany(e => e.Students)
                .WithOptional(e => e.Standard)
                .WillCascadeOnDelete();

            modelBuilder.Entity<Standard>()
                .HasMany(e => e.Teachers)
                .WithOptional(e => e.Standard)
                .WillCascadeOnDelete();

            modelBuilder.Entity<Student>()
                .Property(e => e.StudentName)
                .IsUnicode(false);

            modelBuilder.Entity<Student>()
                .Property(e => e.RowVersion)
                .IsFixedLength();

            modelBuilder.Entity<Student>()
                .HasOptional(e => e.StudentAddress)
                .WithRequired(e => e.Student)
                .WillCascadeOnDelete();

            modelBuilder.Entity<StudentAddress>()
                .Property(e => e.Address1)
                .IsUnicode(false);

            modelBuilder.Entity<StudentAddress>()
                .Property(e => e.Address2)
                .IsUnicode(false);

            modelBuilder.Entity<StudentAddress>()
                .Property(e => e.City)
                .IsUnicode(false);

            modelBuilder.Entity<StudentAddress>()
                .Property(e => e.State)
                .IsUnicode(false);

            modelBuilder.Entity<Teacher>()
                .Property(e => e.TeacherName)
                .IsUnicode(false);

            modelBuilder.Entity<Teacher>()
                .HasMany(e => e.Courses)
                .WithOptional(e => e.Teacher)
                .WillCascadeOnDelete();

            modelBuilder.Entity<View_StudentCourse>()
                .Property(e => e.StudentName)
                .IsUnicode(false);

            modelBuilder.Entity<View_StudentCourse>()
                .Property(e => e.CourseName)
                .IsUnicode(false);
        }
    }
}

Cascade Delete trong Code First

Cascade Delete sẽ tự động xóa các bản ghi phụ thuộc hoặc thiết lập các cột khóa ngoại thành null khi bản ghi cha bị xóa trong cơ sở dữ liệu.

Cascade Delete được bật theo mặc định trong Entity Framework cho tất cả các loại mối quan hệ, chẳng hạn như một-một, một-nhiều và nhiều-nhiều.

Cascade Delete trong mối quan hệ một-một

Hãy xem ví dụ sau đây: các thực thể StudentStudentAddress có mối quan hệ một-một.


public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }

    public virtual StudentAddress Address { get; set; }
}
     
public class StudentAddress 
{
    [ForeignKey("Student")]
    public int StudentAddressId { get; set; }
    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string City { get; set; }
    public int Zipcode { get; set; }
    public string State { get; set; }
    public string Country { get; set; }

    public virtual Student Student { get; set; }
}

Ví dụ sau đây minh họa cascade delete.


using (var ctx = new SchoolContext()) 
{
    var stud = new Student() { StudentName = "James" };
    var add = new StudentAddress() { Address1 = "address" };

    stud.Address = add;

    ctx.Students.Add(stud);

    ctx.SaveChanges();
    
    ctx.Students.Remove(stud);// student and its address will be removed from db

    ctx.SaveChanges();
}

Trong ví dụ trên, đầu tiên EF lưu đối tượng stud của thực thể Student và đối tượng add của thực thể StudentAddress của nó vào cơ sở dữ liệu.

Sau đó, khi xóa đối tượng stud và gọi phương thức SaveChanges(), EF sẽ xóa stud cũng như bản ghi tương ứng của nó trong bảng StudentAddresses.

Do đó, EF cho phép cascade delete theo mặc định.

Cascade Delete trong mối quan hệ một-nhiều

Hãy xem ví dụ sau đây: các thực thể StudentStandard có mối quan hệ một-nhiều.


public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }

    public virtual Standard Standard { get; set; }
}
       
public class Standard
{
    public Standard()
    {
        Students = new List<Student>();
    }
    public int StandardId { get; set; }
    public string Description { get; set; }

    public virtual ICollection<Student> Students { get; set; }
}

Ví dụ sau đây minh họa cascade delete giữa các thực thể có mối quan hệ một-nhiều:


using (var ctx = new SchoolContext()) 
{

    var student1 = new Student() { StudentName = "James" };
    var student2 = new Student() { StudentName = "Gandhi" };

    var standard1 = new Standard() { StandardName = "Standard 1" };

    student1.Standard = standard1;
    student2.Standard = standard1;

    ctx.Students.Add(student1);
    ctx.Students.Add(student2);
                
    //inserts students and standard1 into db
    ctx.SaveChanges();

    //deletes standard1 from db and also set standard_StandardId FK column in Students table to null for
    // all the students that reference standard1.
    ctx.Standards.Remove(standard1);

    ctx.SaveChanges();
}

Trong ví dụ trên, EF xóa đối tượng standard1 khỏi cơ sở dữ liệu và nó cũng thiết lập cột khóa ngoại standard_StandardId trong bảng Students thành null cho tất cả các bản ghi tham chiếu đối tượng standard1.

Lưu ý: EF tự động xóa các bản ghi liên quan trong bảng ở giữa cho các thực thể có mối quan hệ nhiều-nhiều nếu một thực thể bị xóa.

Do đó, EF cho phép cascade delete mặc định cho tất cả các thực thể.

Tắt Cascade Delete

Sử dụng Fluent API để cấu hình tắt cascade delete cho các thực thể để bằng phương thức WillCascadeOnDelete(), như ví dụ bên dưới.


public class SchoolContext<: DbContext
{
    public SchoolContext():base("MySchool")
    {
    }

    public DbSet<Student> Students { get; set; }
    public DbSet<Standard> Standards { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>()
            .HasOptional<Standard>(s => s.Standard)
            .WithMany()
            .WillCascadeOnDelete(false);
    }
}
Lưu ý: Không có attribute chú thích dữ liệu nào có sẵn để tắt cascade delete.

Sử dụng Stored Procedures trong Code First

Entity Framework 6 Code First cung cấp khả năng tạo và sử dụng stored procedure để thực hiện các thao tác thêm, cập nhật và xóa khi gọi phương thức SaveChanges().

Chúng ta hãy sử dụng các stored procedure cho các thao tác CUD (Create, Update, Delete) cho thực thể Student sau.


class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }
    public DateTime DoB { get; set; }
}

Sử dụng phương thức MapToStoredProcedures() để ánh xạ một thực thể với các stored procedure mặc định (các stored procedure mặc định này sẽ được tạo bởi EF API). Ví dụ sau ánh xạ thực thể Student với các stored procedure mặc định.


public class SchoolContext: DbContext 
{
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>()
                    .MapToStoredProcedures();
    }

    public DbSet<Student> Students { get; set; }
}

EF API sẽ tạo ra ba stored procedure Student_Insert, Student_UpdateStudent_Delete cho thực thể Student ở trên, như hình dưới đây.

Sử dụng Stored Procedures trong Code First

Các stored procedure Student_InsertStudent_Update có các tham số cho tất cả các thuộc tính của thực thể Student. Riêng stored procedure Student_Delete chỉ có một tham số cho thuộc tính khóa chính StudentId của thực thể Student. Sau đây là mã của các stored procedure.


CREATE PROCEDURE [dbo].[Student_Insert]
    @StudentName [nvarchar](max),
    @DoB [datetime]
AS
BEGIN
    INSERT [dbo].[Students]([StudentName], [DoB])
    VALUES (@StudentName, @DoB)
    
    DECLARE @StudentId int
    SELECT @StudentId = [StudentId]
    FROM [dbo].[Students]
    WHERE @@ROWCOUNT > 0 AND [StudentId] = scope_identity()
    
    SELECT t0.[StudentId]
    FROM [dbo].[Students] AS t0
    WHERE @@ROWCOUNT > 0 AND t0.[StudentId] = @StudentId
END

CREATE PROCEDURE [dbo].[Student_Update]
    @StudentId [int],
    @StudentName [nvarchar](max),
    @DoB [datetime]
AS
BEGIN
    UPDATE [dbo].[Students]
    SET [StudentName] = @StudentName, [DoB] = @DoB
    WHERE ([StudentId] = @StudentId)
END

CREATE PROCEDURE [dbo].[Student_Delete]
    @StudentId [int]
AS
BEGIN
    DELETE [dbo].[Students]
    WHERE ([StudentId] = @StudentId)
END

Ánh xạ Stored procedure tùy chỉnh cho một thực thể

EF 6 cho phép bạn sử dụng các stored procedure tùy chỉnh của riêng bạn và ánh xạ chúng tới một thực thể. Bạn cũng có thể cấu hình ánh xạ tham số với các thuộc tính của thực thể.

Ví dụ sau ánh xạ các stored procedure tùy chỉnh với thực thể Student.


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>()
		.MapToStoredProcedures(p => p.Insert(sp => sp.HasName("sp_InsertStudent").Parameter(pm => pm.StudentName, "name").Result(rs => rs.StudentId, "Id"))
			.Update(sp => sp.HasName("sp_UpdateStudent").Parameter(pm => pm.StudentName, "name"))
			.Delete(sp => sp.HasName("sp_DeleteStudent").Parameter(pm => pm.StudentId, "Id"))
            );
}

Trong ví dụ trên, thực thể Student được ánh xạ tới các stored procedure sp_InsertStudent, sp_UpdateStudentsp_DeleteStudent. Nó cũng cấu hình ánh xạ giữa các tham số và thuộc tính của thực thể.

Sử dụng stored procedure cho tất cả các thực thể

Bạn có thể ánh xạ tất cả các thực thể của mình với các stored procedure mặc định trong một câu lệnh như dưới đây.


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Types().Configure(t => t.MapToStoredProcedures());
}

Hạn chế

  • Chỉ có thể sử dụng Fluent API để ánh xạ các stored procedure. Không có attribute chú thích dữ liệu nào có sẵn trong EF 6 để ánh xạ stored procedure.
  • Bạn phải ánh xạ các stored procedure thêm, cập nhật và xóa cho một thực thể nếu bạn muốn sử dụng stored procedure cho các hoạt động CUD. Ánh xạ chỉ một trong số các thao tác CUD là không được phép.

Chuyển cấu hình Fluent API sang một lớp riêng trong Code First

Như bạn đã thấy trong các hướng dẫn trước, chúng tôi đã cấu hình tất cả các lớp thực thể bằng Fluent API trong phương thức OnModelCreating().

Tuy nhiên, nó trở nên khó duy trì nếu bạn cấu hình một số lượng lớn các lớp thực thể trong OnModelCreating.

EF 6 cho phép bạn tạo một lớp riêng cho từng thực thể và đặt tất cả các cấu hình liên quan đến thực thể trong lớp đó.

Hãy xem ví dụ sau nơi chúng ta cấu hình thực thể Student.


public class SchoolDBContext: DbContext 
{
    public SchoolDBContext(): base() 
    {
    }

    public DbSet<Student> Students { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>().ToTable("StudentInfo");
            
        modelBuilder.Entity<Student>().HasKey<int>(s => s.StudentKey);
            
        modelBuilder.Entity<Student>()
            .Property(p => p.DateOfBirth)
            .HasColumnName("DoB")
            .HasColumnOrder(3)
            .HasColumnType("datetime2");

        modelBuilder.Entity<Student>()
            .Property(p => p.StudentName)
            .HasMaxLength(50);
                    
        modelBuilder.Entity<Student>()
            .Property(p => p.StudentName)
            .IsConcurrencyToken();
            
        modelBuilder.Entity<Student>()
            .HasMany<Course>(s => s.Courses)
            .WithMany(c => c.Students)
            .Map(cs =>
            {
                cs.MapLeftKey("StudentId");
                cs.MapRightKey("CourseId");
                cs.ToTable("StudentCourse");
            });
    }
}

Bây giờ, bạn có thể di chuyển tất cả các cấu hình liên quan đến thực thể Student sang một lớp riêng có nguồn gốc từ EntityTypeConfiguration<TEntity>.

Hãy xem lớp StudentEntityConfigurations sau bao gồm tất cả các cấu hình cho thực thể Student.


public class StudentEntityConfiguration: EntityTypeConfiguration<Student>
{
    public StudentEntityConfiguration()
    {
            this.ToTable("StudentInfo");
                
            this.HasKey<int>(s => s.StudentKey);
                
            this.Property(p => p.DateOfBirth)
                .HasColumnName("DoB")
                .HasColumnOrder(3)
                .HasColumnType("datetime2");

            this.Property(p => p.StudentName)
                .HasMaxLength(50);
                        
            this.Property(p => p.StudentName)
                .IsConcurrencyToken();
                
            this.HasMany<Course>(s => s.Courses)
                .WithMany(c => c.Students)
                .Map(cs =>
                {
                    cs.MapLeftKey("StudentId");
                    cs.MapRightKey("CourseId");
                    cs.ToTable("StudentCourse");
                });
    }
}

Như bạn có thể thấy ở trên, chúng tôi đã chuyển tất cả các cấu hình cho thực thể Student vào phương thức khởi tạo của lớp StudentEntityConfiguration, có nguồn gốc từ EntityTypeConfiguration<Student>.

Bây giờ, bạn cần thêm lớp cấu hình tùy chỉnh này sử dụng Fluent API, như ví dụ bên dưới.


public class SchoolDBContext: DbContext 
{
    public SchoolDBContext(): base() 
    {
    }

    public DbSet<Student> Students { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Moved all Student related configuration to StudentEntityConfiguration class
        modelBuilder.Configurations.Add(new StudentEntityConfiguration());
    }
}

Do đó, bạn có thể sử dụng nhiều lớp cấu hình để tăng khả năng đọc và bảo trì.

Seed Data trong Code-First

Bạn có thể chèn dữ liệu vào các bảng cơ sở dữ liệu của mình trong quá trình khởi tạo cơ sở dữ liệu. Điều này sẽ rất quan trọng nếu bạn muốn cung cấp một số dữ liệu thử nghiệm cho ứng dụng của bạn hoặc một số dữ liệu chính mặc định cho ứng dụng của bạn.

Để tạo dữ liệu vào cơ sở dữ liệu của bạn, bạn phải tạo một trình khởi tạo DB tùy chỉnh, như bạn đã tạo trong chương Khởi tạo cơ sở dữ liệu và ghi đè phương thức Seed.

Ví dụ sau đây cho thấy cách bạn có thể cung cấp dữ liệu mặc định cho bảng Standard trong khi khởi tạo cơ sở dữ liệu SchoolDB:


public class SchoolDBInitializer : DropCreateDatabaseAlways<SchoolDBContext>
{
    protected override void Seed(SchoolDBContext context)
    {
        IList<Standard> defaultStandards = new List<Standard>();

        defaultStandards.Add(new Standard() { StandardName = "Standard 1", Description = "First Standard" });
        defaultStandards.Add(new Standard() { StandardName = "Standard 2", Description = "Second Standard" });
        defaultStandards.Add(new Standard() { StandardName = "Standard 3", Description = "Third Standard" });

        context.Standards.AddRange(defaultStandards);

        base.Seed(context);
    }
}

Bây giờ, thiết lập lớp khởi tạo DB này trong lớp Context như bên dưới.


public class SchoolContext: DbContext 
{
    public SchoolContext(): base("SchoolDB") 
    {
        Database.SetInitializer(new SchoolDBInitializer());
    }
    
    public DbSet<Student> Students { get; set; }
    public DbSet<Standard> Standards { get; set; }
}

Tạo các quy ước tùy chỉnh trong Code First

Bạn đã tìm hiểu về các quy ước trong Code First trong phần đầu của bài viết. EF 6 cũng cung cấp khả năng định nghĩa các quy ước tùy chỉnh của riêng bạn.

Có hai loại quy ước chính: Quy ước cấu hình và Quy ước mô hình.

Quy ước cấu hình

Quy ước cấu hình là một cách để cấu hình các thực thể mà không ghi đè cấu hình mặc định được cung cấp trong Fluent API.

Bạn có thể định nghĩa một quy ước cấu hình trong phương thức OnModelCreating() và cả trong lớp tùy chỉnh, tương tự như cách bạn sẽ định nghĩa ánh xạ thực thể thông thường với Fluent API.

Ví dụ: bạn muốn cấu hình một thuộc tính làm thuộc tính khóa có tên khớp với {tên thực thể}_ID, ví dụ: thuộc tính Student_ID của thực thể Student sẽ là khóa chính. Sau đây định nghĩa quy ước này.


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder
        .Properties()
        .Where(p => p.Name == p.DeclaringType.Name + "_ID")
        .Configure(p => p.IsKey());

    base.OnModelCreating(modelBuilder);
}

Theo cùng một cách, bạn có thể định nghĩa quy ước cho kích thước của kiểu dữ liệu.

Ví dụ dưới đây định nghĩa một quy ước cho các thuộc tính chuỗi. Nó sẽ tạo các cột nvarchar có kích thước 50 trong SQL Server cho tất cả các thuộc tính kiểu chuỗi của một thực thể.


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
	modelBuilder
		.Properties()
		.Where(p => p.PropertyType.Name == "String")
		.Configure(p => p.HasMaxLength(50));

    base.OnModelCreating(modelBuilder);
}

Bạn cũng có thể định nghĩa một lớp tùy chỉnh cho quy ước này bằng cách kế thừa lớp Convention, như ví dụ bên dưới:


public class PKConvention : Convention
{
    public PKConvention()
    {
        this.Properties()
            .Where(p => p.Name == p.DeclaringType.Name + "_ID")
            .Configure(p => p.IsKey());
    }
}

Sau khi tạo lớp quy ước tùy chỉnh, hãy thêm nó vào các quy ước như dưới đây:


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Conventions.Add<PKConvention>();
}

Quy ước mô hình

Quy ước mô hình dựa trên siêu dữ liệu mô hình cơ bản. Có các quy ước cho cả CSDL và SSDL.

Tạo một lớp triển khai interface IConceptualModelConvention từ các quy ước CSDL và IStoreModelConvention từ các quy ước SSDL.

Truy cập Quy ước tùy chỉnh trong EF 6 để biết thêm thông tin.



Bài viết liên quan:

2 kịch bản lưu dữ liệu trong Entity Framework Core là kịch bản được kết nối và kịch bản ngắt kết nối.

Tạo ứng dụng .NET Core Console đầu tiên và cấu hình sử dụng Entity Framework Core.

Truy vấn trong Entity Framework Core có gì mới? Truy vấn trong EF Core khác EF ở những điểm nào.