Migrations ด้วย sqlx-cli
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”เรานำ schema ที่ออกแบบไว้ในบทเรียนก่อนหน้า มาเปลี่ยนเป็น migration จริงที่มีเวอร์ชัน — ไฟล์ SQL ที่มีหมายเลข เก็บใน git ซึ่งสร้างฐานข้อมูลทั้งหมดขึ้นมาใหม่จากศูนย์ เราจะใช้ sqlx-cli ตัวช่วยฝั่ง command-line ของ crate SQLx ที่ backend ฝั่ง Rust พึ่งพาอยู่แล้ว
เมื่อจบบทเรียน คุณจะมี:
- ฐานข้อมูล
taskflowที่สร้างบน PostgreSQL เครื่องคุณ - ไฟล์
migrations/0001_init.sqlที่สร้าง extensionpgcrypto, ทั้งเจ็ดตาราง และทั้งสาม index - migration นั้นถูกนำไปใช้แล้วและตรวจสอบด้วย
psql
migration คือการเปลี่ยนแปลง schema ฐานข้อมูลที่มีเวอร์ชันและจัดลำดับ เก็บเป็นไฟล์ควบคู่ไปกับโค้ดของคุณ แทนที่จะคลิกไปมาใน GUI หรือรันคำสั่ง CREATE TABLE แบบ ad-hoc ที่ไม่มีใครทำซ้ำได้ คุณเขียนการเปลี่ยนแปลงครั้งเดียวแล้ว commit ลงไป จากนั้นทุกสภาพแวดล้อม — แล็ปท็อปคุณ แล็ปท็อปเพื่อนร่วมทีม CI production — ก็นำขั้นตอนเดียวกันไปใช้ในลำดับเดียวกันเป๊ะ ๆ
ทำไมต้องจัดเวอร์ชัน schema ด้วยล่ะ?
- ทำซ้ำได้ (Reproducibility) checkout ใหม่บวก
sqlx migrate runให้ทุกคนได้ฐานข้อมูลที่เหมือนกันทุกไบต์ ไม่มี schema drift แบบ “มันรันได้บนเครื่องฉันนะ” - ประวัติ (History) ไดเรกทอรี
migrations/คือบันทึกตามลำดับเวลาว่า schema วิวัฒน์มาอย่างไร0001_init.sqlวันนี้0002_add_due_date.sqlเดือนหน้า — เรื่องราวอยู่ตรงนั้นใน git - ความปลอดภัย (Safety) SQLx บันทึกว่า migration ไหนรันไปแล้วในตารางติดตาม
_sqlx_migrationsดังนั้นการรันคำสั่งซ้ำสองครั้งจึงปลอดภัย: ระบบจะข้าม migration ที่ใช้ไปแล้ว - CI/CD คำสั่งเดียวกันที่ตั้งค่าฐานข้อมูลบนเครื่องคุณ ก็ตั้งค่าฐานข้อมูลทดสอบใน CI และฐานข้อมูลจริงตอน deploy
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”migration แบบไฟล์ด้วย sqlx-cli (สิ่งที่เราใช้)
- ข้อดี: SQL ล้วน ๆ ที่คุณควบคุมเต็มที่ ไม่มี ORM มาบังว่าจริง ๆ แล้วรันอะไร; migration อยู่ใน git ข้างโค้ด; SQLx ตรวจ migration เทียบกับฐานข้อมูลตอน compile ในโมดูลต่อ ๆ ไป; ไบนารีเล็ก ๆ ตัวเดียว ไม่มีบริการ runtime เพิ่ม
- ข้อเสีย: คุณเขียน SQL เอง (ถือเป็นข้อดีในคอร์ส แต่อาจน่าเบื่อในทีมใหญ่);
sqlx-cliให้ migration แบบ “up” ไปข้างหน้าอย่างเดียวโดยค่าเริ่มต้นในโหมดง่ายสุด ดังนั้นการย้อนกลับหมายถึงเขียน migration ใหม่; และคุณต้องรักษาลำดับ migration ให้ปลอดภัยต่อ FK เอง
migration ที่สร้างโดย ORM / auto-diff (ทางเลือก)
- ข้อดี: เครื่องมือ diff โมเดลของคุณเทียบกับฐานข้อมูลและเขียน migration ให้ เขียน SQL ด้วยมือน้อยลง
- ข้อเสีย: SQL ที่สร้างขึ้นอาจน่าประหลาดใจหรือไม่ปลอดภัยกับตารางขนาดใหญ่ คุณผูกกับ convention ของ ORM นั้น และ abstraction ทำให้ให้เหตุผลได้ยากขึ้นว่าอะไรกันแน่ที่ลงไปที่ฐานข้อมูล — ตรงกันข้ามกับสิ่งที่เราต้องการตอนเรียนรู้
สำหรับคอร์สที่เป้าหมายคือเข้าใจ stack จากบนถึงล่าง migration SQL ที่เขียนด้วยมือคือทางเลือกที่ถูกต้อง
ลงมือสร้าง
หัวข้อที่มีชื่อว่า “ลงมือสร้าง”ติดตั้ง sqlx-cli
หัวข้อที่มีชื่อว่า “ติดตั้ง sqlx-cli”ถ้ายังไม่มี ติดตั้ง CLI พร้อมเฉพาะ driver ของ PostgreSQL:
cargo install sqlx-cli --no-default-features --features rustls,postgresชี้ไปยังฐานข้อมูลของคุณ
หัวข้อที่มีชื่อว่า “ชี้ไปยังฐานข้อมูลของคุณ”sqlx-cli อ่าน connection string จากตัวแปรสภาพแวดล้อม DATABASE_URL ซึ่งอยู่ใน .env ที่คุณสร้างไว้ในโมดูล setup อยู่แล้ว:
export DATABASE_URL=postgres://taskflow:taskflow@localhost:5432/taskflowตรวจให้แน่ใจว่าคอนเทนเนอร์ PostgreSQL จากโมดูล Docker Compose กำลังทำงานอยู่ก่อนไปต่อ
สร้างฐานข้อมูล
หัวข้อที่มีชื่อว่า “สร้างฐานข้อมูล”sqlx database createคำสั่งนี้สร้างฐานข้อมูล taskflow ตามชื่อใน DATABASE_URL ถ้ายังไม่มีอยู่ (มีคำสั่งคู่กัน sqlx database drop สำหรับตอนที่คุณอยากเริ่มใหม่จากศูนย์)
เพิ่ม migration แรก
หัวข้อที่มีชื่อว่า “เพิ่ม migration แรก”sqlx migrate add initคำสั่งนี้สร้างไดเรกทอรี migrations/ (ถ้ายังไม่มี) และวางไฟล์ว่างที่ขึ้นต้นด้วย timestamp หรือลำดับหมายเลข เพื่อความชัดเจนตลอดคอร์สนี้ เราจะเรียกไฟล์นั้นว่า migrations/0001_init.sql เปิดขึ้นมาแล้วเติมเนื้อหา
เขียน migrations/0001_init.sql
หัวข้อที่มีชื่อว่า “เขียน migrations/0001_init.sql”นี่คือ schema ทั้งหมดใน migration เดียว ลำดับสำคัญ: ตารางที่มี foreign key ต้องสร้าง หลัง ตารางปลายทางที่ชี้ไป ตาราง join มาถัดมา แล้วปิดท้ายด้วย index เมื่อตารางที่เกี่ยวข้องมีครบแล้ว
-- 0001_init.sql — initial TaskFlow schema
-- gen_random_uuid() lives in the pgcrypto extension.-- It ships with PostgreSQL 13+ but must be enabled per-database.create extension if not exists pgcrypto;
-- Accountscreate table users ( id uuid primary key default gen_random_uuid(), email text unique not null, password_hash text not null, display_name text not null, created_at timestamptz not null default now());
-- Boards owned by a usercreate table boards ( id uuid primary key default gen_random_uuid(), owner_id uuid not null references users(id) on delete cascade, title text not null, created_at timestamptz not null default now());
-- Board membership (many-to-many: users <-> boards)create table board_members ( board_id uuid not null references boards(id) on delete cascade, user_id uuid not null references users(id) on delete cascade, role text not null default 'member', primary key (board_id, user_id));
-- Columns (lanes) on a boardcreate table columns ( id uuid primary key default gen_random_uuid(), board_id uuid not null references boards(id) on delete cascade, title text not null, position double precision not null);
-- Cards inside a columncreate table cards ( id uuid primary key default gen_random_uuid(), column_id uuid not null references columns(id) on delete cascade, title text not null, description text, position double precision not null, created_at timestamptz not null default now());
-- Labels defined on a boardcreate table labels ( id uuid primary key default gen_random_uuid(), board_id uuid not null references boards(id) on delete cascade, name text not null, color text not null);
-- Card <-> label tagging (many-to-many)create table card_labels ( card_id uuid not null references cards(id) on delete cascade, label_id uuid not null references labels(id) on delete cascade, primary key (card_id, label_id));
-- Indexes for the hot lookup pathscreate index on board_members(user_id);create index on columns(board_id);create index on cards(column_id, position);มีบางจุดที่ควรหยุดพิจารณา:
create extension if not exists pgcrypto;ต้องมาก่อน เพราะทุกcreate tableด้านล่างพึ่งgen_random_uuid()เป็นค่าเริ่มต้น บน PostgreSQL 13+ ฟังก์ชันนี้มาพร้อมเครื่อง — extension แค่ต้องถูกเปิดใช้ครั้งเดียวต่อฐานข้อมูล- ลำดับที่ปลอดภัยต่อ FK
usersก่อนboardsก่อนcolumnsก่อนcards; ตาราง joinboard_membersและcard_labelsมาท้ายสุด เพราะแต่ละตัวอ้างถึงแม่สองตาราง สลับลำดับนี้แล้ว PostgreSQL จะปฏิเสธ migration พร้อม error “relation does not exist” - index อยู่ท้ายสุด — คุณสร้าง index ให้ได้เฉพาะตารางที่มีอยู่แล้ว ทั้งสามตัวนี้ครอบคลุมคิวรีที่เราจะรันบ่อยจริง ๆ บทเรียนถัดไป อธิบายว่าทำไมแต่ละตัวจึงมีรูปร่างแบบนั้น
รัน migration
หัวข้อที่มีชื่อว่า “รัน migration”sqlx migrate runSQLx นำ migration ที่ค้างอยู่ทุกตัวไปใช้ตามลำดับและบันทึกไว้ในตาราง _sqlx_migrations รันครั้งที่สองจะพิมพ์ว่าไม่มีอะไรต้องทำ เพราะข้าม migration ที่ apply ไปแล้ว จึงปลอดภัยที่จะรันซ้ำใน CI และตอน deploy
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”เปิดเซสชัน psql กับฐานข้อมูลเดียวกันแล้วลิสต์ตาราง:
psql "$DATABASE_URL" -c '\dt'คุณควรเห็นทั้งเจ็ดตารางบวกตารางจดบันทึกของ SQLx:
List of relations Schema | Name | Type | Owner--------+------------------+-------+---------- public | board_members | table | taskflow public | boards | table | taskflow public | card_labels | table | taskflow public | cards | table | taskflow public | columns | table | taskflow public | labels | table | taskflow public | users | table | taskflow public | _sqlx_migrations | table | taskflow(8 rows)ตรวจตัวอย่างคอลัมน์ของตารางหนึ่งเพื่อยืนยันว่ารูปร่างลงถูก:
psql "$DATABASE_URL" -c '\d cards'ผลลัพธ์ควรลิสต์ id, column_id, title, description, position และ created_at พร้อม index cards_column_id_position_idx แนบอยู่ด้านล่าง ถ้าทั้งสองคำสั่งดูถูกต้อง แสดงว่า schema ทำงานจริงแล้ว
คุณติดตั้ง sqlx-cli สร้างฐานข้อมูล taskflow และเพิ่ม migration แรก คุณเขียน migrations/0001_init.sql — extension pgcrypto, ทั้งเจ็ดตารางในลำดับที่ปลอดภัยต่อ foreign key และทั้งสาม index — จากนั้นนำไปใช้ด้วย sqlx migrate run และยืนยันผลด้วย psql \dt ตอนนี้ schema มีเวอร์ชันอยู่ใน git และทำซ้ำได้บนทุกเครื่อง ต่อไป เราจะเจาะลึกกลยุทธ์การจัดเรียงเบื้องหลังคอลัมน์ position เหล่านั้นใน indexes & ordering