# Rust และ SQLx ในปี 2026: การตรวจสอบ Query ขณะ Compile และคำถามสัมภาษณ์งาน > เรียนรู้วิธีที่ SQLx 0.9 ตรวจสอบ SQL query ขณะ compile, การกำหนดค่า sqlx.toml และเตรียมตัวสัมภาษณ์งานตำแหน่ง backend Rust - Published: 2026-09-12 - Updated: 2026-09-12 - Author: Anthony Fillion-Maillet - Tags: rust, sqlx, database, postgresql, interview - Reading time: 12 min --- 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!](https://docs.rs/sqlx/latest/sqlx/macro.query.html) ตรวจสอบทุก SQL statement กับ schema ของฐานข้อมูลระหว่าง `cargo build` คุณสมบัตินี้ตรวจจับการพิมพ์ผิด ประเภทข้อมูลไม่ตรงกัน และคอลัมน์ที่หายไปก่อนที่โค้ดจะทำงาน ```rust // src/db/users.rs use sqlx::{FromRow, PgPool}; #[derive(FromRow)] pub struct User { pub id: i32, pub email: String, pub created_at: chrono::DateTime, } pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result, 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](https://github.com/launchbadge/sqlx) ที่รวมการกำหนดค่าฐานข้อมูล การ 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" [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 ```rust // src/db/search.rs use sqlx::{AssertSqlSafe, PgPool}; // ชื่อตารางแบบ static: compile ได้โดยตรง const TABLE: &str = "products"; pub async fn search_products( pool: &PgPool, user_query: &str, ) -> Result, 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 { // ตรวจสอบชื่อตารางกับ 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 อย่างชัดเจนหลังจากตรวจสอบ ## การ 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 แทนที่จะเปิดการเชื่อมต่อใหม่ทุกครั้ง ```rust // src/main.rs 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 ```rust // src/db/orders.rs pub async fn create_order_with_items( pool: &PgPool, order: NewOrder, items: Vec, ) -> Result { 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, // คอลัมน์ nullable จับคู่กับ Option pub bio: Option, } 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, 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/rust/rust-sqlx-compile-time-queries-tutorial