Danh sách bài viết

Bài 51: PostgreSQL Setup Với sqlx

Bài 51 của series Rust RESTful API — mở Group 6 PostgreSQL + sqlx: cài đặt PostgreSQL 17 alpine qua docker-compose.yml cross-platform (Mac/Linux/Windows) với named volume postgres_data persist + healthcheck pg_isready CI/CD ready + connection lock shop / shop_dev / port 5432; .env file chứa DATABASE_URL=postgres://shop:shop_dev_password@localhost:5432/shop_dev format URL-encoded password + .gitignore local only + .env.example template commit cho onboarding; cài sqlx-cli qua cargo install sqlx-cli --no-default-features --features postgres,rustls skip MySQL/SQLite skip native TLS cross-platform không cần OpenSSL; init crate shop-db thứ 3 workspace member (sau shop-api + shop-common) với 3 file src/{lib,pool,products}.rs + migrations/ folder; create_pool function dùng PgPoolOptions config max_connections(20) cap tối đa + min_connections(2) keep warm + acquire_timeout(5s) trả error nhanh nếu pool full + idle_timeout(60s) release connection idle + test_before_acquire(true) ping connection tránh stale; first migration 20260615120000_create_products.sql với 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 nullable + created_at/updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + 2 index products_slug_idx + products_created_at_idx DESC; type mapping lock B51 MANDATORY: Money(Decimal) ↔ NUMERIC(15, 2) scale 2 cho VND max 999_999_999_999.99 + ProductId(u64) ↔ BIGSERIAL sqlx cast i64 ↔ u64 + Rust String ↔ TEXT (KHÔNG VARCHAR(N) validate app level qua validator crate B41) + u32 stock ↔ INT 32-bit + DateTime<Utc> ↔ TIMESTAMPTZ; 3 cách query — sqlx::query! macro compile-time check SQL syntax + type return anonymous record cho INSERT/UPDATE/DELETE, sqlx::query_as! macro compile-time check return struct mapped cho SELECT, sqlx::query() runtime function PgRow manual map cho dynamic SQL (search filter user input B59 deep); compile-time check yêu cầu DATABASE_URL ENV hoặc offline cache .sqlx/ folder cho CI/CD; lock Shop API static SQL (CRUD đa số) → query!/query_as! safety, dynamic SQL → query() runtime (G10); ProductRow struct 8 field DB mapping + 4 CRUD function list_products(pool, limit, offset) paginated query + count_products(pool) count tổng + create_product(pool, ...) insert + RETURNING + find_by_slug(pool, slug) option lookup; refactor AppState thêm field db: PgPool (PgPool clone-cheap Arc internal share OK cross handler) + main.rs load .env qua dotenvy + create_pool + sqlx::migrate!() apply migration on startup (dev only — production tách lệnh riêng B52); foundation cho B52 sqlx migration system sâu (up-only vs reversible + transaction-wrapped + rollback strategy production + refactor handler product_service Shop API hoàn chỉnh dùng shop_db), B57 pool tuning sâu, B59 dynamic query builder, B62 schema design đầy đủ 15 table Shop API.

15/06/2026
14 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Cài PostgreSQL 17 qua docker compose cross-platform (Mac/Linux/Windows).
  • Cài sqlx-cli cho migration tooling.
  • Hiểu format connection string postgres://user:pass@host:port/db.
  • Setup .env file cho DATABASE_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! vs sqlx::query_as! vs runtime query().
  • Refactor product_service Shop API từ in-memory Vec<Product> sang DB query.
2

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_data persist data qua restart container.
  • healthcheck pg_isready CI/CD ready — docker compose up chờ healthy trước khi expose port.
3

.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&param2=value2
  • Scheme postgres:// hoặc postgresql:// (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_api hiển thị trong pg_stat_activity, connect_timeout=10 giâ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).

4

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 databasecreate/drop database 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-features bỏ MySQL + SQLite driver (Shop API chỉ dùng Postgres, tránh tốn dung lượng + build time).
  • --features rustls dù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://....

5

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 default max_connections = 100 chia 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ả error PoolTimedOut; 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_connections 2 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.

6

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) ↔ Postgres BIGSERIAL (id auto-increment 64-bit). sqlx cast i64u64 qua newtype implementation B44.
  • Rust Money(Decimal) ↔ Postgres NUMERIC(15, 2) — scale 2 cho VND (2 chữ số thập phân), precision 15 max value 999_999_999_999.99 (~1000 tỷ VND, dư cho mọi giá Shop API).
  • Rust String ↔ Postgres TEXT — performance trong Postgres là TEXT = VARCHAR = CHAR(N) identical, KHÔNG dùng VARCHAR(N) vì validate length ở app level qua validator crate (lock B41).
  • Rust u32 stock ↔ Postgres INT 32-bit signed (Shop API cast safe: stock luôn không âm + max 1 triệu lock B42, fit trong i32 range).
  • Rust chrono::DateTime<Utc> ↔ Postgres TIMESTAMPTZ (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.

7

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ợp QueryBuilder (B59 deep dive G10).
  • CI/CD setup sqlx prepare generate .sqlx/ cache commit vào git, CI build không cần DB sống (deep B53).
8

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ới fetch_optional trả 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_productcrates/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.

9

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.

10

Tổng Kết

  • PostgreSQL 17 alpine qua Docker Compose — cross-platform, version lock, cleanup dễ, multi-project isolation.
  • shop / shop_dev / port 5432 lock connection dev local; production tách credential qua secret manager (G19).
  • .env local only (.gitignore), .env.example template commit cho onboarding.
  • sqlx-cli install với feature postgres,rustls — skip MySQL/SQLite, skip OpenSSL native.
  • Crate shop-db init lần đầu — workspace member thứ 3 (sau shop-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, Rust StringTEXT (KHÔNG VARCHAR).
  • 3 cách query: query! / query_as! (compile-time check), query() runtime cho dynamic SQL.
  • AppState.db: PgPool — clone-cheap (Arc internal) 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/}.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Tại sao Shop API chọn Docker Compose thay vì cài PostgreSQL native? Liệt kê 3 lý do.
  2. Connection string postgres://... có những phần nào? Cho ví dụ kèm SSL param + application_name.
  3. Pool config max(20) + min(2) — tại sao không để max(100)? Postgres default max_connections là bao nhiêu?
  4. sqlx::query! macro vs sqlx::query() runtime — khi nào chọn cái nào? Trade-off compile-check vs flexibility.
  5. Tại sao map Money sang NUMERIC(15, 2) thay BIGINT cents? Trade-off chính xác vs performance + storage.
Đáp án
  1. Docker Compose vs PostgreSQL native — 3 lý do chính: (a) Cross-platform consistent — image postgres:17-alpine y 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 isolationdocker compose down -v drop container + volume xóa sạch trong 5 giây; cài native phải brew 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 postgres 10 giây, native phải DROP DATABASE + CREATE DATABASE + restore schema. (c) Version lock + CI/CD paritydocker-compose.yml commit vào git là source-of-truth version, lock image: postgres:17-alpine mọi dev pull cùng version; CI/CD GitHub Actions cũng dùng services: postgres: image: postgres:17-alpine y 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 readypg_isready probe đảm bảo container healthy trước khi expose port, CI test không flake do race condition Postgres chưa ready accept connection.
  2. Connection string format: postgres://USER:PASSWORD@HOST:PORT/DATABASE?param1=value1&param2=value2. 6 phần: (a) Scheme: postgres:// hoặc postgresql:// (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 password p@ss:w0rd encode p%40ss%3Aw0rd). (c) HOST: hostname hoặc IP (localhost, 127.0.0.1, db.internal.aws.com, Unix socket %2Fvar%2Frun%2Fpostgresql URL-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&param=value cấ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; sslrootcert verify cert AWS RDS chống cert giả mạo; application_name=shop_api_v2.3.1 identifier hiển thị trong pg_stat_activity giúp DBA debug query slow đến từ service nào version nào; connect_timeout=10 giây timeout handshake TCP + auth; statement_timeout=30000 ms (30s) cap mọi query — abort runaway query chống lock table dài. Shop API B51 dev local đơn giản postgres://shop:shop_dev_password@localhost:5432/shop_dev không cần SSL (loopback) + không cần app_name (1 service local). Production G19 sẽ wire đầy đủ.
  3. Pool config max(20) vs max(100) — Postgres default limit: (a) Postgres default max_connections = 100 set trong postgresql.conf ngay 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-api 20 + shop-worker G21 dự kiến 10 + shop-cli G29 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ếu shop-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ăng max_connections. (f) Trade-off tăng max: tăng 20 → 50 không giúp throughput (CPU bottleneck trước connection), tăng max_connections Postgres 100 → 500 → contention spinlock + ProcArray scan O(N) khi backend nhiều → P99 query tăng. Recommendation chung: max_connections Postgres = 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.
  4. sqlx::query! macro vs sqlx::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 ($1 bind String vào column BIGINT → compile error), check column type return (struct field price: i64 nhưng column NUMERIC → compile error), refactor schema column rename → compile catch mọi query bị break. Cons: yêu cầu DATABASE_URL ENV set lúc build (CI/CD setup phức tạp hơn) hoặc sqlx prepare generate .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 builder QueryBuilder::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), manual row.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 setup sqlx prepare generate .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, runtime query() chỉ cho dynamic SQL có justification rõ ràng.
  5. MoneyNUMERIC(15, 2) vs BIGINT 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 max 999_999_999_999.99 (~1000 tỷ VND). Pros: trực quan đọc DB SELECT price FROM products trả 25000000.00 đúng định dạng người dùng nhìn; map thẳng rust_decimal::Decimal không cần convert; aggregate SUM(price), AVG(price) chính xác tuyệt đối không float drift; CHECK constraint price > 0 rõ 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 cents approach — store 25000000 VND = 25000000 BIGINT (đơn vị nhỏ nhất VND không có decimal cent), USD store cents $25.00 = 2500 BIGINT. 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 (DB 2500 → app $25.00), dễ bug forget divide 100; aggregate ra cents phả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::Decimal map 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 API NUMERIC(15, 2); nếu nghiệp vụ cần precision khác (BTC 8 decimal) sẽ tạo column riêng NUMERIC(20, 8) không reuse price.
12

Bài Tiếp Theo

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