Rust และ SQLx ในปี 2026: การตรวจสอบ Query ขณะ Compile และคำถามสัมภาษณ์งาน

เรียนรู้วิธีที่ SQLx 0.9 ตรวจสอบ SQL query ขณะ compile, การกำหนดค่า sqlx.toml และเตรียมตัวสัมภาษณ์งานตำแหน่ง backend Rust

บทเรียน Rust SQLx พร้อมการตรวจสอบ Query ขณะ Compile

SQLx 0.9 เปลี่ยนแปลงวิธีการเข้าถึงฐานข้อมูลใน Rust โดยการจับข้อผิดพลาด SQL ขณะ compile แทนที่จะรอจนถึง runtime เวอร์ชันนี้เปิดตัวในเดือนพฤษภาคม 2026 พร้อมระบบการกำหนดค่าใหม่ผ่าน sqlx.toml ความยืดหยุ่นของ runtime และความปลอดภัยของ query ที่เข้มงวดขึ้นผ่าน trait SqlSafeStr

จุดเด่นของ SQLx

ต่างจาก ORM ที่สร้าง SQL จาก struct ของ Rust, SQLx ตรวจสอบ SQL ที่เขียนด้วยมือกับฐานข้อมูลจริงขณะ compile การพิมพ์ชื่อคอลัมน์ผิดจะทำให้ build ล้มเหลว ไม่ใช่เกิดข้อผิดพลาดใน production

การตรวจสอบ Query ขณะ Compile ใน SQLx 0.9

Macro query! ตรวจสอบทุก SQL statement กับ schema ของฐานข้อมูลระหว่าง cargo build คุณสมบัตินี้ตรวจจับการพิมพ์ผิด ประเภทข้อมูลไม่ตรงกัน และคอลัมน์ที่หายไปก่อนที่โค้ดจะทำงาน

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> {
    // ตรวจสอบขณะ compile: ชื่อคอลัมน์, ประเภทข้อมูล และโครงสร้างที่ return
    sqlx::query_as!(
        User,
        r#"
        SELECT id, email, created_at
        FROM users
        WHERE email = $1
        "#,
        email
    )
    .fetch_optional(pool)
    .await
}

Macro query_as! จับคู่แถวผลลัพธ์โดยตรงกับ struct User ถ้าตาราง users ไม่มีคอลัมน์ created_at การ compile จะล้มเหลวพร้อมข้อความแสดงข้อผิดพลาดที่ชี้ไปยังบรรทัดที่มีปัญหา

ระบบการกำหนดค่า sqlx.toml

SQLx 0.9 แนะนำ ไฟล์ sqlx.toml ที่รวมการกำหนดค่าฐานข้อมูล การ override ประเภทข้อมูล และการตั้งค่าหลายฐานข้อมูลไว้ที่เดียว วิธีนี้แทนที่ตัวแปร environment ที่กระจัดกระจายและทำให้ CI pipeline ง่ายขึ้น

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

การ override ประเภทข้อมูลขจัดการแปลงประเภทซ้ำซ้อน ทุกคอลัมน์ UUID จะถูกจับคู่กับ uuid::Uuid โดยอัตโนมัติ และทุก timestamptz กับ chrono::DateTime โดยไม่ต้องมี annotation ต่อ query

SqlSafeStr: ป้องกัน SQL Injection ในระดับประเภทข้อมูล

SQLx 0.9 เสริมความปลอดภัยของ query โดยกำหนดให้ใช้ SqlSafeStr สำหรับ parameter ที่เป็น string ตามค่าเริ่มต้น เฉพาะ &'static str เท่านั้นที่ implement trait นี้ ป้องกัน string แบบ dynamic ที่อาจมี injection payload

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

// ชื่อตารางแบบ static: compile ได้โดยตรง
const TABLE: &str = "products";

pub async fn search_products(
    pool: &PgPool,
    user_query: &str,
) -> Result<Vec<Product>, sqlx::Error> {
    // ปลอดภัย: $1 เป็นค่า parameter ไม่ใช่ SQL ที่ถูก interpolate
    sqlx::query_as!(
        Product,
        r#"SELECT * FROM products WHERE name ILIKE '%' || $1 || '%'"#,
        user_query
    )
    .fetch_all(pool)
    .await
}

// ตารางแบบ dynamic จาก config: ครอบด้วย AssertSqlSafe หลังจากตรวจสอบ
pub async fn count_table(pool: &PgPool, table: &str) -> Result<i64, sqlx::Error> {
    // ตรวจสอบชื่อตารางกับ allowlist ก่อน assert safety
    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)
}

Input จากผู้ใช้ผ่าน parameterized query ด้วย placeholder $1 SQL แบบ dynamic จากแหล่งที่เชื่อถือได้ต้องครอบด้วย AssertSqlSafe อย่างชัดเจนหลังจากตรวจสอบ

พร้อมที่จะพิชิตการสัมภาษณ์ Rust แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การ Migrate ฐานข้อมูลด้วย sqlx-cli

เครื่องมือ sqlx-cli จัดการ schema migration ด้วยไฟล์ SQL ที่มีเวอร์ชัน Migration จะทำงานตามลำดับ และเครื่องมือจะติดตามว่า migration ไหนถูกใช้แล้ว

bash
# ติดตั้ง CLI (0.9.x ไม่ต้องใช้ --locked อีกต่อไป)
cargo install sqlx-cli --features postgres

# สร้าง migration ใหม่
sqlx migrate add create_users_table

# รัน migration ที่รอดำเนินการ
sqlx migrate run

# ตรวจสอบสถานะ migration
sqlx migrate info

แต่ละ migration สร้างคู่ไฟล์ในไดเรกทอรี 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;

ไฟล์ up.sql ทำงานเมื่อ migrate run และ down.sql เมื่อ migrate revert การเก็บทั้งสองไฟล์ช่วยให้สามารถ rollback deployment ที่ล้มเหลวได้

โหมด Offline สำหรับ CI Pipeline

การตรวจสอบขณะ compile ต้องการการเชื่อมต่อฐานข้อมูลระหว่าง build สำหรับ CI environment ที่ไม่มีการเข้าถึงฐานข้อมูล SQLx cache metadata ของ query ในไดเรกทอรี .sqlx/

bash
# สร้าง query cache ในเครื่อง (ต้องการ DATABASE_URL)
cargo sqlx prepare

# Commit ไดเรกทอรี .sqlx/
git add .sqlx/
git commit -m "Update SQLx query cache"

ใน CI, build ใช้ 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

ตัวแปร environment SQLX_OFFLINE=true บอก SQLx ให้ใช้ metadata ที่ cache แทนการเชื่อมต่อฐานข้อมูล วิธีนี้ทำให้ CI เร็วและหลีกเลี่ยงข้อมูลประจำตัวฐานข้อมูลใน build environment

Connection Pooling ด้วย PgPool

SQLx มี connection pooling ในตัว PgPool จัดการ pool ของการเชื่อมต่อ นำกลับมาใช้ใหม่ข้าม request แทนที่จะเปิดการเชื่อมต่อใหม่ทุกครั้ง

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 เป็น Clone และ Send ปลอดภัยที่จะแชร์ข้าม task
    let app_state = AppState { db: pool };
    
    // เริ่ม web server ด้วย app_state...
    Ok(())
}

ตั้งค่า max_connections ตามขีดจำกัดการเชื่อมต่อของฐานข้อมูลและจำนวน instance ของแอปพลิเคชัน กฎทั่วไป: database_max_connections / จำนวน_instance

คำถามสัมภาษณ์: SQLx และการเข้าถึงฐานข้อมูลใน Rust

การสัมภาษณ์เทคนิคสำหรับตำแหน่ง backend Rust มักเจาะลึกเรื่องการจัดการฐานข้อมูล คำถามเหล่านี้ทดสอบความเข้าใจเกี่ยวกับการรับประกันขณะ compile ของ SQLx และ pattern แบบ async

ถ: SQLx ตรวจสอบ query ขณะ compile อย่างไร?

Macro query! เชื่อมต่อกับฐานข้อมูลที่ทำงานอยู่ระหว่าง cargo build มัน parse SQL ส่งไปยังฐานข้อมูลเพื่อวางแผน (โดยไม่ execute) และตรวจสอบว่าชื่อคอลัมน์มีอยู่ ประเภทข้อมูลตรงกัน และ query ถูกต้องตาม syntax Query planner ของฐานข้อมูลทำการตรวจสอบ ดังนั้น edge case เฉพาะของ Postgres, MySQL หรือ SQLite จะถูกจับได้

ถ: จะเกิดอะไรขึ้นถ้า schema ฐานข้อมูลเปลี่ยนหลังจาก compile?

Binary ที่ compile แล้วมี query plan จากเวลา build ถ้า schema เปลี่ยน (คอลัมน์ถูกเปลี่ยนชื่อ ประเภทเปลี่ยน) query จะล้มเหลวขณะ runtime ด้วย decode error หรือ column-not-found error ทีมลดปัญหานี้โดยรัน migration ก่อน deployment และ rebuild หลังจากเปลี่ยน schema ใน production, blue-green deployment รับประกันว่า binary ใหม่และ schema ใหม่จะ deploy พร้อมกัน

ถ: เมื่อไหร่ควรเลือก SQLx แทน Diesel หรือ SeaORM?

SQLx เหมาะกับโปรเจกต์ที่นักพัฒนาต้องการควบคุม SQL เต็มที่โดยไม่ต้องเรียน DSL Diesel สร้าง SQL จากโค้ด Rust และตรวจจับการเปลี่ยนแปลง schema ขณะ compile ผ่านไฟล์ schema.rs SeaORM ให้ API แบบ ActiveRecord พร้อม migration SQLx เหมาะที่สุดเมื่อทีมเขียน SQL ที่ซับซ้อน (CTE, window function, feature เฉพาะของ database) และต้องการตรวจสอบขณะ compile โดยไม่มี abstraction

ถ: อธิบาย tradeoff ระหว่าง query! และ query_as!

Macro query! return ประเภท record แบบไม่ระบุชื่อที่มี field ตรงกับคอลัมน์ SELECT Macro query_as! จับคู่ผลลัพธ์กับ struct ที่มีชื่อซึ่ง implement FromRow ใช้ query! สำหรับ query ครั้งเดียวที่การกำหนด struct เพิ่ม overhead ใช้ query_as! เมื่อประเภทผลลัพธ์ปรากฏในหลายที่หรือต้อง implement trait

rust
// query! return record แบบไม่ระบุชื่อ
let row = sqlx::query!("SELECT id, name FROM users WHERE id = $1", user_id)
    .fetch_one(pool)
    .await?;
let name: String = row.name; // เข้าถึงด้วยชื่อ field

// query_as! จับคู่กับ struct
let user: User = sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", user_id)
    .fetch_one(pool)
    .await?;

ถ: จัดการ transaction ใน SQLx อย่างไร?

เริ่ม transaction ด้วย pool.begin() execute query บน transaction object จากนั้นเรียก commit() หรือปล่อยให้มัน 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)
}

ถ้า query ใดล้มเหลว transaction จะ rollback อัตโนมัติเมื่อ tx ถูก drop การ rollback แบบชัดเจนไม่ค่อยจำเป็น

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

Pattern สำหรับ Production ใน SQLx 0.9

Pattern เหล่านี้จัดการสถานการณ์ production ทั่วไป: การจัดการคอลัมน์ nullable ประเภทที่กำหนดเอง และการทำงานแบบ batch

คอลัมน์ Nullable และประเภท Option

rust
#[derive(FromRow)]
pub struct UserProfile {
    pub id: i32,
    pub email: String,
    pub avatar_url: Option<String>, // คอลัมน์ nullable จับคู่กับ 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 ด้วย UNNEST

สำหรับ bulk insert, UNNEST หลีกเลี่ยง 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())
}

สรุปเกี่ยวกับ SQLx ใน Rust

  • Macro query! ตรวจสอบ SQL ขณะ compile กับฐานข้อมูลจริง จับการพิมพ์ผิดและประเภทไม่ตรงก่อน runtime
  • SQLx 0.9 แนะนำ sqlx.toml สำหรับ override ประเภท การกำหนดค่าหลายฐานข้อมูล และการโหลด extension ของ SQLite
  • SqlSafeStr ป้องกัน SQL injection โดยจำกัด query string ให้เป็นค่า static เว้นแต่จะครอบด้วย AssertSqlSafe อย่างชัดเจน
  • ใช้ cargo sqlx prepare เพื่อ cache metadata ของ query สำหรับ build แบบ offline ใน CI
  • PgPool จัดการ connection pooling ด้วยขีดจำกัด timeout และการตั้งค่า idle ที่กำหนดค่าได้
  • Migration เก็บในไฟล์ SQL ที่จัดการโดย sqlx-cli พร้อมคู่ up/down สำหรับการสนับสนุน rollback
  • สำหรับการเตรียมสัมภาษณ์ Rust ฝึกอธิบายการตรวจสอบขณะ compile, tradeoff ของ query! vs query_as! และ pattern การจัดการ transaction
ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Rust เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 12 กันยายน 2569

แท็ก

#rust
#sqlx
#database
#postgresql
#interview

แชร์

บทความที่เกี่ยวข้อง

แผนภาพการจัดการหน่วยความจำของสมาร์ตพอยน์เตอร์ Rust Box, Rc, Arc และ RefCell

สมาร์ตพอยน์เตอร์ใน Rust อธิบายครบ: Box, Rc, Arc และ RefCell ปี 2026

อธิบายสมาร์ตพอยน์เตอร์ Box, Rc, Arc และ RefCell ใน Rust พร้อมตัวอย่างที่คอมไพล์ได้ปี 2026 ตารางตัดสินใจ และคำถามสัมภาษณ์ที่พบบ่อย

คู่มือขั้นสูง Rust traits และ generics พร้อมตัวอย่างโค้ดและโลโก้ปู Rust

Rust Traits และ Generics ปี 2026: Trait Upcasting, AsyncFn และรูปแบบขั้นสูง

คู่มือเชิงลึกเกี่ยวกับ Rust traits และ generics พร้อมฟีเจอร์ใหม่จาก Rust 2024 Edition: trait upcasting, AsyncFn closures, RPITIT และรูปแบบขั้นสูงที่ใช้ในการสัมภาษณ์จริง

แผนภาพแสดงระบบ Ownership และ Borrowing ของภาษา Rust

ทำความเข้าใจ Ownership และ Borrowing ใน Rust อย่างลึกซึ้ง สำหรับนักพัฒนาและผู้เตรียมสอบสัมภาษณ์

บทความเจาะลึกระบบ Ownership และ Borrowing ของ Rust ตั้งแต่พื้นฐานจนถึงขั้นสูง พร้อมตัวอย่างโค้ดจริงและแนวทางแก้ปัญหา Compiler Error ที่พบบ่อย เหมาะสำหรับการเตรียมสัมภาษณ์งาน