Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Cài PostgreSQL 17 qua
docker composecross-platform (Mac/Linux/Windows). - Cài
sqlx-clicho migration tooling. - Hiểu format connection string
postgres://user:pass@host:port/db. - Setup
.envfile choDATABASE_URL(sqlx compile-time check yêu cầu). - Init crate mới
shop-db— workspace member thứ 3. - Hiểu khác biệt
sqlx::query!vssqlx::query_as!vs runtimequery(). - Refactor
product_serviceShop API từ in-memoryVec<Product>sang DB query.
PostgreSQL 17 Qua Docker Compose
Shop API dùng Docker thay vì cài PostgreSQL native vì 4 lý do thực tế:
- Cross-platform consistent — image y hệt nhau trên macOS, Linux, Windows; loại bỏ class lỗi "works on my machine".
- Version lock chính xác — pin tag
postgres:17-alpine, mọi máy dev cùng minor version. - Cleanup dễ — drop container + volume xóa sạch, không rác trong system Postgres.
- Multi-project isolation — mỗi project DB riêng container riêng port riêng, không xung đột.
Tạo file docker-compose.yml ở root project:
# File: docker-compose.yml (root project)
services:
postgres:
image: postgres:17-alpine
container_name: shop_postgres
restart: unless-stopped
environment:
POSTGRES_USER: shop
POSTGRES_PASSWORD: shop_dev_password
POSTGRES_DB: shop_dev
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U shop -d shop_dev"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
driver: local
Khởi chạy container:
docker compose up -d postgres
docker compose ps # status
docker compose logs -f postgres # follow log
Test connection bằng psql bên trong container:
docker compose exec postgres psql -U shop -d shop_dev -c "SELECT version();"
# PostgreSQL 17.x on x86_64-pc-linux-gnu, ...
Dừng container hoặc cleanup hoàn toàn:
docker compose down # dừng container, giữ volume
docker compose down -v # dừng + xóa volume (MẤT toàn bộ data)
Lock decision Shop API B51 vĩnh viễn:
- Image:
postgres:17-alpine(size nhỏ ~80MB, security update nhanh). - User / DB name:
shop / shop_dev(dev local), production sẽ tách credential riêng qua secret manager (G19). - Port:
5432(default Postgres, dev expose ra host). - Volume named
postgres_datapersist data qua restart container. healthcheck pg_isreadyCI/CD ready —docker compose upchờ healthy trước khi expose port.
.env File + DATABASE_URL
sqlx-cli + macro sqlx::query!/query_as! đọc DATABASE_URL từ environment để kết nối DB lúc compile + introspect schema. Tạo file .env ở root project:
# File: .env (root project, LOCAL ONLY — KHÔNG commit)
DATABASE_URL=postgres://shop:shop_dev_password@localhost:5432/shop_dev
RUST_LOG=shop_api=debug,sqlx=warn,tower_http=debug
Format connection string chuẩn:
postgres://USER:PASSWORD@HOST:PORT/DATABASE?param1=value1¶m2=value2
- Scheme
postgres://hoặcpostgresql://(alias, sqlx accept cả hai). - URL-encode password nếu có ký tự đặc biệt (
@→%40,:→%3A,/→%2F). - Query params:
sslmode=requireép TLS,application_name=shop_apihiển thị trongpg_stat_activity,connect_timeout=10giây.
Thêm .env vào .gitignore tránh leak credential:
# File: .gitignore (extend B51)
/target
.env
.env.local
.env.*.local
Tạo .env.example commit vào git làm template cho dev mới onboard:
# File: .env.example (commit vào git)
DATABASE_URL=postgres://shop:CHANGE_ME@localhost:5432/shop_dev
RUST_LOG=shop_api=debug,sqlx=warn,tower_http=debug
Lock decision Shop API B51: .env LOCAL ONLY, KHÔNG commit; .env.example template commit. Production set env qua orchestrator (Docker secrets, Kubernetes ConfigMap + Secret, fly.io secret, Railway variable) — KHÔNG dùng file .env trong production (lock B10 continued).
Cài sqlx-cli
sqlx-cli là binary command-line tách rời crate sqlx, cung cấp 3 nhóm lệnh:
sqlx migrate— quản lý migration (add,run,revert,info).sqlx database—create/dropdatabase qua connection string.sqlx prepare— generate offline query cache.sqlx/folder cho CI/CD build không cần DB sống.
Install qua cargo install:
cargo install sqlx-cli --no-default-features --features postgres,rustls
Hai flag quan trọng:
--no-default-featuresbỏ MySQL + SQLite driver (Shop API chỉ dùng Postgres, tránh tốn dung lượng + build time).--features rustlsdùng pure-Rust TLS thay native-tls — cross-platform không cần OpenSSL system library.
Verify cài đặt:
sqlx --version
# sqlx-cli 0.8.x
Test database create (noop nếu container đã tạo DB qua POSTGRES_DB environment ở bước 2):
sqlx database create
# database "shop_dev" already exists, skipping
Lệnh đọc DATABASE_URL từ .env tự động (sqlx-cli tích hợp dotenv loader). Nếu không có .env, export biến thủ công: export DATABASE_URL=postgres://....
Init Crate shop-db
shop-db là workspace member thứ 3 (sau shop-api binary + shop-common lib từ B10), đóng vai trò PostgreSQL adapter — đóng gói pool config + repository function + migration folder. Tạo crate mới qua cargo:
cargo new --lib crates/shop-db
Cập nhật workspace root Cargo.toml thêm member + dep mới:
# File: Cargo.toml (workspace root, extend B51)
[workspace]
members = ["crates/shop-api", "crates/shop-common", "crates/shop-db"]
resolver = "3"
[workspace.dependencies]
# ... existing deps B10/B19/B41
sqlx = { version = "0.8", features = [
"postgres",
"runtime-tokio",
"tls-rustls",
"macros",
"chrono",
"uuid",
"rust_decimal",
] }
dotenvy = "0.15"
Tạo crates/shop-db/Cargo.toml:
# File: crates/shop-db/Cargo.toml
[package]
name = "shop-db"
version = "0.1.0"
edition = "2024"
[dependencies]
sqlx = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true }
rust_decimal = { workspace = true }
shop-common = { path = "../shop-common" }
Tạo file entry crates/shop-db/src/lib.rs:
// File: crates/shop-db/src/lib.rs
pub mod pool;
pub mod products;
pub use pool::create_pool;
Tạo crates/shop-db/src/pool.rs chứa factory function tạo connection pool:
// File: crates/shop-db/src/pool.rs
use sqlx::postgres::{PgPool, PgPoolOptions};
use std::time::Duration;
pub async fn create_pool(database_url: &str) -> Result<PgPool, sqlx::Error> {
PgPoolOptions::new()
.max_connections(20)
.min_connections(2)
.acquire_timeout(Duration::from_secs(5))
.idle_timeout(Duration::from_secs(60))
.test_before_acquire(true)
.connect(database_url)
.await
}
Giải nghĩa từng config field:
max_connections(20)cap tối đa connection mở đồng thời. Postgres defaultmax_connections = 100chia per service — Shop API lấy 20, chừa cho worker (G21), CLI (G29), admin tool kết nối song song.min_connections(2)keep warm 2 connection idle, tránh cost handshake lần đầu request → throughput tăng ~15%.acquire_timeout(5s)chờ tối đa 5 giây nếu pool full → trả errorPoolTimedOut; cao hơn dễ ăn cascade timeout HTTP, thấp hơn flap khi spike.idle_timeout(60s)release connection idle quá 60s → tiết kiệm slot Postgres, vẫn giữmin_connections2 warm.test_before_acquire(true)ping (SELECT 1) trước khi cấp connection → tránh stale connection (NAT timeout, Postgres restart). Cost ~1ms/request, đáng đổi cho reliability dev local.
Pool config sẽ tune chi tiết theo workload thật ở B57 (Pool Tuning Sâu) — B51 dùng config sweet-spot startup-grade.
First Migration: Tạo Bảng products
Migration là SQL script versioned đặt trong folder migrations/, sqlx track version đã apply qua bảng nội bộ _sqlx_migrations. Tạo migration đầu tiên qua sqlx migrate add:
sqlx migrate add --source crates/shop-db/migrations create_products
# Creating crates/shop-db/migrations/20260615120000_create_products.sql
Flag --source chỉ folder migration (lock Shop API: crates/shop-db/migrations/). Filename prefix là timestamp YYYYMMDDHHMMSS sqlx tự sinh — sort theo thứ tự chronological.
Edit file SQL vừa tạo:
-- crates/shop-db/migrations/20260615120000_create_products.sql
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
price NUMERIC(15, 2) NOT NULL CHECK (price > 0),
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX products_slug_idx ON products(slug);
CREATE INDEX products_created_at_idx ON products(created_at DESC);
Apply migration:
sqlx migrate run --source crates/shop-db/migrations
# Applied 20260615120000/migrate create_products (1.2ms)
Verify bảng đã tạo trong psql:
docker compose exec postgres psql -U shop -d shop_dev -c "\d products"
Type mapping lock B51 vĩnh viễn cho Shop API:
- Rust
ProductId(u64)↔ PostgresBIGSERIAL(id auto-increment 64-bit). sqlx casti64↔u64qua newtype implementation B44. - Rust
Money(Decimal)↔ PostgresNUMERIC(15, 2)— scale 2 cho VND (2 chữ số thập phân), precision 15 max value999_999_999_999.99(~1000 tỷ VND, dư cho mọi giá Shop API). - Rust
String↔ PostgresTEXT— performance trong Postgres làTEXT=VARCHAR=CHAR(N)identical, KHÔNG dùngVARCHAR(N)vì validate length ở app level quavalidatorcrate (lock B41). - Rust
u32 stock↔ PostgresINT32-bit signed (Shop API cast safe: stock luôn không âm + max 1 triệu lock B42, fit trongi32range). - Rust
chrono::DateTime<Utc>↔ PostgresTIMESTAMPTZ(timestamp with time zone, store UTC + offset).
Lock decision: KHÔNG dùng VARCHAR(N) trong schema — Postgres TEXT không limit length, validate ở app level qua validator attribute #[validate(length(min=3, max=200))] (B41 lock). Lý do: thay limit qua migration tốn DB lock + downtime, sửa attribute Rust chỉ deploy code mới.
query! Vs query_as! Vs Runtime query()
sqlx cung cấp 3 cách viết query, mỗi cách có trade-off riêng giữa safety và flexibility:
sqlx::query!macro — compile-time check SQL syntax + type binding + column type. KHÔNG return struct (trả anonymous record tuple-like access qua field name).sqlx::query_as!macro — compile-time check + return struct user-defined (struct field name PHẢI match column name).sqlx::query()runtime function — KHÔNG compile-time check, flexible cho dynamic SQL (filter user input, sort by user input, query builder).
Bảng so sánh nhanh:
Function | Compile check | Return type | Use case
query! | YES | Anonymous record | INSERT/UPDATE/DELETE
query_as! | YES | Custom struct | SELECT mapped struct
query | NO | PgRow (manual map) | Dynamic SQL (search builder)
Compile-time check yêu cầu sqlx kết nối DB sống lúc build qua DATABASE_URL ENV, hoặc đọc offline cache .sqlx/ folder generate bởi sqlx prepare (CI/CD pattern). Macro query!/query_as! introspect schema → type column → so sánh type Rust → compile error nếu mismatch.
Lock decision Shop API B51 vĩnh viễn:
- Static SQL (CRUD đa số) →
query!/query_as!— safety tối đa, compile catch bug schema change. - Dynamic SQL (search filter, sort by user input từ query string) →
query()runtime kết hợpQueryBuilder(B59 deep dive G10). - CI/CD setup
sqlx preparegenerate.sqlx/cache commit vào git, CI build không cần DB sống (deep B53).
Refactor product_service Từ In-Memory Sang DB
Tạo crates/shop-db/src/products.rs chứa struct ProductRow map DB row + 4 CRUD function:
// File: crates/shop-db/src/products.rs
use sqlx::PgPool;
use rust_decimal::Decimal;
use chrono::{DateTime, Utc};
pub struct ProductRow {
pub id: i64,
pub name: String,
pub slug: String,
pub price: Decimal,
pub stock: i32,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn list_products(
pool: &PgPool,
limit: i64,
offset: i64,
) -> Result<Vec<ProductRow>, sqlx::Error> {
sqlx::query_as!(
ProductRow,
r#"
SELECT id, name, slug, price, stock, description, created_at, updated_at
FROM products
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
"#,
limit,
offset
)
.fetch_all(pool)
.await
}
pub async fn count_products(pool: &PgPool) -> Result<i64, sqlx::Error> {
let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM products")
.fetch_one(pool)
.await?;
Ok(row.0)
}
pub async fn create_product(
pool: &PgPool,
name: &str,
slug: &str,
price: Decimal,
stock: i32,
description: Option<&str>,
) -> Result<ProductRow, sqlx::Error> {
sqlx::query_as!(
ProductRow,
r#"
INSERT INTO products (name, slug, price, stock, description)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, name, slug, price, stock, description, created_at, updated_at
"#,
name,
slug,
price,
stock,
description
)
.fetch_one(pool)
.await
}
pub async fn find_by_slug(
pool: &PgPool,
slug: &str,
) -> Result<Option<ProductRow>, sqlx::Error> {
sqlx::query_as!(
ProductRow,
r#"
SELECT id, name, slug, price, stock, description, created_at, updated_at
FROM products
WHERE slug = $1
"#,
slug
)
.fetch_optional(pool)
.await
}
4 function pattern chuẩn cho mọi entity Shop API tương lai (Order, Cart, User, Category — G7+):
list_xxx(pool, limit, offset)paginated SELECT với ORDER BY created_at DESC.count_xxx(pool)SELECT COUNT(*) cho pagination metadata.create_xxx(pool, ...)INSERT + RETURNING * tiết kiệm round-trip.find_by_xxx(pool, key)SELECT WHERE vớifetch_optionaltrảOption<T>.
Update crates/shop-api/src/state.rs thêm field db: PgPool:
// File: crates/shop-api/src/state.rs (extend B51)
use sqlx::PgPool;
use std::sync::Arc;
use shop_common::config::AppConfig;
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
pub config: Arc<AppConfig>,
}
impl AppState {
pub fn new(db: PgPool, config: AppConfig) -> Self {
Self {
db,
config: Arc::new(config),
}
}
}
PgPool nội bộ là Arc<SharedPool> — clone-cheap (atomic increment) — share OK cross handler qua State<AppState> extractor.
Update crates/shop-api/src/main.rs wire pool + apply migration on startup:
// File: crates/shop-api/src/main.rs (extend B51)
use shop_db::create_pool;
use shop_common::config::AppConfig;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
tracing_subscriber::fmt::init();
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set in .env");
let db = create_pool(&database_url).await?;
tracing::info!("connected to PostgreSQL");
// Apply migrations on startup (dev only — production tách lệnh riêng B52)
sqlx::migrate!("../shop-db/migrations").run(&db).await?;
tracing::info!("migrations applied");
let config = AppConfig::from_env()?;
let state = AppState::new(db, config);
let app = build_router(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
tracing::info!("listening on {}", listener.local_addr()?);
axum::serve(listener, app).await?;
Ok(())
}
Update crates/shop-api/Cargo.toml thêm dep:
# File: crates/shop-api/Cargo.toml (extend B51)
[dependencies]
# ... existing deps
shop-db = { path = "../shop-db" }
sqlx = { workspace = true }
dotenvy = { workspace = true }
Handler list_products + create_product ở crates/shop-api/src/routes/products.rs sẽ refactor sang dùng shop_db::products::* ở B52 — B51 chỉ wire infrastructure pool + migration on startup, KHÔNG chạm logic handler.
Verify End-To-End
Khởi động container + chạy app:
docker compose up -d postgres
cargo run -p shop-api
# connected to PostgreSQL
# migrations applied
# listening on 0.0.0.0:3000
Insert product test qua psql cho data sống:
docker compose exec postgres psql -U shop -d shop_dev -c \
"INSERT INTO products (name, slug, price, stock) VALUES ('iPhone 15', 'iphone-15', 25000000.00, 10);"
# INSERT 0 1
Verify row trong DB:
-- chạy trong psql session
SELECT id, name, slug, price, stock, created_at FROM products;
-- id | name | slug | price | stock | created_at
-- ----+----------+----------+-------------+-------+-------------------------------
-- 1 | iPhone 15| iphone-15| 25000000.00 | 10 | 2026-06-15 12:30:15.234+00
Verify HTTP endpoint (handler list_products vẫn skeleton vec![] in-memory ở B51 — refactor sang shop_db ở B52):
curl http://localhost:3000/api/v1/products
# {"items":[],"total":0,"page":1,"size":20,"hasNext":false}
# Note: B51 mới setup DB pool — handler chuyển sang shop_db ở B52
Verify pool log trong RUST_LOG=sqlx=info:
RUST_LOG=sqlx=info,shop_api=debug cargo run -p shop-api
# sqlx::pool: pool established min=2 max=20
# shop_api: listening on 0.0.0.0:3000
Suggested commit: B51: PostgreSQL 17 docker-compose + sqlx-cli + shop-db crate init + products migration.
Tổng Kết
- PostgreSQL 17 alpine qua Docker Compose — cross-platform, version lock, cleanup dễ, multi-project isolation.
shop / shop_dev / port 5432lock connection dev local; production tách credential qua secret manager (G19)..envlocal only (.gitignore),.env.exampletemplate commit cho onboarding.sqlx-cliinstall với featurepostgres,rustls— skip MySQL/SQLite, skip OpenSSL native.- Crate
shop-dbinit lần đầu — workspace member thứ 3 (saushop-api,shop-common). - Pool config:
max(20),min(2),acquire_timeout(5s),idle_timeout(60s),test_before_acquire(true). - First migration
create_products:BIGSERIAL+NUMERIC(15, 2)+TEXT+INT+TIMESTAMPTZ+ 2 index. - Type mapping lock B51:
Money(Decimal)↔NUMERIC(15, 2),ProductId(u64)↔BIGSERIAL, RustString↔TEXT(KHÔNGVARCHAR). - 3 cách query:
query!/query_as!(compile-time check),query()runtime cho dynamic SQL. AppState.db: PgPool— clone-cheap (Arcinternal) share OK cross handler.- Migration on startup dev only — production tách lệnh riêng (B52 deep).
- File path lock:
crates/shop-db/{src/lib.rs, src/pool.rs, src/products.rs, migrations/}.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Tại sao Shop API chọn Docker Compose thay vì cài PostgreSQL native? Liệt kê 3 lý do.
- Connection string
postgres://...có những phần nào? Cho ví dụ kèm SSL param + application_name. - Pool config
max(20) + min(2)— tại sao không đểmax(100)? Postgres defaultmax_connectionslà bao nhiêu? sqlx::query!macro vssqlx::query()runtime — khi nào chọn cái nào? Trade-off compile-check vs flexibility.- Tại sao map
MoneysangNUMERIC(15, 2)thayBIGINT cents? Trade-off chính xác vs performance + storage.
Đáp án
- Docker Compose vs PostgreSQL native — 3 lý do chính: (a) Cross-platform consistent — image
postgres:17-alpiney hệt nhau trên macOS Apple Silicon + Intel, Linux x86_64 + ARM, Windows WSL2. Loại bỏ class lỗi "works on my machine" do version Postgres khác nhau giữa dev (vd dev Mac dùng Postgres 16 Homebrew, dev Linux dùng 15 apt, production deploy 17 — mỗi env behavior khác ở edge case như JSON path operator, partition pruning, parallel query). Docker pin tag chính xác mọi dev cùng version. (b) Cleanup dễ + multi-project isolation —docker compose down -vdrop container + volume xóa sạch trong 5 giây; cài native phảibrew uninstall postgresql+rm -rf /usr/local/var/postgres+ uninstall extension; mỗi project Shop API + portfolio site + side project có DB riêng container riêng port riêng (5432, 5433, 5434) không xung đột global Postgres single instance. Reset DB test =docker compose down -v && docker compose up -d postgres10 giây, native phảiDROP DATABASE+CREATE DATABASE+ restore schema. (c) Version lock + CI/CD parity —docker-compose.ymlcommit vào git là source-of-truth version, lockimage: postgres:17-alpinemọi dev pull cùng version; CI/CD GitHub Actions cũng dùngservices: postgres: image: postgres:17-alpiney hệt local; production deploy AWS RDS Postgres 17 cùng major version; staging/prod/CI/local đồng bộ 100%. Native install version trôi theo OS update — risk bug fix differ. Bonus lý do thứ 4: healthcheck CI/CD ready —pg_isreadyprobe đảm bảo container healthy trước khi expose port, CI test không flake do race condition Postgres chưa ready accept connection. - Connection string format:
postgres://USER:PASSWORD@HOST:PORT/DATABASE?param1=value1¶m2=value2. 6 phần: (a) Scheme:postgres://hoặcpostgresql://(alias chính thức RFC 3986, sqlx accept cả hai). (b) USER:PASSWORD: credential phân cách bằng:; URL-encode password nếu chứa ký tự đặc biệt —@→%40,:→%3A,/→%2F,?→%3F,#→%23(vd passwordp@ss:w0rdencodep%40ss%3Aw0rd). (c) HOST: hostname hoặc IP (localhost,127.0.0.1,db.internal.aws.com, Unix socket%2Fvar%2Frun%2FpostgresqlURL-encoded). (d) PORT: default 5432, có thể skip nếu dùng default. (e) DATABASE: tên DB sau path/. (f) Query params:?param=value¶m=valuecấu hình connection. Ví dụ production-grade với SSL + monitoring:postgres://shop_prod:Xy9%40z%[email protected]:5432/shop_prod?sslmode=require&sslrootcert=/etc/ssl/rds-ca.pem&application_name=shop_api_v2.3.1&connect_timeout=10&statement_timeout=30000. Giải nghĩa params:sslmode=requireép TLS encrypt traffic chống MITM trong VPC AWS;sslrootcertverify cert AWS RDS chống cert giả mạo;application_name=shop_api_v2.3.1identifier hiển thị trongpg_stat_activitygiúp DBA debug query slow đến từ service nào version nào;connect_timeout=10giây timeout handshake TCP + auth;statement_timeout=30000ms (30s) cap mọi query — abort runaway query chống lock table dài. Shop API B51 dev local đơn giảnpostgres://shop:shop_dev_password@localhost:5432/shop_devkhông cần SSL (loopback) + không cần app_name (1 service local). Production G19 sẽ wire đầy đủ. - Pool config max(20) vs max(100) — Postgres default limit: (a) Postgres default
max_connections = 100set trongpostgresql.confngay sau install — đây là HARD LIMIT toàn instance, share giữa mọi client kết nối (app, monitoring, DBA query, replication, background worker Postgres nội bộ). (b) Mỗi connection ăn ~10MB RAM trong Postgres (backend process per connection, không async như Rust) — 100 connection = 1GB RAM chỉ cho connection state, chưa kể query buffer + sort memory + shared_buffers. (c) Shop API lấy 20 chia cho service khác:shop-api20 +shop-workerG21 dự kiến 10 +shop-cliG29 dự kiến 5 + admin tool ad-hoc 5 + monitoring (Prometheus pg_exporter, pgAnalyze) 10 + replication slot 3 + DBA reserved 10 = ~63 connection. Còn 37 connection buffer cho spike + Postgres internal. Nếushop-apiăn hết 100 → mọi service khác không connect được → cascade failure. (d) 20 connection đủ cho throughput với async runtime: 1 connection xử lý 100-500 req/s tùy query complexity (P50 query 5ms = 200 req/s/connection; pool 20 = 4000 req/s tổng). Shop API target 1000 req/s đủ thừa. (e) PgBouncer transaction pooling tăng effective connection 10x (G19 production), giảm cần thiết tăngmax_connections. (f) Trade-off tăng max: tăng 20 → 50 không giúp throughput (CPU bottleneck trước connection), tăngmax_connectionsPostgres 100 → 500 → contention spinlock + ProcArray scan O(N) khi backend nhiều → P99 query tăng. Recommendation chung:max_connectionsPostgres = 2-4x CPU core; mỗi service ăn 10-30% tổng max. Lock Shop API B51: max 20 sweet spot, tune deep B57 theo benchmark workload thật. sqlx::query!macro vssqlx::query()runtime — decision matrix: (a)sqlx::query!macro chọn khi: SQL static viết tay lúc compile (không build chuỗi runtime), schema biết trước, dev muốn safety tối đa. Pros: compile-time check SQL syntax (typo column → compile error thay runtime panic), check type binding ($1bindStringvào columnBIGINT→ compile error), check column type return (struct fieldprice: i64nhưng columnNUMERIC→ compile error), refactor schema column rename → compile catch mọi query bị break. Cons: yêu cầuDATABASE_URLENV set lúc build (CI/CD setup phức tạp hơn) hoặcsqlx preparegenerate.sqlx/cache; KHÔNG support dynamic SQL (filter user input nối chuỗi); slower compile vì macro introspect DB schema mỗi build (cache giảm impact). Use case: 90% CRUD operation Shop API — SELECT by ID, INSERT new row, UPDATE field cố định, DELETE WHERE pk. (b)sqlx::query()runtime function chọn khi: SQL dynamic build từ runtime (search filter optional, sort column từ user input whitelist, query builderQueryBuilder::new(...).push(...)), legacy SQL từ file external, migration script. Pros: KHÔNG cần DB sống lúc build, flexible build chuỗi SQL runtime, integrate query builder pattern, dynamic column selection. Cons: KHÔNG compile-time check (runtime error nếu column sai, type mismatch), manualrow.try_get::<Type, _>("column")mapping verbose, dễ SQL injection nếu nối chuỗi user input thay.bind(value). Decision Shop API: (i) CRUD đa số (B62-B67 G7) →query!/query_as!default; (ii) search products với filter optional (B98 G10) →QueryBuilder+query()runtime build WHERE chain; (iii) admin sort by column user input (B99 G10) →query()runtime với whitelist column name validate trước khi nối chuỗi. CI/CD setupsqlx preparegenerate.sqlx/cache commit git, build trên CI không cần PostgreSQL service (B53 deep dive). Lock Shop API B51 vĩnh viễn: safety-first →query!default, runtimequery()chỉ cho dynamic SQL có justification rõ ràng.Money↔NUMERIC(15, 2)vsBIGINT cents— trade-off chính xác/performance/storage: (a) NUMERIC(15, 2) approach — Postgres native arbitrary-precision decimal, scale 2 cho VND (2 chữ số thập phân), precision 15 max999_999_999_999.99(~1000 tỷ VND). Pros: trực quan đọc DBSELECT price FROM productstrả25000000.00đúng định dạng người dùng nhìn; map thẳngrust_decimal::Decimalkhông cần convert; aggregateSUM(price),AVG(price)chính xác tuyệt đối không float drift; CHECK constraintprice > 0rõ ràng; report tài chính + accounting compliance (PCI DSS, IFRS) yêu cầu exact decimal. Cons: storage 8-32 bytes per row tùy precision (1000 tỷ row × 16 bytes = 16TB chỉ cho price column — concern khi scale Google/Amazon level, không phải Shop API); arithmetic chậm hơn integer 3-5x (CPU native int 64-bit fast path vs NUMERIC software arbitrary precision); index B-tree comparison chậm hơn integer. (b)BIGINT centsapproach — store 25000000 VND =25000000BIGINT (đơn vị nhỏ nhất VND không có decimal cent), USD store cents$25.00=2500BIGINT. Pros: storage 8 bytes fixed; arithmetic native int 64-bit fast; index B-tree comparison fast; aggregate sum/avg integer overflow safe trong i64 range (~9 quintillion); join/group by hiệu năng cao hơn. Cons: convert tầng app mỗi lần đọc/ghi (DB2500→ app$25.00), dễ bug forget divide 100; aggregate racentsphải convert thủ công; report SQL ad-hoc DBA phải nhớ divide; multi-currency phức tạp (USD 100 cents/dollar, JPY không cent, BTC 8 decimal satoshi — mỗi currency factor khác); edge case currency exchange rate decimal 6 (vd 1 USD = 24566.123456 VND) không fit cents integer. Decision Shop API B51 lock NUMERIC(15, 2): (i) Shop API throughput 1000 req/s không phải Amazon scale 100K req/s — performance NUMERIC negligible; (ii) accounting compliance rõ ràng cho audit; (iii)rust_decimal::Decimalmap clean không cần convert tầng app; (iv) JSON wire format string"25000000.00"tránh JS f64 precision loss (lock B44 continued); (v) multi-currency tương lai G24 vẫn dùng NUMERIC(15, 2) per currency, không cần refactor. Lock B51 vĩnh viễn: mọi money column Shop APINUMERIC(15, 2); nếu nghiệp vụ cần precision khác (BTC 8 decimal) sẽ tạo column riêngNUMERIC(20, 8)không reuseprice.
Bài Tiếp Theo
Bài 52: sqlx Migration System Sâu — chi tiết sqlx migration: up-only vs reversible, naming convention timestamp, embed migration into binary với migrate!() macro, transaction-wrapped migration, rollback strategy production, refactor handler product_service Shop API hoàn chỉnh dùng shop_db.
