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.

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ácMô tảVí dụ thực tế
CreateChè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
ReadTruy 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
UpdateSử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
DeleteXoá bản ghi không còn cần thiếtXoá bài đăng, huỷ đơn hàng

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.

CRUDHTTP MethodMục đích
CreatePOSTTạo resource mới
ReadGETLấy resource hoặc danh sách
UpdatePUT / PATCHSửa resource đã có
DeleteDELETEXoá 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).

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ả.

# 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

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.

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

HTTP Status Codes tương ứng


3. CRUD trong SQL

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

-- 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

Đặ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.

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):

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

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.

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

5.3 Entity / Model

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)]

Các lệnh Migration quan trọng

# 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:

Add-Migration InitialCreate
Update-Database

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.

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

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.

// 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íConnectedDisconnected
Cùng DbContext?Không
Change TrackingTự độngThủ công
Phổ biến ởDesktop app, batch jobWeb API, MVC app
Quản lý EntityStateEF 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ĩaSQL tương ứng
AddedEntity mới, chưa có trong DBINSERT
ModifiedEntity đã tồn tại, có thay đổiUPDATE
DeletedĐánh dấu để xoáDELETE
UnchangedEntity tồn tại, không thay đổi(không có SQL)
DetachedKhông được theo dõi(không có SQL)
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á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();

9.2 READ — Truy vấn

// 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();

9.3 UPDATE — Cập nhật

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

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:

// 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();
}

9.4 DELETE — Xoá

// 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

# 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]

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

// 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


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