Rust và SQLx 2026: Truy Vấn Được Kiểm Tra Tại Thời Điểm Biên Dịch và Câu Hỏi Phỏng Vấn

Tìm hiểu cách SQLx 0.9 xác minh các truy vấn SQL tại thời điểm biên dịch, cấu hình sqlx.toml, và chuẩn bị cho phỏng vấn kỹ thuật vị trí backend Rust.

Hướng dẫn Rust SQLx với Truy Vấn Được Kiểm Tra Tại Thời Điểm Biên Dịch

SQLx 0.9 mang đến cách tiếp cận mới cho việc truy cập cơ sở dữ liệu trong Rust. Thay vì phát hiện lỗi SQL khi ứng dụng chạy trong môi trường production, phiên bản này bắt lỗi ngay tại thời điểm biên dịch. Ra mắt vào tháng 5 năm 2026, SQLx 0.9 giới thiệu hệ thống cấu hình mới thông qua sqlx.toml, tính linh hoạt về runtime, và bảo mật truy vấn nghiêm ngặt hơn thông qua trait SqlSafeStr.

Điểm Khác Biệt của SQLx

Không giống các ORM tạo SQL từ struct Rust, SQLx xác minh các truy vấn SQL viết tay với cơ sở dữ liệu thực tại thời điểm biên dịch. Lỗi đánh máy trong tên cột sẽ làm build thất bại, không phải gây lỗi trong production.

Xác Minh Truy Vấn Tại Thời Điểm Biên Dịch trong SQLx 0.9

Macro query! kiểm tra mọi câu lệnh SQL với schema cơ sở dữ liệu trong quá trình cargo build. Tính năng này phát hiện lỗi đánh máy, không khớp kiểu dữ liệu, và các cột thiếu trước khi code được thực thi.

src/db/users.rsrust
use sqlx::{FromRow, PgPool};

#[derive(FromRow)]
pub struct User {
    pub id: i32,
    pub email: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result<Option<User>, sqlx::Error> {
    // Được xác minh tại thời điểm biên dịch: tên cột, kiểu dữ liệu, và cấu trúc trả về
    sqlx::query_as!(
        User,
        r#"
        SELECT id, email, created_at
        FROM users
        WHERE email = $1
        "#,
        email
    )
    .fetch_optional(pool)
    .await
}

Macro query_as! ánh xạ các hàng kết quả trực tiếp vào struct User. Nếu bảng users không có cột created_at, biên dịch sẽ thất bại với thông báo lỗi rõ ràng chỉ đến dòng có vấn đề.

Hệ Thống Cấu Hình sqlx.toml

SQLx 0.9 giới thiệu file sqlx.toml tập trung cấu hình cơ sở dữ liệu, ghi đè kiểu dữ liệu, và thiết lập đa cơ sở dữ liệu. Cách tiếp cận này thay thế các biến môi trường phân tán và đơn giản hóa pipeline CI.

toml
# sqlx.toml
[common]
database_url_var = "DATABASE_URL"

[macros]
default_type_override.uuid = "uuid::Uuid"
default_type_override.timestamptz = "chrono::DateTime<chrono::Utc>"

[sqlite]
extensions = ["uuid", "crypto"]

Việc ghi đè kiểu dữ liệu loại bỏ việc ép kiểu lặp đi lặp lại. Mỗi cột UUID tự động được ánh xạ thành uuid::Uuid, và mỗi timestamptz thành chrono::DateTime mà không cần chú thích cho từng truy vấn.

SqlSafeStr: Ngăn Chặn SQL Injection ở Cấp Độ Kiểu Dữ Liệu

SQLx 0.9 tăng cường bảo mật truy vấn bằng cách yêu cầu SqlSafeStr cho các tham số chuỗi. Theo mặc định, chỉ &'static str triển khai trait này, chặn các chuỗi động có thể mang payload injection.

src/db/search.rsrust
use sqlx::{AssertSqlSafe, PgPool};

// Tên bảng tĩnh: biên dịch trực tiếp
const TABLE: &str = "products";

pub async fn search_products(
    pool: &PgPool,
    user_query: &str,
) -> Result<Vec<Product>, sqlx::Error> {
    // An toàn: $1 là giá trị tham số, không phải SQL được nội suy
    sqlx::query_as!(
        Product,
        r#"SELECT * FROM products WHERE name ILIKE '%' || $1 || '%'"#,
        user_query
    )
    .fetch_all(pool)
    .await
}

// Bảng động từ config: bọc trong AssertSqlSafe sau khi xác thực
pub async fn count_table(pool: &PgPool, table: &str) -> Result<i64, sqlx::Error> {
    // Xác thực tên bảng với danh sách cho phép trước khi khẳng định an toàn
    let allowed = ["users", "products", "orders"];
    if !allowed.contains(&table) {
        return Err(sqlx::Error::Configuration("Invalid table".into()));
    }
    
    let query = format!("SELECT COUNT(*) as count FROM {}", table);
    let row: (i64,) = sqlx::query_as(&AssertSqlSafe(query))
        .fetch_one(pool)
        .await?;
    Ok(row.0)
}

Đầu vào từ người dùng đi qua parameterized query với placeholder $1. SQL động từ các nguồn đáng tin cậy yêu cầu bọc rõ ràng bằng AssertSqlSafe sau khi xác thực.

Sẵn sàng chinh phục phỏng vấn Rust?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Migration Cơ Sở Dữ Liệu với sqlx-cli

Công cụ sqlx-cli quản lý các migration schema với các file SQL có phiên bản. Migration được chạy theo thứ tự, và công cụ theo dõi những migration nào đã được áp dụng.

bash
# Cài đặt CLI (0.9.x không còn cần --locked)
cargo install sqlx-cli --features postgres

# Tạo migration mới
sqlx migrate add create_users_table

# Chạy các migration đang chờ
sqlx migrate run

# Kiểm tra trạng thái migration
sqlx migrate info

Mỗi migration tạo một cặp file trong thư mục migrations/:

sql
-- migrations/20260912120000_create_users_table.up.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
sql
-- migrations/20260912120000_create_users_table.down.sql
DROP TABLE IF EXISTS users;

File up.sql được chạy khi migrate run, và down.sql khi migrate revert. Giữ cả hai file cho phép rollback các deployment thất bại.

Chế Độ Offline cho Pipeline CI

Kiểm tra tại thời điểm biên dịch yêu cầu kết nối cơ sở dữ liệu trong quá trình build. Đối với môi trường CI không có quyền truy cập cơ sở dữ liệu, SQLx cache metadata truy vấn trong thư mục .sqlx/.

bash
# Tạo cache truy vấn cục bộ (yêu cầu DATABASE_URL)
cargo sqlx prepare

# Commit thư mục .sqlx/
git add .sqlx/
git commit -m "Update SQLx query cache"

Trong CI, build sử dụng metadata đã cache:

yaml
# .github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      SQLX_OFFLINE: true
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: cargo build --release

Biến môi trường SQLX_OFFLINE=true cho SQLx biết sử dụng metadata đã cache thay vì kết nối đến cơ sở dữ liệu. Cách tiếp cận này giữ CI nhanh và tránh thông tin xác thực cơ sở dữ liệu trong môi trường build.

Connection Pooling với PgPool

SQLx cung cấp connection pooling tích hợp. PgPool quản lý một pool các kết nối, tái sử dụng chúng qua các request thay vì mở kết nối mới mỗi lần.

src/main.rsrust
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = PgPoolOptions::new()
        .max_connections(10)
        .min_connections(2)
        .acquire_timeout(Duration::from_secs(3))
        .idle_timeout(Duration::from_secs(600))
        .connect(&std::env::var("DATABASE_URL").expect("DATABASE_URL required"))
        .await?;
    
    // Pool là Clone và Send, an toàn để chia sẻ giữa các task
    let app_state = AppState { db: pool };
    
    // Khởi động web server với app_state...
    Ok(())
}

Đặt max_connections dựa trên giới hạn kết nối của cơ sở dữ liệu và số lượng instance ứng dụng. Quy tắc thông thường: database_max_connections / số_instance.

Câu Hỏi Phỏng Vấn: SQLx và Truy Cập Cơ Sở Dữ Liệu trong Rust

Các cuộc phỏng vấn kỹ thuật cho vị trí backend Rust thường đào sâu vào việc xử lý cơ sở dữ liệu. Những câu hỏi này kiểm tra hiểu biết về các đảm bảo compile-time của SQLx và các pattern async.

H: SQLx xác minh truy vấn tại thời điểm biên dịch như thế nào?

Các macro query! kết nối đến cơ sở dữ liệu đang chạy trong quá trình cargo build. Chúng phân tích SQL, gửi đến cơ sở dữ liệu để lập kế hoạch (không thực thi), và xác minh rằng tên cột tồn tại, kiểu dữ liệu khớp, và truy vấn hợp lệ về cú pháp. Query planner của cơ sở dữ liệu thực hiện xác thực, nên các trường hợp đặc biệt của Postgres, MySQL, hoặc SQLite đều được phát hiện.

H: Điều gì xảy ra nếu schema cơ sở dữ liệu thay đổi sau khi biên dịch?

Binary đã biên dịch chứa query plan từ thời điểm build. Nếu schema thay đổi (cột đổi tên, kiểu thay đổi), truy vấn sẽ thất bại tại runtime với lỗi decode hoặc column-not-found. Các nhóm giảm thiểu điều này bằng cách chạy migration trước deployment và rebuild sau khi thay đổi schema. Trong production, blue-green deployment đảm bảo binary mới và schema mới được triển khai cùng lúc.

H: Khi nào nên chọn SQLx thay vì Diesel hoặc SeaORM?

SQLx phù hợp với các dự án mà lập trình viên muốn kiểm soát hoàn toàn SQL mà không cần học DSL. Diesel tạo SQL từ code Rust và phát hiện thay đổi schema tại thời điểm biên dịch thông qua file schema.rs. SeaORM cung cấp API kiểu ActiveRecord với migration. SQLx phù hợp nhất khi nhóm viết SQL phức tạp (CTE, window function, tính năng đặc thù của database) và muốn kiểm tra compile-time mà không có abstraction.

H: Giải thích sự đánh đổi giữa query! và query_as!

Macro query! trả về kiểu record ẩn danh với các trường khớp với cột SELECT. Macro query_as! ánh xạ kết quả vào struct có tên triển khai FromRow. Sử dụng query! cho các truy vấn một lần mà việc định nghĩa struct thêm overhead. Sử dụng query_as! khi kiểu kết quả xuất hiện ở nhiều nơi hoặc cần triển khai trait.

rust
// query! trả về record ẩn danh
let row = sqlx::query!("SELECT id, name FROM users WHERE id = $1", user_id)
    .fetch_one(pool)
    .await?;
let name: String = row.name; // Truy cập theo tên trường

// query_as! ánh xạ vào struct
let user: User = sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", user_id)
    .fetch_one(pool)
    .await?;

H: Xử lý transaction trong SQLx như thế nào?

Bắt đầu transaction với pool.begin(), thực thi truy vấn trên đối tượng transaction, sau đó gọi commit() hoặc để nó drop để rollback.

src/db/orders.rsrust
pub async fn create_order_with_items(
    pool: &PgPool,
    order: NewOrder,
    items: Vec<OrderItem>,
) -> Result<i32, sqlx::Error> {
    let mut tx = pool.begin().await?;
    
    let order_id: (i32,) = sqlx::query_as(
        "INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id"
    )
    .bind(order.user_id)
    .bind(order.total)
    .fetch_one(&mut *tx)
    .await?;
    
    for item in items {
        sqlx::query(
            "INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)"
        )
        .bind(order_id.0)
        .bind(item.product_id)
        .bind(item.quantity)
        .execute(&mut *tx)
        .await?;
    }
    
    tx.commit().await?;
    Ok(order_id.0)
}

Nếu bất kỳ truy vấn nào thất bại, transaction tự động rollback khi tx được drop. Rollback rõ ràng hiếm khi cần thiết.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Các Pattern Production cho SQLx 0.9

Các pattern này xử lý các tình huống production phổ biến: xử lý cột nullable, kiểu tùy chỉnh, và thao tác batch.

Cột Nullable và Kiểu Option

rust
#[derive(FromRow)]
pub struct UserProfile {
    pub id: i32,
    pub email: String,
    pub avatar_url: Option<String>, // Cột nullable ánh xạ thành Option
    pub bio: Option<String>,
}

pub async fn update_profile(
    pool: &PgPool,
    user_id: i32,
    avatar_url: Option<&str>,
    bio: Option<&str>,
) -> Result<(), sqlx::Error> {
    sqlx::query!(
        r#"
        UPDATE users 
        SET avatar_url = COALESCE($2, avatar_url),
            bio = COALESCE($3, bio),
            updated_at = NOW()
        WHERE id = $1
        "#,
        user_id,
        avatar_url,
        bio
    )
    .execute(pool)
    .await?;
    Ok(())
}

Batch Insert với UNNEST

Đối với bulk insert, UNNEST tránh nhiều round trip:

rust
pub async fn insert_tags(pool: &PgPool, tags: &[String]) -> Result<Vec<i32>, sqlx::Error> {
    let ids: Vec<(i32,)> = sqlx::query_as(
        r#"
        INSERT INTO tags (name)
        SELECT * FROM UNNEST($1::text[])
        ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
        RETURNING id
        "#
    )
    .bind(tags)
    .fetch_all(pool)
    .await?;
    
    Ok(ids.into_iter().map(|(id,)| id).collect())
}

Tổng Kết về SQLx trong Rust

  • Các macro query! xác minh SQL tại thời điểm biên dịch với cơ sở dữ liệu thực, phát hiện lỗi đánh máy và không khớp kiểu trước runtime
  • SQLx 0.9 giới thiệu sqlx.toml cho ghi đè kiểu, cấu hình đa cơ sở dữ liệu, và tải extension SQLite
  • SqlSafeStr ngăn chặn SQL injection bằng cách giới hạn chuỗi truy vấn ở giá trị tĩnh trừ khi được bọc rõ ràng trong AssertSqlSafe
  • Sử dụng cargo sqlx prepare để cache metadata truy vấn cho build offline trong CI
  • PgPool xử lý connection pooling với giới hạn, timeout, và cài đặt idle có thể cấu hình
  • Migration được lưu trong các file SQL quản lý bởi sqlx-cli, với cặp up/down để hỗ trợ rollback
  • Để chuẩn bị phỏng vấn Rust, thực hành giải thích xác minh compile-time, sự đánh đổi query! vs query_as!, và các pattern xử lý transaction
Thử thách hôm nay

Bạn có tìm ra lỗi trong Rust không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 12 tháng 9, 2026

Thẻ

#rust
#sqlx
#database
#postgresql
#interview

Chia sẻ

Bài viết liên quan