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.

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.
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.
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.
# 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.
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.
# 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 infoMỗi migration tạo một cặp file trong thư mục migrations/:
-- 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);-- 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/.
# 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:
# .github/workflows/ci.yml
jobs:
build:
runs-on: ubuntu-latest
env:
SQLX_OFFLINE: true
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --releaseBiế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.
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.
// 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.
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
#[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:
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
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ử.

Viết bởi
Anthony Fillion-MailletNgườ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ẻ
Chia sẻ
Bài viết liên quan

Con trỏ thông minh trong Rust: Box, Rc, Arc và RefCell năm 2026
Con trỏ thông minh Box, Rc, Arc và RefCell trong Rust được giải thích qua các ví dụ biên dịch được năm 2026, bảng quyết định và những câu hỏi phỏng vấn thường gặp.

Rust Traits và Generics năm 2026: Trait Upcasting, AsyncFn và Các Pattern Nâng Cao
Hướng dẫn chuyên sâu về traits và generics trong Rust với các tính năng mới của Edition 2024: trait upcasting, AsyncFn closures, RPITIT và các pattern nâng cao thường gặp trong phỏng vấn kỹ thuật.

Ownership và Borrowing trong Rust: Hướng dẫn toàn diện cho phỏng vấn kỹ thuật
Tìm hiểu sâu về hệ thống Ownership và Borrowing trong Rust — từ khái niệm cơ bản đến các pattern nâng cao giúp lập trình viên tự tin vượt qua phỏng vấn kỹ thuật.