Skip to content

Chương 7: CRUD và Thao Tác CSDL với Entity Framework Core¤

1. CRUD là gì?¤

CRUD là từ viết tắt của Create, Read, Update, Delete — bốn thao tác cơ bản tạo nên nền tảng của mọi hệ thống quản lý dữ liệu.

Text Only
C — Create   : Tạo mới dữ liệu
R — Read     : Đọc / truy vấn dữ liệu
U — Update   : Cập nhật dữ liệu
D — Delete   : Xoá dữ liệu

Mô tả từng thao tác¤

Thao tác Mô tả Ví dụ thực tế
Create Chèn bản ghi mới vào hệ thống lưu trữ Đăng ký tài khoản mới, thêm sản phẩm
Read Truy xuất dữ liệu đã tồn tại, không thay đổi gì Xem hồ sơ, tìm kiếm sản phẩm
Update Sửa dữ liệu đã có mà không tạo bản ghi mới Đổi mật khẩu, cập nhật địa chỉ giao hàng
Delete Xoá bản ghi không còn cần thiết Xoá bài đăng, huỷ đơn hàng

Tại sao CRUD quan trọng?

Nếu một thao tác không thể mô tả bằng một trong bốn hàm này, có thể nó cần phải là một model riêng biệt. CRUD đặc biệt quan trọng trong: phát triển ứng dụng web, thiết kế API, hệ thống quản lý CSDL, và kiến trúc phần mềm.


2. CRUD trong RESTful API¤

Trong môi trường REST, các thao tác CRUD ánh xạ trực tiếp sang các HTTP method.

CRUD HTTP Method Mục đích
Create POST Tạo resource mới
Read GET Lấy resource hoặc danh sách
Update PUT / PATCH Sửa resource đã có
Delete DELETE Xoá resource

2.1 CREATE — POST¤

Khi tạo thành công, server trả về header chứa link đến resource mới và HTTP status code 201 (CREATED).

HTTP
POST http://www.myrestaurant.com/dishes/

Body:
{
  "dish": {
    "name": "Avocado Toast",
    "price": 8
  }
}

Response (201 CREATED):
{
  "dish": {
    "id": 1223,
    "name": "Avocado Toast",
    "price": 8
  }
}

2.2 READ — GET¤

Đọc dữ liệu không bao giờ thay đổi thông tin — nó chỉ lấy về. Gọi GET 10 lần liên tiếp phải trả về cùng kết quả.

HTTP
# Lấy toàn bộ danh sách
GET http://www.myrestaurant.com/dishes/
→ 200 OK

# Lấy một phần tử cụ thể theo id
GET http://www.myrestaurant.com/dishes/1223
→ 200 OK | 404 NOT FOUND nếu không tìm thấy

2.3 UPDATE — PUT¤

HTTP
PUT http://www.myrestaurant.com/dishes/1223

Body:
{
  "dish": {
    "name": "Avocado Toast",
    "price": 10
  }
}

Response: 200 OK (hoặc 204 NO CONTENT)

2.4 DELETE — DELETE¤

Gọi DELETE trên resource không tồn tại không được thay đổi trạng thái hệ thống — phải trả về 404 NOT FOUND.

HTTP
DELETE http://www.myrestaurant.com/dishes/1223
→ 204 NO CONTENT (không có body)

HTTP Status Codes tương ứng¤

Hỏi: Mỗi thao tác CRUD có những status code nào?

Trả lời:

Thao tác Status Code thường gặp Ý nghĩa
POST (Create) 201 Tạo thành công
POST (Create) 400 Dữ liệu đầu vào sai
POST (Create) 409 Resource đã tồn tại
GET (Read) 200 Tìm thấy, trả về
GET (Read) 404 Không tồn tại
PUT/PATCH (Update) 200 Cập nhật thành công
PUT/PATCH (Update) 400 Dữ liệu cập nhật sai
PUT/PATCH (Update) 404 Không tìm thấy để cập nhật
DELETE 204 Xoá thành công
DELETE 404 Không tìm thấy để xoá

3. CRUD trong SQL¤

Trong SQL, các thao tác CRUD tương ứng với những lệnh SQL cụ thể.

SQL
-- CREATE
INSERT INTO dishes (name, price, category)
VALUES ('Avocado Toast', 8, 'Breakfast');

-- READ
SELECT * FROM dishes WHERE id = 1223;

-- UPDATE
UPDATE dishes SET price = 10 WHERE id = 1223;

-- DELETE
DELETE FROM dishes WHERE id = 1223;

4. Entity Framework Core là gì?¤

Entity Framework Core (EF Core) là một ORM (Object-Relational Mapper) của Microsoft dành cho .NET. Thay vì viết SQL thủ công, bạn thao tác với dữ liệu thông qua các đối tượng C#, và EF Core tự động sinh SQL tương ứng.

flowchart LR
    A[C# Code] --> B[EF Core]
    B --> C[(SQL Database)]
    C --> B
    B --> A
Hold "Alt" / "Option" to enable pan & zoom

Đặc điểm nổi bật¤

  • Cross-platform: chạy trên Windows, Linux, macOS
  • Code-First: định nghĩa model bằng C#, EF tạo CSDL
  • Database-First: reverse engineer CSDL thành C# model
  • Migration: quản lý thay đổi schema CSDL theo phiên bản
  • LINQ queries: truy vấn dữ liệu bằng LINQ thay vì SQL thuần

5. Các khái niệm cốt lõi¤

5.1 DbContext¤

DbContext quản lý kết nối CSDL, theo dõi thay đổi trên entity, và thực thi query. Nó xoá bỏ nhu cầu viết SQL phức tạp thủ công vì tự động sinh ra chúng.

C#
public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(
            "Server=(localdb)\\mssqllocaldb;Database=ShopDb;Trusted_Connection=True;");
    }
}

Hoặc cấu hình qua Dependency Injection (khuyến nghị trong ASP.NET Core):

C#
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
JSON
// appsettings.json
{
  "ConnectionStrings": {
    "Default": "Server=(localdb)\\mssqllocaldb;Database=ShopDb;Trusted_Connection=True;"
  }
}

DbContext KHÔNG thread-safe

EF Core không hỗ trợ nhiều thao tác song song trên cùng một DbContext instance. Điều này bao gồm cả việc thực thi async query song song và sử dụng đồng thời từ nhiều thread. Trong web app, mỗi HTTP request nên dùng một DbContext riêng.

5.2 DbSet¤

DbSet đại diện cho một tập hợp entity trong CSDL (về mặt logic là một bảng ảo). Cần định nghĩa nó cho từng kiểu entity (như Customer, Order, Product) để truy vấn và thao tác dữ liệu.

C#
// DbSet<Product> ánh xạ tới bảng Products trong database
public DbSet<Product> Products { get; set; }

5.3 Entity / Model¤

C#
public class Product
{
    public int Id { get; set; }           // Primary key (tự động nhận dạng)
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; } // Navigation property
}

6. Migration — Quản lý Schema CSDL¤

Migration là cơ chế giúp đồng bộ model C# với schema CSDL theo lịch sử thay đổi.

flowchart LR
    A[Thêm/Sửa Entity] --> B[add-migration]
    B --> C[File migration được tạo]
    C --> D[database update]
    D --> E[(CSDL được cập nhật)]
Hold "Alt" / "Option" to enable pan & zoom

Các lệnh Migration quan trọng¤

Bash
# Tạo migration mới (sau khi thay đổi model)
dotnet ef migrations add InitialCreate

# Áp dụng migration vào database
dotnet ef database update

# Xem danh sách migrations
dotnet ef migrations list

# Rollback về migration trước
dotnet ef database update TênMigrationCũ

# Xoá migration chưa apply
dotnet ef migrations remove

Hoặc dùng Package Manager Console trong Visual Studio:

PowerShell
Add-Migration InitialCreate
Update-Database
Hỏi: File migration sinh ra trông như thế nào?

Trả lời: EF Core tạo ra hai file:

C#
// Migrations/20240101_InitialCreate.cs
public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Tạo bảng Products
        migrationBuilder.CreateTable(
            name: "Products",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Name = table.Column<string>(maxLength: 100, nullable: false),
                Price = table.Column<decimal>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Products", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // Rollback: xoá bảng Products
        migrationBuilder.DropTable(name: "Products");
    }
}
  • Phương thức Up(): áp dụng thay đổi lên CSDL
  • Phương thức Down(): rollback, hoàn tác thay đổi

7. Connected vs Disconnected Scenario¤

Đây là một trong những khái niệm quan trọng nhất khi làm việc với EF Core.

7.1 Connected Scenario¤

Trong Connected Scenario, cùng một DbContext instance được dùng để lấy và lưu entity. Điều này có nghĩa DbContext theo dõi trạng thái entity xuyên suốt vòng đời của chúng.

C#
using (var context = new AppDbContext())
{
    // 1. Lấy entity (context bắt đầu track)
    var product = context.Products.Find(1);

    // 2. Sửa trong cùng context
    product.Price = 200;

    // 3. Lưu — EF tự biết cần UPDATE
    context.SaveChanges();
}
sequenceDiagram
    participant App
    participant DbContext
    participant DB
    App->>DbContext: Find(1)
    DbContext->>DB: SELECT WHERE Id=1
    DB-->>DbContext: entity (state: Unchanged)
    App->>DbContext: product.Price = 200
    Note over DbContext: state tự động: Modified
    App->>DbContext: SaveChanges()
    DbContext->>DB: UPDATE Products SET Price=200 WHERE Id=1
Hold "Alt" / "Option" to enable pan & zoom

7.2 Disconnected Scenario¤

Trong Disconnected Scenario, DbContext không có sẵn để theo dõi thay đổi sau khi dữ liệu được lấy về. Tình huống này phổ biến trong web API, nơi entity được truyền qua HTTP. Không có change tracking: entity không được theo dõi sau khi DbContext bị dispose, nghĩa là thay đổi phải được quản lý thủ công.

C#
// Request 1: lấy data (DbContext #1, rồi dispose)
Product product;
using (var context = new AppDbContext())
{
    product = context.Products.Find(1);
} // DbContext bị dispose, không còn tracking

// ... entity được truyền qua HTTP, user sửa ...
product.Price = 999;

// Request 2: lưu (DbContext #2, khác với #1)
using (var context = new AppDbContext())
{
    // Phải thông báo cho context biết entity này đã bị Modified
    context.Entry(product).State = EntityState.Modified;
    context.SaveChanges();
}

So sánh Connected vs Disconnected¤

Tiêu chí Connected Disconnected
Cùng DbContext? Không
Change Tracking Tự động Thủ công
Phổ biến ở Desktop app, batch job Web API, MVC app
Quản lý EntityState EF tự xử lý Lập trình viên phải set

8. EntityState — Trạng thái Entity¤

Trong Connected Scenario, DbContext theo dõi entity và trạng thái của chúng (Added, Modified, Deleted). Khi gọi SaveChanges(), EF Core tự động sinh và thực thi câu lệnh SQL INSERT, UPDATE, hoặc DELETE tương ứng dựa trên trạng thái entity.

EntityState Ý nghĩa SQL tương ứng
Added Entity mới, chưa có trong DB INSERT
Modified Entity đã tồn tại, có thay đổi UPDATE
Deleted Đánh dấu để xoá DELETE
Unchanged Entity tồn tại, không thay đổi (không có SQL)
Detached Không được theo dõi (không có SQL)
C#
var product = context.Products.Find(1);
Console.WriteLine(context.Entry(product).State); 
// → Unchanged

product.Price = 500;
Console.WriteLine(context.Entry(product).State); 
// → Modified (tự động!)

9. Thực hiện CRUD với EF Core¤

9.1 CREATE — Thêm mới¤

C#
// Cách 1: DbSet.Add
var product = new Product { Name = "Laptop", Price = 15000000 };
context.Products.Add(product);
context.SaveChanges();

// Cách 2: DbContext.Add (dùng khi không biết type cụ thể)
context.Add(product);
context.SaveChanges();

// Cách 3: Thêm nhiều cùng lúc — AddRange
var products = new List<Product>
{
    new Product { Name = "Phone", Price = 8000000 },
    new Product { Name = "Tablet", Price = 12000000 }
};
context.Products.AddRange(products);
context.SaveChanges();

Async luôn được ưu tiên trong web app

C#
await context.Products.AddAsync(product);
await context.SaveChangesAsync();

9.2 READ — Truy vấn¤

C#
// Lấy tất cả
var all = context.Products.ToList();

// Lấy theo điều kiện
var cheap = context.Products
    .Where(p => p.Price < 5000000)
    .ToList();

// Lấy một bản ghi theo primary key (nhanh nhất)
var product = context.Products.Find(1);

// Lấy một bản ghi theo điều kiện (trả về null nếu không tìm thấy)
var product = context.Products
    .FirstOrDefault(p => p.Name == "Laptop");

// Bao gồm dữ liệu quan hệ (eager loading)
var products = context.Products
    .Include(p => p.Category)
    .ToList();
Hỏi: AsNoTracking() là gì và khi nào dùng?

Trả lời: Dùng AsNoTracking() cho các query chỉ đọc vì nó bỏ qua overhead của EF Core's change tracking, cải thiện hiệu năng đáng kể. Đây là điều cốt yếu với read-only query.

C#
// Dành cho read-only, không cần cập nhật sau
var products = context.Products
    .AsNoTracking()
    .Where(p => p.Price > 1000)
    .ToList();

9.3 UPDATE — Cập nhật¤

Connected Scenario (đơn giản nhất):

C#
using (var context = new AppDbContext())
{
    var product = context.Products.Find(1);
    product.Price = 20000000; // EF tự phát hiện thay đổi
    context.SaveChanges();   // Chỉ UPDATE các field đã thay đổi
}

Disconnected Scenario:

C#
// entity đến từ ngoài (API request, form submit...)
public void UpdateProduct(Product updatedProduct)
{
    using var context = new AppDbContext();

    // Cách 1: Attach + set state
    context.Entry(updatedProduct).State = EntityState.Modified;
    context.SaveChanges();

    // Cách 2: DbContext.Update() — tương đương
    context.Update(updatedProduct);
    context.SaveChanges();
}

Lưu ý khi dùng Update() trong disconnected

context.Update(entity) sẽ đánh dấu tất cả các field là Modified → SQL UPDATE sẽ ghi lại toàn bộ cột, kể cả những cột không thay đổi. Trong connected scenario, EF chỉ UPDATE đúng các field thực sự thay đổi.

9.4 DELETE — Xoá¤

C#
// Cách 1: Connected — tìm rồi xoá
var product = context.Products.Find(1);
if (product != null)
{
    context.Products.Remove(product);
    context.SaveChanges();
}

// Cách 2: Disconnected — tạo stub entity để tránh SELECT thừa
var stub = new Product { Id = 1 };
context.Products.Attach(stub);
context.Products.Remove(stub);
context.SaveChanges();

// Cách 3: EF Core 7+ ExecuteDelete (không cần load entity vào memory)
await context.Products
    .Where(p => p.Price < 100)
    .ExecuteDeleteAsync();

10. Cài đặt EF Core¤

Bash
# Cài provider SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

# Cài tools để dùng migration
dotnet add package Microsoft.EntityFrameworkCore.Tools

# Hoặc dùng NuGet Package Manager Console:
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools

11. Quy trình tổng quát xây dựng ứng dụng CRUD với EF Core¤

flowchart TD
    A[1. Tạo Entity/Model class] --> B[2. Tạo DbContext]
    B --> C[3. Đăng ký DI trong Program.cs]
    C --> D[4. add-migration]
    D --> E[5. database update]
    E --> F[6. Viết service/repository thực hiện CRUD]
    F --> G[7. Gọi từ Controller/API endpoint]
Hold "Alt" / "Option" to enable pan & zoom

Ví dụ hoàn chỉnh — Console App¤

C#
// 1. Model
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

// 2. DbContext
public class SchoolDbContext : DbContext
{
    public DbSet<Student> Students { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=SchoolDb;Trusted_Connection=True;");
}

// 3. Thao tác CRUD
class Program
{
    static async Task Main()
    {
        using var context = new SchoolDbContext();
        await context.Database.MigrateAsync(); // apply migration

        // CREATE
        var student = new Student { Name = "Nguyen Van A", Email = "a@example.com" };
        context.Students.Add(student);
        await context.SaveChangesAsync();
        Console.WriteLine($"Đã thêm: Id = {student.Id}");

        // READ
        var all = await context.Students.ToListAsync();
        foreach (var s in all)
            Console.WriteLine($"{s.Id}: {s.Name}");

        // UPDATE
        var found = await context.Students.FindAsync(student.Id);
        found.Name = "Nguyen Van B";
        await context.SaveChangesAsync();

        // DELETE
        context.Students.Remove(found);
        await context.SaveChangesAsync();
        Console.WriteLine("Đã xoá.");
    }
}

12. Best Practices¤

Các nguyên tắc tốt khi dùng EF Core

  1. Dùng async/await cho mọi thao tác I/O với CSDL (ToListAsync, SaveChangesAsync...)
  2. Dùng AsNoTracking() cho các query chỉ đọc — tăng hiệu năng đáng kể
  3. Inject DbContext qua DI thay vì tạo trực tiếp — dễ test, đúng vòng đời
  4. Dùng FindAsync(id) khi cần lấy theo primary key — kiểm tra cache trước khi ra DB
  5. Tránh N+1 problem bằng cách dùng .Include() để eager load dữ liệu quan hệ
  6. Dùng transactions khi cần nhiều thao tác phải thành công cùng lúc
C#
// Tránh N+1: đừng làm thế này
var orders = context.Orders.ToList();
foreach (var o in orders)
    Console.WriteLine(o.Customer.Name); // mỗi dòng = 1 query!

// Làm đúng: Include trước
var orders = context.Orders.Include(o => o.Customer).ToList();
Hỏi: EF Core tự dùng transaction chưa?

Trả lời: Mặc định EF tự triển khai transaction. Khi bạn thực hiện thay đổi nhiều dòng hoặc nhiều bảng rồi gọi SaveChanges, EF đảm bảo tất cả thay đổi thành công hoặc tất cả thất bại. Nếu có lỗi xảy ra, các thay đổi trước đó sẽ tự động được rollback.

Nếu cần kiểm soát transaction thủ công:

C#
using var transaction = await context.Database.BeginTransactionAsync();
try
{
    context.Products.Add(new Product { Name = "A", Price = 100 });
    await context.SaveChangesAsync();

    context.Orders.Add(new Order { ProductId = 1 });
    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

Tóm tắt¤

mindmap
  root((EF Core CRUD))
    CRUD
      Create: Add va SaveChanges
      Read: Find - Where - Include
      Update: Track changes - Update
      Delete: Remove - ExecuteDelete
    Concepts
      DbContext
      DbSet
      EntityState
      Migration
    Scenarios
      Connected
      Disconnected
    Best Practices
      Async
      AsNoTracking
      Include tranh loi N+1
      Dependency Injection
Hold "Alt" / "Option" to enable pan & zoom