C# Cơ bản .NET Core §1) Cài đặt, chương trình C# đầu tiên §2) Biến, kiểu dữ liệu và nhập/xuất §3) Toán tử số học và gán §4) So sánh, logic và lệnh if, switch §5) Vòng lặp for, while §6) Phương thức - Method §7) Phương thức - Delegate §8) Lớp - Class §9) Kiểu vô danh và dynamic §10) Biểu thức lambda §11) Event §12) Hàm hủy - Quá tải toán tử - thành viên tĩnh - indexer §13) Lớp lồng nhau - namespace §14) null và nullable §15) Mảng §16) Chuỗi ký tự §17) Tính kế thừa §18) Tính đa hình - abstract - interface §19) Struct và Enum §20) Ngoại lệ Exeption §21) IDisposable - using §22) File cơ bản §23) FileStream §24) Generic §25) Collection - List §26) SortedList §27) Queue / Stack §28) Linkedlist §29) Dictionary - HashSet §30) Phương thức mở rộng §31) ObservableCollection §32) LINQ §33) (Multithreading) async - bất đồng bộ §34) Type §35) Attribute Annotation §36) DI Dependency Injection §37) (Multithreading) Parallel §38) (Networking) HttpClient §39) (Networking) HttpMessageHandler §40) (Networking) HttpListener §41) (Networking) Tcp TcpListenerr/TcpClient §42) (ADO.NET) SqlConnection §43) (ADO.NET) SqlCommand §44) (EF Core) Tổng quan §45) (EF Core) Tạo Model §46) (EF Core) Fluent API §47) (EF Core) Query §48) (EF Core) Scaffold §49) (EF Core) Migration §50) (ASP.NET CORE) Hello World! §51) (ASP.NET CORE) Middleware §52) (ASP.NET CORE) Map - Request - Response §53) (ASP.NET CORE) IServiceCollection - MapWhen §54) (ASP.NET CORE) Session - ISession §55) (ASP.NET CORE) Configuration §56) (ASP.NET CORE MVC) Controller - View

Tạo Model với Data Annotation

Trong phần này sẽ thực hành tạo model chi tiết hơn, trước tiên tạo một dự án console mẫu đặt tên là ef02, cài các package vào như phần Giới thiệu EF Core . Mục đích ở đây là sử dụng EF Core từng bước tạo ra được một CSDL tên là shopdata với hai bảng đó là Category và bảng Product có sự dàng buộc. Trước tiên từ kiến thức ở ví dụ trước, tạo ra hai Model đơn giản như sau:

Model/Product.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ef02.Model
{
    [Table("Product")]                         // Ánh xạ bảng Product 
    public class Product
    {
        [Key]                                  // Là Primary key
        public int ProductId {set; get;}

        [Required]                              // Cột trong DB, Not Null
        [StringLength(50)]                      // nvarchar(50)
        public string Name {set; get;}

        [Column(TypeName="Money")]              // cột kiểu Money
        public decimal Price {set; get;}
    }
}
Model/Category.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ef02.Model
{
    [Table("Category")]
    public class Category
    {
        [Key]
        public int CategoryId {set; get;}

        [StringLength(100)]
        public string Name {set; get;}

        [Column(TypeName="NTEXT")]
        public string Description {set; get;}
    }
}

Những khái niệm về tạo mối liên hệ trong EF Core

Một mối liên hệ là ràng buộc giữa hai đối tượng (entity) dữ liệu, như trong các hệ quản trị CSDL quan hệ, nó thể hiện bởi các khóa ngoại - foreign key (FK). Để tạo ra các mối quan hệ trong EF Core, trước tiên hiểu một số khái niệm sau:

  • Bảng dữ liệu phụ thuộc hay bảng con (Dependent entity) - là những bảng có chứa khóa ngoại (FK) tham chiếu đến bảng khác
  • Bảng dữ liệu chính hay bảng cha (Principal entity) - là bảng có chứa khóa chính
  • Khóa chính - PK là thuộc tính, chứa giá trị duy nhất để xác định dòng dữ liệu
  • Khóa ngoại - Foreign Key (FK) - là thuộc tính trong bảng con thuộc được sử dụng để lưu khóa chính của bảng cha.
  • Thuộc tính điều hướng (Navigation) thuộc tính này chứa tham chiếu đến một đối tượng từ bảng khác, có các loại như:
    • Điều hướng tập hợp(Collection navigation) tham chiếu đến một tập hợp các đối tượng bảng khác (quan hệ một nhiều)
    • Điều hướng tham chiếu (Reference navigation) tham chiếu đến một đối tượng khác. (quan hệ một một)
    • Điều hướng nghịch (Inverse navigation) - thuộc tính điều hướng tham chiếu đến một điều hướng khác, sử dụng để tạo FK tham chiếu đến đối tượng cùng kiểu

Những khái niệm này ta sẽ chỉ ra khi bắt gặp trong quá trình thực hành sau đây

Tạo lớp Context chứa 2 Model trên

Model/ShopContext.cs
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace ef02.Model
{
    public class ShopContext : DbContext
    {
        // Chuỗi kết nối
        protected string connect_str = @"Data Source=localhost,1433;Initial Catalog=shopdata;User ID=SA;Password=Password123";

        public DbSet<Product>  products   {set; get;}  // bảng Product
        public DbSet<Category> categories {set; get;}  // bảng Category

        override protected void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder.UseSqlServer(connect_str);
        }
        // kích hoạt chế độ logger, hiện thị thông tin ra Console
        public static void EnableLogging()
        {
            using (var c = new ShopContext())  {
                IServiceProvider provider = c.GetInfrastructure<IServiceProvider>();
                ILoggerFactory loggerFactory = provider.GetService<ILoggerFactory>();
                loggerFactory.AddConsole();
            }
        }
        // Xóa database
        public static async Task DeleteDatabase()
        {
            using (var context = new ShopContext())
            {
                bool deleted = await context.Database.EnsureDeletedAsync();
                string deletionInfo = deleted ? "đã xóa db" : "không xóa được db";
                Console.WriteLine($"{deletionInfo}");
            }
        }
        // Xóa database
        public static async Task CreateDatabase()
        {
            using (var context = new ShopContext())
            {
                bool created = await context.Database.EnsureCreatedAsync();
                string createdInfo = created ? "Đã tạo mới" : "Đã tồn tại";
                Console.WriteLine($"{createdInfo}");
            }
        }
    }
}

Như vậy context ShopContext sẽ làm việc trên CSDL shopdata với hai bảng là ProductCategory. Trước tiên áp dụng với đoạn code sau:

static async Task Main(string[] args)
{
          ShopContext.EnableLogging();         // kích hoạt chế độ Logger để thấy thông tin khi EF làm việc
    await ShopContext.DeleteDatabase();        // xóa database: shopdata nếu tồn tại
    await ShopContext.CreateDatabase();        // tạo lại database: shopdata
}

Kết quả mỗi lần chạy code trên, nó sẽ tạo lại CSDL shopdata, trong đó có quan sát được các câu lệnh SQL khi nó thi hành. Tư cơ sở này sẽ tìm hiểu dần các đặc điểm phức tạp hơn sau đây.

Ánh xạ cột với Attribute Column

Attribute [Column] để ánh xạ thuộc tính vào Model vào bảng CSDL. Nếu muốn thiết lập kiểu của cột dữ liệu, thì thiết lập với TypeName="Kiểu trong SQL"

[Column(TypeName="Kiểu")]

Vị dụ trên:

[Column(TypeName="Money")]          // ánh xạ thuộc tính Price, kiểu decimal vào cột Price,  kiểu Money của bảng
public decimal Price {set; get;}

Một số kiểu hỗ trợ bởi SQL Server như

bigint          numeric         bit             smallint    decimal
smallmoney      int             tinyint         money       date
datetimeoffset  datetime2       smalldatetime   datetime
time            char	        varchar         text
nchar	        nvarchar        ntext           binary
varbinary       image

Tạo ra sự liên hệ ForeignKey

Ở ví dụ trên, giờ muốn thiết lập mỗi một sản phẩm Product thì tương ứng có một key trỏ đến ID của Category nếu có mà Product thuộc về. Rất đơn giản hãy thêm một thuộc tính kiểu Category vào Product

Model/Product.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ef02.Model
{
    [Table("Product")]
    public class Product
    {
        [Key]
        public int ProductId {set; get;}

        [Required]
        [StringLength(50)]
        public string Name {set; get;}

        [Column(TypeName="Money")]
        public decimal Price {set; get;}

        public Category Category {set; get;}  // Thuộc tính tạo ra FK
    }
}

Chạy lại, thấy bảng Product được tạo bằng SQL sau:

CREATE TABLE [Products] (
      [ProductId] int NOT NULL IDENTITY,
      [Name] nvarchar(50) NOT NULL,
      [Price] Money NOT NULL,
      [CategoryId] int NULL,
      CONSTRAINT [PK_Products] PRIMARY KEY ([ProductId]),
      CONSTRAINT [FK_Products_Category_CategoryId]
                  FOREIGN KEY ([CategoryId]) REFERENCES [Category] ([CategoryId])
                  ON DELETE NO ACTION /* khi xóa Category thì Product không thay đổi gì*/
    );

Thấy rằng, bảng Product đã được chèn thêm cột có tên CategoryId và đã thiết lập ràng bộc đến bảng Category, tên CategoryId nó tự động đặt giống tên khóa chính của Category

Nếu đặt tên FK CategoryID trên thành một tên khác, thì hãy sử dụng thuộc tính [ForeignKey], ví dụ nếu muốn khóa này tên sẽ là CateId

/..
[ForeignKey("CateID")]     // Khóa ngoại tự đặt CateID tham chiếu đến khóa chính CategoryID của bảng Category
public Category Category {set; get;}
/..

Chạy lại thấy SQL tạo Product như sau:

CREATE TABLE [Products] (
  [ProductId] int NOT NULL IDENTITY,
  [Name] nvarchar(50) NOT NULL,
  [Price] Money NOT NULL,
  [CateID] int NULL, /* FK key tự đặt */
  CONSTRAINT [PK_Products] PRIMARY KEY ([ProductId]),
  CONSTRAINT [FK_Products_Category_CateID]
             FOREIGN KEY ([CateID]) REFERENCES [Category] ([CategoryId])  /* ràng buộc đến Category */
             ON DELETE NO ACTION /* khi xóa Category thì Product không thay đổi gì*/
);

Mặc dù cách trên, bảng trong DB đã có cột FK CategoryId, nhưng trong Model chưa có, nên nếu cần lấy FK này thì chỉ cần thêm nó vào Model

/..
    public class Product
    {
        public int CategoryId {set; get;}      // thêm thuộc tính
        public Category Category {set; get;}   // Thuộc tính tạo ra FK
    }
/..

Vậy đã có thuộc tính CategoryID để đọc được dữ liệu này từ bảng Database, khi chạy tạo lại Product thấy có một điểm khác

CREATE TABLE [Products] (
  [ProductId] int NOT NULL IDENTITY,
  [Name] nvarchar(50) NOT NULL,
  [Price] Money NOT NULL,
  [CategoryId] int NOT NULL,
  CONSTRAINT [PK_Products] PRIMARY KEY ([ProductId]),
  CONSTRAINT [FK_Products_Category_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Category] ([CategoryId])
  ON DELETE CASCADE    /* Khi xóa Category, các sản phẩm thuộc về nó cũng bị xóa theo */
);

Có quan hệ ON DELETE CASCADE ở trên bởi vì [CategoryId] int NOT NULL, nếu muốn khi xóa Category, sản phẩm không bị xóa theo thì khóa ngoại [CategoryId] phải có khả năng nhận null, làm điều đó chir việc thay đổi sau:s

/..
    public class Product
    {
        public int? CategoryId {set; get;}     // thuộc tính có khả năng nhận NULL
        public Category Category {set; get;}   // Navigation Reference
    }
/..
Đên đây, lớp Category gọi là bảng chính, nó chứa khóa chính (CategoryId), bảng Product gọi là bảng con, nó chứa khóa ngoại FK CategoryId tham chiếu đến khóa chính của Category.

Thuộc tính Category của lớp Product gọi là điều hướng tham chiếu (Navigation Reference), ví nó tham chiếu đến một đối tượng lớp Category.

Load các thuộc tính liên hệ với FK

Ở Model Product mặc dù có thuộc tính Category:

public Category Category {set; get;}

Nhưng khi EF nạp thông thường thì thuộc tính này không tự động được nạp từ DB (nó nhận null). Ví dụ, thêm vào ShopContext

public class ShopContext : DbContext
{
    /..
    // Chèn dữ liệu mẫu
    public static async Task InsertSampleData()
    {
        using (var context = new ShopContext())
        {
            await context.AddRangeAsync(
                new Category() {Name = "Cate1", Description = "Description1"},
                new Category() {Name = "Cate2", Description = "Description2"}
            );
            await context.SaveChangesAsync();
            Category cate2 = await  (from c  in context.categories where c.Name == "Cate2" select c)
                                    .FirstOrDefaultAsync();
            await  context.AddRangeAsync(
                new Product()  {Name = "Sản phẩm 1", Price=12, Category = cate2},
                new Product()  {Name = "Sản phẩm 2", Price=11, Category = cate2},
                new Product()  {Name = "Sản phẩm 3", Price=33, Category = cate2}
            );
            await context.SaveChangesAsync();
        }
    }

    // Lấy một Product từ DB theo ProductID
    public static async Task<Product> FindProduct(int id)
    {
        using (var context = new ShopContext())
        {
            var p =  await (from c in context.products where c.ProductId == id select c)
                           .FirstOrDefaultAsync();
            return  p;
        }
    }
    /..
}

Áp dụng

static async Task Main(string[] args)
{
    ShopContext.EnableLogging();
    await ShopContext.DeleteDatabase();
    await ShopContext.CreateDatabase();
    await ShopContext.InsertSampleData();           // chèn dữ liệu mẫu

    var p    = await ShopContext.FindProduct(2);    // lấy 1 sản phẩm
    var c    = p.Category;
    if (p != null)
    {
        Console.WriteLine($"{p.Name} có CategoryId = {p.CategoryId}");
        string CategoryName = (c != null) ? c.Name :  "Category đang null";
        Console.WriteLine(CategoryName);
    }
}

Chạy thử, thấy dòng:

Sản phẩm 2 có CategoryId = 2
Category đang null

Như vậy thấy rằng thuộc tính Category không nạp, khi Product đọc được từ EF. Nếu muốn thuộc tính này, nhận đối tượng theo đúng khóa ngoại CategoryId thì làm như sau, sửa lại FindProduct:

public class ShopContext : DbContext
{
    /..
    // Lấy một Product từ DB theo ProductID
    public static async Task<Product> FindProduct(int id)
    {
        using (var context = new ShopContext())
        {
            var p =  await (from c in context.products where c.ProductId == id select c)
                           .FirstOrDefaultAsync();

            await  context.Entry(p)                   // lấy DbEntityEntry liên quan đến p
                          .Reference(x => x.Category) // lấy tham chiếu, liên quan đến thuộc tính Category
                          .LoadAsync();               // nạp thuộc tính từ DB
            
            return  p;
        }
    }
    /..
}

Trong đó: DbContext.Entry() lấy đối tượng DbEntityEntry trong EF liên quan đến đối tượng dữ liệu, từ đối tượng này có thể thi hành các tác vụ khác nhau.

DbEntityEntry.Reference() lấy tham chiếu theo thuộc tính nào đó của dữ liệu (dạng giá trị đơn - Navigation Reference).

DbEntityEntry.Collection() lấy tham chiếu theo thuộc tính nào đó của dữ liệu (dạng tập hợp - Navigatin Collection).

Chạy lại, thấy:

Sản phẩm 2 có CategoryId = 2
Cate2

Vậy thuộc tính tham chiếu đến đối tượng khác cũng đã được nạp.

Tương tự, hãy thêm vào Category thuộc tính products biểu diễn danh sách các sản phẩm thuộc về Category này.

[Table("Category")]
public class Category
{
    [Key]
    public int CategoryId {set; get;}

    [StringLength(100)]
    public string Name {set; get;}

    [Column(TypeName="ntext")]
    public string Description {set; get;}

    public List<Product> Products { get; set;}    // Đây là điều hướng dạng Collection Navigation (tập hợp)
}

Lazy load

Bạn cũng có thể cài đặt chế độ lazy load, thuộc tính tham chiếu tự động load khi nó được truy cập. Cài vào package:

dotnet add package Microsoft.EntityFrameworkCore.Proxies

Ở phương thức khởi tạo thêm vào

optionsBuilder.UseLazyLoadingProxies();

Lúc này thuộc tính tham chiếu khai báo với virtual thì nó sẽ tự động nạp khi truy cập

Tương tự, bạn có thể thêm thuộc tính products vào Model Cateogry như sau:

public virtual ICollection<Product> products { get; set;} = new List<Product>();

Bạn cũng nạp được danh sách các sản phẩm cho một Category, ví dụ:

public static async Task<Category> FindCategoryByName(string namecate)
{
    using (var context = new ShopContext())
    {

        var category =  await (from c  in context.categories where c.Name == namecate select c)
                                .FirstOrDefaultAsync();
    
        // các các Product vào  thuộc tính products
        await  context.Entry(category)
                      .Collection(x => x.products) // lấy đối tượng liên quan dạng tập hợp
                      .LoadAsync();
        

        return  category;

    }
}

Tạo điều hướng nghịch với thuộc tính InverseProperty

Đến đây, các khái niệm mỗi liên hệ đã lướt qua gồm: khóa ngoại (FK), khóa chính (PK), bảng cha, bảng con, điều hướng tham chiếu, điều hướng tập hợp.

Còn khái niệm nữa là điều hướng nghịch, như khái niệm phía trên: Điều hướng nghịch (Inverse navigation) - thuộc tính điều hướng tham chiếu đến một điều hướng khác, sử dụng để tạo FK tham chiếu đến đối tượng cùng kiểu

Ví dụ: Mỗi sản phẩm Product ở trên nó thuộc về một Category xác định bởi khóa ngoại của nó là CategoryId, bây giờ muốn: mỗi sản phẩm có thể thuộc về một Category nữa (có nghĩa một sản phẩm có thể nằm trong hai Category), vậy phải làm thế nào.

Ta sẽ đặt thuộc tính Category thứ hai này là SecondCategory, theo logic thông thường thì sẽ tạo một khóa ngoại nữa tham chiếu đến Category

public class Product
{
    [Key]
    public int ProductId {set; get;}

    [Required]
    [StringLength(50)]
    public string Name {set; get;}

    [Column(TypeName="Money")]
    public decimal Price {set; get;}


    public int CategoryId {set; get;}               // FK thứ nhất
    [ForeignKey("CategoryId")]
    public virtual Category Category {set; get;}

    public int? CategorySecondId;                   // FK thứ hai
    [ForeignKey("CategorySecondId")]
    public Category SecondCategory {set; get;}      // Chú ý thuộc tính SecondCategory cùng kiểu thuộc tính Category
}

Khi chạy nó sẽ báo lỗi, không thể tạo ra mối liên hệ trên. Nguyên nhân là đã tồn tại FK thứ nhất với điều hướng tham chiếu đến Category rồi, không thể có FK thứ hai. Để giải quyết thì phải thiết lập thuộc tính SecondCategory là tham chiếu nghịch, nó tham chiếu đến một tham chiếu tập hợp khác.

Trước tiên trong Category cần tạo ra một tham chiếu tập hợp như sau:

public class Category
{
    [Key]
    public int CategoryId {set; get;}

    [StringLength(100)]
    public string Name {set; get;}

    [Column(TypeName="ntext")]
    public string Description {set; get;}

     public virtual ICollection<Product> products { get; set;}

     //Tham chiếu tập hợp được tạo ra để đối tượng khác tham chiếu ngược lại
     public List<Product> OtherProduct {set; get;}
}

Đã có thuộc tính OtherProduct trong Category, giờ chỉ việc thiết lập SecondCategory của Product tham chiếu ngược về thành phần này bằng thuộc tính InverseProperty

public class Product
{
    [Key]
    public int ProductId {set; get;}

    [Required]
    [StringLength(50)]
    public string Name {set; get;}

    [Column(TypeName="Money")]
    public decimal Price {set; get;}


    public int CategoryId {set; get;}               // FK thứ nhất
    [ForeignKey("CategoryId")]
    public virtual Category Category {set; get;}    // Các sản phầm thiết lập Category này là Category thứ nhất

    public int? CategorySecondId;                   // FK thứ hai
    [ForeignKey("CategorySecondId")]  
    [InverseProperty("OtherProduct")]               // Tham  chiều người về OtherProduct của Category
    public Category SecondCategory {set; get;}      // Các sản phẩm coi Category là Category thứ 2
}

Chạy, nhìn thấy câu truy vấn tạo bảng Product như sau

CREATE TABLE [Products] (
          [ProductId] int NOT NULL IDENTITY,
          [Name] nvarchar(50) NOT NULL,
          [Price] Money NOT NULL,
          [CategoryId] int NOT NULL,
          [CategorySecondId] int NULL,
          CONSTRAINT [PK_Products] PRIMARY KEY ([ProductId]),
          CONSTRAINT [FK_Products_Category_CategoryId]
                     FOREIGN KEY ([CategoryId]) REFERENCES [Category] ([CategoryId]) ON DELETE CASCADE,
    
          CONSTRAINT [FK_Products_Category_CategorySecondId]
                     FOREIGN KEY ([CategorySecondId]) REFERENCES [Category] ([CategoryId]) ON DELETE NO ACTION

Nó đã tạo ra bảng có 2 FK, một là CategoryId, một là CategorySecondId cùng tham chiếu đến bảng Category.

Đăng ký theo dõi ủng hộ kênh