บทเรียน 108 · Rails · Day 2

Migration — ใบสั่งช่างต่อเติมตู้ และแปลนตู้ฉบับล่าสุด

~25 นาที (บทนี้ลงมือเยอะ) · ควรผ่าน บทเรียน 107 (มีตาราง users · posts · comments ในเครื่องแล้ว) มาก่อน

เวลาจะเพิ่มช่องใหม่ในตาราง เช่น "คำโปรยใต้ชื่อบทความ" เราไม่เข้าไปแก้ฐานข้อมูลด้วยมือ แต่เขียนเป็นไฟล์หนึ่งไฟล์ แล้วให้ Rails ลงมือให้ — ไฟล์นั้นเรียกว่า migration
อ่านจบบทนี้ คุณจะ: เพิ่มคอลัมน์ด้วย migration ได้เอง · ถอยกลับได้ · อ่านออกว่า db/migrate/ กับ db/schema.rb ต่างกันยังไง · รู้ว่า Rails จำได้ยังไงว่าทำอะไรไปแล้ว · และรู้จักกับดักอันดับหนึ่งของมือใหม่: แก้ไฟล์ migration ที่รันไปแล้ว

🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้

🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 17 (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน ใช้แทนกันได้)

📦 ตัวอย่างในบทนี้ใช้แอป wongnok_blog ที่มีตาราง users / posts / comments และข้อมูลตัวอย่าง (seeds) ชุดเดียวกับบท 107 · ผลทุกก้อนรันจริงแล้วก็อปมา · ตัวเลขหน้าชื่อไฟล์ migration (เช่น 20261104114634) และเวลา (0.0033s) ของคุณจะไม่ตรงกับในบท เพราะมันคือวันเวลาที่คุณสั่ง

ปัญหา: ร้าน 5 สาขา ช่างต่อเติมตู้แบบปากเปล่า

schema คือ "หน้าตาของฐานข้อมูล" — มีตารางอะไร แต่ละตารางมีคอลัมน์ชื่ออะไร เก็บข้อมูลชนิดไหน · โค้ดของเราพึ่ง schema ตลอด เช่น ถ้าโค้ดอ่าน post.subtitle แต่ตารางไม่มีคอลัมน์ subtitle แอปพังทันที

ปัญหาคือ schema อยู่ในฐานข้อมูล ไม่ได้อยู่ในโค้ด · สไลด์หน้า 12 ยกสถานการณ์ให้: ทีม 5 คน · เครื่อง 5 เครื่อง · ฐานข้อมูลของเว็บจริง 1 ตัว

(เครื่องของเราแต่ละคนเรียกว่า development · เว็บจริงที่ลูกค้าใช้อยู่เรียกว่า production · แต่ละที่มีฐานข้อมูลของตัวเองแยกกัน)

migration แก้ทุกข้อด้วยวิธีเดียว: ทุกการเปลี่ยน schema ต้องเขียนเป็นไฟล์หนึ่งไฟล์เก็บใน git · ใครดึงโค้ดไปก็ได้ไฟล์ไปด้วย สั่ง bin/rails db:migrate ทีเดียว ฐานข้อมูลในเครื่องเขาก็หน้าตาเหมือนของเรา · และ git log (ดูประวัติ commit) ตอบได้ว่าใครเพิ่ม เพิ่มเมื่อไหร่ (คำว่า "ทำไม" ตอบได้ถ้าเขียน commit message ดี)

🏪 เทียบให้เห็นภาพ — แฟรนไชส์ 5 สาขา ช่างต่อเติมตู้เก็บแฟ้ม

ร้านเรามี 5 สาขา ทุกสาขาต้องมีตู้เก็บแฟ้ม (ฐานข้อมูล) หน้าตาเหมือนกันเป๊ะ เพราะพนักงาน (โค้ด) ย้ายสาขาไปมาแล้วต้องหาของเจอที่เดิม · ถ้าสาขาหนึ่งเรียกช่างมาเพิ่มช่องบนแฟ้มแบบบอกปากเปล่า สาขาอื่นไม่มีทางรู้ · ตู้แต่ละสาขาค่อย ๆ ไม่เหมือนกัน

migration คือใบสั่งช่าง — ทุกการต่อเติมต้องเขียนเป็นใบ มีวันที่ มีลำดับ เก็บรวมในแฟ้มกลางของแฟรนไชส์ (git) · สาขาไหนอยากเป็นปัจจุบัน ก็ไล่ทำใบที่ยังไม่ได้ทำจนครบ

ที่ไม่เหมือนกัน: ช่างจริงอ่านใบสั่งแล้วตีความเองได้ ("น่าจะหมายถึงลิ้นชักนี้") · แต่ Rails ทำตามที่เขียนเป๊ะ ๆ ไม่เดาให้ — เขียนให้ลบช่องไหน ข้อมูลในช่องนั้นหายจริง ไม่มีใครทักสักคำ

🗣 ศัพท์ที่เพิ่งเรียนไป
schema
หน้าตาของฐานข้อมูล: มีตารางอะไร คอลัมน์อะไร ชนิดไหน · ไม่รวมตัวข้อมูลข้างใน · ถาม senior ได้ว่า “PR นี้เปลี่ยน schema ไหม ถ้าเปลี่ยนมี migration มาด้วยหรือยัง”
migration
ไฟล์หนึ่งไฟล์ = การเปลี่ยน schema หนึ่งครั้ง · อยู่ใน db/migrate/ · เก็บใน git เหมือนโค้ด · พิมพ์บอก AI ได้ว่า “write a Rails migration that adds a subtitle string column to posts”
environment · production
ชุดเครื่อง + ฐานข้อมูลแต่ละที่: development = เครื่องเรา (ครัวทดลอง) · production = เว็บจริง หรือร้านจริงที่ลูกค้ากำลังนั่งกินอยู่ · (ระหว่างกลางบางทีมมี staging = ร้านซ้อมก่อนเปิดจริง) · ฐานข้อมูลแต่ละ environment แยกกัน ต้องสั่ง migrate แยกกัน · deploy = เอาโค้ดชุดใหม่ขึ้นร้านจริง · ถาม senior ได้ว่า “ตอน deploy ขึ้น production ใครเป็นคนสั่ง db:migrate หรือมันรันเอง”

เทียบกับ goose ที่เคยใช้ใน Go

ในบท 31 คุณเขียน migration ของ Go ด้วย goose มาแล้ว · แนวคิดเหมือนกันทุกอย่าง ต่างแค่ภาษาที่เขียน: goose ให้เขียน SQL ตรง ๆ สองท่อน (ทำ · ย้อน) ส่วน Rails ให้เขียน Ruby ท่อนเดียว แล้วมันคิดวิธีย้อนให้เอง

Go · goose — …_add_subtitle_to_posts.sql

-- +goose Up
ALTER TABLE posts ADD COLUMN subtitle VARCHAR;

-- +goose Down
ALTER TABLE posts DROP COLUMN subtitle;

เขียนเองทั้งขาไป (Up) และขากลับ (Down) · ลืมเขียน Down = ย้อนไม่ได้

Rails — …_add_subtitle_to_posts.rb

class AddSubtitleToPosts < ActiveRecord::Migration[8.1]
  def change
    add_column :posts, :subtitle, :string
  end
end

เขียนแค่ขาไปใน change · Rails รู้เองว่าขากลับของ add_column คือ remove_column

อ่านฝั่ง goose ทีละบรรทัด (SQL แปลเป็นภาษาคน):

ฝั่ง Rails อ่านทีละคำในหัวข้อถัดไป · ตอนนี้ดูแค่ว่ามันสั้นกว่า และไม่มีท่อนขากลับ (ไฟล์ goose ผมเขียนให้ดูเทียบ ไม่ได้รันในบทนี้ · ไฟล์ Rails คือไฟล์ที่ Rails สร้างให้จริง — เดี๋ยวได้สร้างเอง)

เรื่องgoose (บท 31)Rails
ไฟล์อยู่ไหนmigrations/*.sqldb/migrate/*.rb
สร้างไฟล์ใหม่goose create add_x sqlbin/rails generate migration AddXToY
ทำใบที่ค้างgoose upbin/rails db:migrate
ถอยหนึ่งใบgoose downbin/rails db:rollback
ดูว่าใบไหนทำแล้วgoose status (Pending / วันที่)bin/rails db:migrate:status (down / up)
สมุดจดในฐานข้อมูลตาราง goose_db_versionตาราง schema_migrations
แปลนตู้ฉบับล่าสุดไม่มีdb/schema.rb (Rails เขียนให้เอง — หัวข้อถัดไป)

ส่วน AutoMigrate ของ GORM (บท 27) ไม่ใช่ migration แบบนี้ — มันดู struct แล้ว "เติมของที่ขาด" อย่างเดียว ไม่มีไฟล์ ไม่มีประวัติ (บท 31 เห็นแล้วว่าเปลี่ยนชื่อ field แล้วได้คอลัมน์ว่างเพิ่มมาเฉย ๆ)

🗣 ศัพท์ที่เพิ่งเรียนไป
up · down · change
up = ขาไป (ทำตามใบ) · down = ขากลับ (รื้อออก) · change = method เดียวที่ Rails อ่านได้ทั้งสองทาง · Rails ยังให้เขียน def up / def down แยกแบบ goose ได้ด้วย ใช้ตอนที่ Rails เดาขากลับเองไม่ได้ (บท 109) · ถาม senior ได้ว่า “migration ตัวนี้ควรเขียน change หรือแยก up/down”
reversible (ย้อนกลับได้)
migration ที่ถอยกลับได้โดยไม่พัง · add_column · create_table ย้อนเองได้ · พิมพ์บอก AI ได้ว่า “make sure this migration is reversible”

เปิดแฟ้มใบสั่งช่าง: db/migrate/ ของจริง

ในแอปที่ทำต่อจากบท 107 มีใบสั่งช่างอยู่ 5 ใบ · ลองสั่ง ls ดู (ผลจริง):

$ ls db/migrate
20261104113601_create_posts.rb
20261104113603_add_published_and_views_to_posts.rb
20261104113604_create_users.rb
20261104113606_add_user_to_posts.rb
20261104113607_create_comments.rb

เปิดใบที่สองดู — ใบที่เพิ่มสองช่องให้ตาราง posts:

# db/migrate/20261104113603_add_published_and_views_to_posts.rb
class AddPublishedAndViewsToPosts < ActiveRecord::Migration[8.1]
  def change
    add_column :posts, :published, :boolean, default: false, null: false
    add_column :posts, :views, :integer, default: 0, null: false
  end
end

schema.rb — แปลนตู้ฉบับล่าสุด

ถ้าอยากรู้ว่า "ตอนนี้ตาราง posts มีช่องอะไรบ้าง" ต้องไล่อ่านใบสั่งทั้ง 5 ใบแล้วรวมในหัวเอง — เหนื่อย · Rails เลยเขียนไฟล์ db/schema.rb ให้ทุกครั้งที่ migrate เสร็จ: สรุปหน้าตาฐานข้อมูล ณ ตอนนี้ ไว้ในไฟล์เดียว

ไฟล์จริงของแอปเรา (ตัดคอมเมนต์ภาษาอังกฤษ 11 บรรทัดแรกออก — ใจความคือ "ไฟล์นี้สร้างอัตโนมัติ อย่าแก้เอง ให้ใช้ migration และควร commit เข้า git"):

# db/schema.rb (ตัดคอมเมนต์ด้านบนออก)
ActiveRecord::Schema[8.1].define(version: 2026_11_04_113607) do
  # These are extensions that must be enabled in order to support this database
  enable_extension "pg_catalog.plpgsql"

  create_table "comments", force: :cascade do |t|
    t.text "body"
    t.bigint "post_id", null: false
    t.bigint "user_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["post_id"], name: "index_comments_on_post_id"
    t.index ["user_id"], name: "index_comments_on_user_id"
  end

  create_table "posts", force: :cascade do |t|
    t.string "title"
    t.text "body"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.boolean "published", default: false, null: false
    t.integer "views", default: 0, null: false
    t.bigint "user_id", null: false
    t.index ["user_id"], name: "index_posts_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_foreign_key "comments", "posts"
  add_foreign_key "comments", "users"
  add_foreign_key "posts", "users"
end

อ่านทีละส่วน:

📒 เทียบให้เห็นภาพ — แฟ้มใบสั่งช่าง กับ แปลนตู้ฉบับล่าสุด

db/migrate/ = แฟ้มใบสั่งช่างทุกใบตั้งแต่วันเปิดร้าน เรียงตามวันที่ · ตอบคำถาม "ตู้นี้ถูกต่อเติมอะไรมาบ้าง ใครสั่ง เมื่อไหร่"

db/schema.rb = แปลนตู้ฉบับล่าสุด เหมือนถ่ายรูปตู้ตอนนี้แปะไว้ · ตอบคำถาม "ตอนนี้ตู้หน้าตาเป็นยังไง" · ช่างวาดแปลนใหม่ให้ทุกครั้งที่ทำงานเสร็จ (ทุก db:migrate) — เราไม่วาดเอง

สองอย่างนี้ไม่ซ้ำซ้อนกัน ตอบคนละคำถาม · และเปิดสาขาใหม่ก็ไม่ต้องไล่ทำใบสั่งย้อนไป 5 ปี แค่สร้างตู้ตามแปลนล่าสุดทีเดียว (หัวข้อ "เพื่อนใหม่เข้าทีม" ข้างล่าง)

ที่ไม่เหมือนกัน: รูปถ่ายตู้จริงถ่ายจากตู้ · แต่ schema.rb เขียนจากฐานข้อมูลในเครื่องคุณตอนที่ migrate เสร็จ · ถ้าฐานข้อมูลในเครื่องคุณมีของแปลก ๆ (เช่นตารางที่ลองสร้างเล่นแล้วลืมลบ) มันก็ไปโผล่ใน schema.rb ด้วย — ก่อน commit จึงต้องเปิดดูว่าที่เปลี่ยนมีแค่ที่ตั้งใจ

🗣 ศัพท์ที่เพิ่งเรียนไป
schema.rb · schema dump
ไฟล์สรุปหน้าตาฐานข้อมูลล่าสุด · Rails "dump" (เทออกมาเป็นไฟล์) ให้ทุกครั้งหลัง migrate · ห้ามแก้มือ แต่ต้อง commit · ถาม senior ได้ว่า “schema.rb ใน PR นี้มีบรรทัดที่ไม่เกี่ยวกับ migration ของผมโผล่มา ควรทำยังไง”
snapshot กับ history
snapshot = ภาพถ่าย ณ ตอนนี้ (schema.rb) · history หรือ event log = รายการทุกครั้งที่เปลี่ยน (db/migrate/) · พิมพ์บอก AI ได้ว่า “read db/schema.rb to see the current columns, don't guess from the migrations” — AI จะได้ไม่เดาคอลัมน์ผิด

schema_migrations — สมุดเซ็นรับงานช่าง

คำถามต่อมา: สั่ง db:migrate ซ้ำสิบรอบ ทำไม Rails ไม่เพิ่มคอลัมน์ published ซ้ำสิบรอบ?
เพราะในฐานข้อมูลมีตารางลับอีกตัวชื่อ schema_migrations เก็บเลขประจำตัวของใบที่ทำไปแล้ว ใบละหนึ่งแถว · ก่อนทำใบไหน Rails เปิดดูตารางนี้ก่อน ถ้ามีเลขนั้นแล้วก็ข้าม

ดูข้างในด้วย console (bin/rails console) · บรรทัดนี้ยาวหน่อย เพราะ schema_migrations ไม่มี model ให้เรียกแบบ Post.all เลยต้องส่งคำสั่ง SQL ไปตรง ๆ (ผลจริง):

wongnok-blog(dev):001> ActiveRecord::Base.connection.execute("SELECT * FROM schema_migrations ORDER BY version").to_a
   (60.2ms)  SELECT * FROM schema_migrations ORDER BY version /*application='WongnokBlog'*/
=>
[{"version" => "20261104113601"},
 {"version" => "20261104113603"},
 {"version" => "20261104113604"},
 {"version" => "20261104113606"},
 {"version" => "20261104113607"}]
✍️ เทียบให้เห็นภาพ — สมุดเซ็นรับงานช่างที่แขวนไว้ข้างตู้

แต่ละสาขามีสมุดแขวนข้างตู้ ช่างทำใบไหนเสร็จก็จดเลขใบนั้นลงสมุด · ช่างคนใหม่มาถึง เปิดแฟ้มใบสั่ง (db/migrate/) เทียบกับสมุด (schema_migrations) → ใบไหนยังไม่มีในสมุด ทำเฉพาะใบนั้น

ที่ไม่เหมือนกัน: ช่างจริงเห็นใบสั่งที่ถูกแก้ข้อความก็อาจสงสัย · แต่ Rails ดูแค่เลขหน้าใบ ไม่อ่านเนื้อหาซ้ำ — เลขอยู่ในสมุดแล้วคือ "ทำแล้ว" จบ ต่อให้เนื้อหาในใบเปลี่ยนไปหมดก็ตาม (กับดักท้ายบท)

เทียบกับ Go: นี่คือตาราง goose_db_version ของ goose ในบท 31 นั่นเอง · ของ goose มีหลายช่อง (version_id · is_applied · tstamp) ส่วนของ Rails มีช่องเดียวคือ version — มีแถว = ทำแล้ว · ไม่มีแถว = ยังไม่ทำ

🗣 ศัพท์ที่เพิ่งเรียนไป
schema_migrations · migration version
ตารางที่ Rails จดว่าทำ migration ใบไหนไปแล้ว · version = ตัวเลข 14 หลักหน้าชื่อไฟล์ · ถาม senior ได้ว่า “migration ตัวนี้ขึ้น up แล้วบน staging หรือยัง เช็คจาก schema_migrations ได้ไหม”
pending migration
ใบสั่งที่มีไฟล์แล้วแต่ยังไม่ได้ทำ (เลขยังไม่อยู่ในสมุด) · Rails จะไม่ยอมเปิดหน้าเว็บในเครื่องเราจนกว่าจะ migrate (เจอในภารกิจ) · พิมพ์บอก AI ได้ว่า “I got ActiveRecord::PendingMigrationError after pulling, what should I run?”

ลงมือ: เพิ่มคำโปรยใต้ชื่อบทความ

โจทย์: บทความแต่ละชิ้นอยากมี คำโปรยสั้น ๆ ใต้ชื่อเรื่อง เช่น "ต้มยำกุ้งน้ำข้น" — "สูตรเข้มข้นแบบร้านดัง" · ต้องเพิ่มคอลัมน์ subtitle (ข้อความสั้น) ในตาราง posts

ก่อนเริ่ม: commit ของที่ได้จากบท 107 ไว้ก่อน · ขั้น ③ จะใช้ git diff ดูว่า schema.rb เปลี่ยนตรงไหน ซึ่งเทียบกับ commit ล่าสุด — ถ้า schema.rb ยังไม่เคย commit มันจะไม่โชว์อะไรเลย (&& = ทำคำสั่งแรกสำเร็จแล้วค่อยทำคำสั่งถัดไป เหมือนบท 105):

$ git add . && git commit -m "schema และ seeds จากบท 107"
…(ผลของ commit ตัดออก)…
$ git status
On branch main
nothing to commit, working tree clean

nothing to commit, working tree clean = "ไม่มีอะไรค้าง ทุกอย่างบันทึกแล้ว" — ต่อจากนี้ทุกอย่างที่เปลี่ยนจะเห็นใน git diff · git status · (ถ้า git commit ตอบ nothing to commit ตั้งแต่แรก = คุณ commit ไว้แล้ว ไปต่อได้เลย · บรรทัด On branch main ของคุณอาจเป็น master ไม่ต่างกัน)

① ให้ Rails เขียนใบสั่งให้

$ bin/rails generate migration AddSubtitleToPosts subtitle:string
      invoke  active_record
      create    db/migrate/20261104114634_add_subtitle_to_posts.rb

เปิดไฟล์ที่ได้ — Rails เขียนเนื้อหาให้ครบแล้ว ไม่ต้องพิมพ์อะไรเพิ่ม:

# db/migrate/20261104114634_add_subtitle_to_posts.rb
class AddSubtitleToPosts < ActiveRecord::Migration[8.1]
  def change
    add_column :posts, :subtitle, :string
  end
end

ตอนนี้ มีแค่ใบสั่ง ยังไม่มีช่างลงมือ · ฐานข้อมูลยังไม่มีคอลัมน์ subtitle · และ schema.rb ยังไม่เปลี่ยนสักตัวอักษร

$ bin/rails db:migrate:status

db:migrate:status = "ขอดูว่าใบไหนทำแล้ว ใบไหนยัง" · หลัง generate แต่ยังไม่ migrate — ใบใหม่จะขึ้นว่าอะไร? และขึ้นไหม?

ผลจริง — ใบใหม่ขึ้นแล้ว สถานะ down:

database: w108

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20261104113601  Create posts
   up     20261104113603  Add published and views to posts
   up     20261104113604  Create users
   up     20261104113606  Add user to posts
   up     20261104113607  Create comments
  down    20261104114634  Add subtitle to posts
  • database: w108 — ชื่อฐานข้อมูลที่เครื่องผมใช้ตรวจ · ของคุณน่าจะเป็น wongnok_blog_development
  • up = ทำแล้ว (เลขอยู่ในสมุด schema_migrations) · down = มีไฟล์แต่ยังไม่ได้ทำ
  • Rails สร้างรายการนี้จากสองที่รวมกัน: ไฟล์ใน db/migrate/ กับ แถวใน schema_migrations

② สั่งช่างลงมือ: db:migrate

$ bin/rails db:migrate
== 20261104114634 AddSubtitleToPosts: migrating ===============================
-- add_column(:posts, :subtitle, :string)
   -> 0.0033s
== 20261104114634 AddSubtitleToPosts: migrated (0.0033s) ======================

③ แปลนตู้เปลี่ยนตรงไหน

ใช้ git diff ดูว่า schema.rb ต่างจากก่อน migrate ยังไง (ผลจริง ตัดบรรทัดที่ไม่เปลี่ยนรอบ ๆ ออกบางส่วน):

$ git diff db/schema.rb
-ActiveRecord::Schema[8.1].define(version: 2026_11_04_113607) do
+ActiveRecord::Schema[8.1].define(version: 2026_11_04_114634) do
…
     t.boolean "published", default: false, null: false
     t.integer "views", default: 0, null: false
     t.bigint "user_id", null: false
+    t.string "subtitle"
     t.index ["user_id"], name: "index_posts_on_user_id"
   end

เช็คสถานะอีกรอบ — ใบ subtitle กลายเป็น up (ผลจริง ตัดบรรทัดบนออก):

$ bin/rails db:migrate:status
…
   up     20261104113607  Create comments
   up     20261104114634  Add subtitle to posts

④ เรียกช่างมารื้อ: db:rollback

$ bin/rails db:rollback
== 20261104114634 AddSubtitleToPosts: reverting ===============================
-- remove_column(:posts, :subtitle, :string)
   -> 0.0040s
== 20261104114634 AddSubtitleToPosts: reverted (0.0046s) ======================

สถานะตอนนี้กลับเป็น down — ไฟล์ยังอยู่ แค่ยังไม่ได้ทำ (ผลจริง):

$ bin/rails db:migrate:status
…
   up     20261104113607  Create comments
  down    20261104114634  Add subtitle to posts

⚠️ rollback ลบข้อมูลในคอลัมน์นั้นทิ้งจริง · ผมลองใส่คำโปรย "อร่อยมาก" ให้ทั้ง 6 บทความ แล้ว rollback → migrate กลับมาใหม่ · คอลัมน์กลับมา แต่ค่าทุกแผ่นเป็น nil หมด · ในเครื่องตัวเองไม่เป็นไร แต่บน production = ข้อมูลลูกค้าหาย

🔨 เทียบให้เห็นภาพ — เรียกช่างมารื้องานใบล่าสุด

rollback = เรียกช่างกลับมารื้อสิ่งที่ใบล่าสุดต่อเติมไว้ แล้วขีดเลขใบนั้นออกจากสมุด · ใบสั่งยังอยู่ในแฟ้ม พร้อมทำใหม่เมื่อสั่ง

ที่ไม่เหมือนกัน: ช่างจริงรื้อช่องออกจากแฟ้มแล้ว ของที่เขียนในช่องนั้นอาจถ่ายเอกสารเก็บไว้ได้ · rollback ไม่เก็บอะไรไว้ให้เลย ข้อมูลในช่องหายไปพร้อมช่อง · และถ้าใบสั่งถูกแก้ไขไปแล้ว ช่างจะรื้อ "ตามใบที่แก้" ไม่ใช่ตามสิ่งที่ต่อเติมจริง (กับดักท้ายบท)

🗣 ศัพท์ที่เพิ่งเรียนไป
migration generator
คำสั่ง bin/rails generate migration ชื่อ คอลัมน์:ชนิด ที่เขียนไฟล์ให้ · ชื่อแบบ AddXToY / RemoveXFromY / CreateY ได้เนื้อหาให้เลย · พิมพ์บอก AI ได้ว่า “give me the rails generate migration command for adding a subtitle string to posts”
migrate · rollback · migration status
migrate = ทำใบที่ down · rollback = ย้อนใบล่าสุด · status = ดูว่าใบไหน up/down · ถาม senior ได้ว่า “ก่อนเปิด PR ผมควรลอง rollback แล้ว migrate ใหม่ให้ดูว่าย้อนได้จริงไหม” — ทีมส่วนใหญ่ชอบคำถามนี้

คลังคำที่เขียนใน change ได้

ไม่ต้องจำ — ให้รู้ว่ามีคำอะไรบ้าง แล้วกลับมาเปิดตารางนี้ (หรือ cheat sheet) ตอนใช้:

คำสั่งภาษาคน · ในร้านอาหารตัวอย่าง
create_tableต่อลิ้นชักใหม่ทั้งลิ้นชัก (ตารางใหม่)create_table :tags do |t| … end
drop_tableรื้อลิ้นชักทิ้งทั้งลิ้นชัก พร้อมแฟ้มข้างในdrop_table :tags
add_columnเพิ่มช่องบนแฟ้มadd_column :posts, :subtitle, :string
remove_columnลบช่องบนแฟ้ม (ของที่เขียนในช่องหายด้วย)remove_column :posts, :subtitle, :string
rename_columnเปลี่ยนชื่อช่อง ข้อมูลยังอยู่rename_column :posts, :body, :content
add_indexติดป้ายสารบัญให้หาตามช่องนี้เร็วขึ้น (บท 109)add_index :posts, :subtitle
add_referenceเพิ่มช่อง "เป็นของ … เลขที่" เช่น user_id (บท 115)add_reference :posts, :user, null: false, foreign_key: true

สไลด์หน้า 14 มีคำอื่นอีก เช่น rename_table (เปลี่ยนชื่อลิ้นชัก) · remove_index (แกะป้ายสารบัญออก) — ไม่ต้องจำตอนนี้ · คำที่ใช้ตั้งกฎให้ตัวตู้ (เช่น "ช่องนี้ห้ามว่าง") จะได้ใช้จริงในบท 111

และชื่อใบแบบไหน generator เดาเนื้อหาให้ได้ — ผมลองสั่งทีละแบบ (ผลจริง ตัดมาเฉพาะบรรทัดใน change):

สั่ง bin/rails generate migration …ได้ใน change
AddSubtitleToPosts subtitle:stringadd_column :posts, :subtitle, :string
RemoveSubtitleFromPosts subtitle:stringremove_column :posts, :subtitle, :string
CreateTags name:stringcreate_table :tags do |t| + t.string :name + t.timestamps
AddSlugToPosts slug:string:indexadd_column :posts, :slug, :string + add_index :posts, :slug
AddCategoryToPosts category:referencesadd_reference :posts, :category, null: false, foreign_key: true
AddReadingMinutesPosts reading_minutes:integer (ลืมคำว่า To)ว่างเปล่า — def change กับ end เฉย ๆ ไม่มี error

แถวสุดท้ายสำคัญ: ชื่อผิดแบบ generator ไม่ฟ้อง แค่ให้ใบเปล่า · ถ้าไม่เปิดดูแล้วสั่ง migrate ไปเลย ก็จะ "สำเร็จ" โดยไม่ได้เพิ่มอะไร · เปิดไฟล์ดูทุกครั้งหลัง generate · (ส่วน AddSubtitleToPost ที่ลืม s ไม่เป็นไร ผมลองแล้ว Rails ยังเติม :posts ให้ถูก)

คำสั่ง db: ที่ใช้บ่อย และอันไหนห้ามแตะบนร้านจริง

คำสั่งทำอะไร (ภาษาคน)บนเครื่องเราบน production
bin/rails db:migrateทำทุกใบที่ down ตามลำดับ แล้วเขียน schema.rb ใหม่ใช้บ่อยใช้ตอน deploy (ตามขั้นตอนของทีม)
bin/rails db:rollbackย้อนใบล่าสุด 1 ใบใช้ได้อันตราย — ข้อมูลในช่องที่ถูกรื้อหายจริง
bin/rails db:rollback STEP=3ย้อน 3 ใบล่าสุด (เปลี่ยนเลขได้)ใช้ได้อันตราย
bin/rails db:migrate:statusดูว่าใบไหน up / down · แค่ดู ไม่เปลี่ยนอะไรใช้บ่อยปลอดภัย
bin/rails db:prepareฐานข้อมูลยังไม่มี → สร้าง + โหลด schema.rb + ใส่ seeds · มีอยู่แล้ว → ทำใบที่ downใช้ตอนเพิ่ง clone โปรเจกต์บางทีมให้ระบบ deploy เรียกให้
bin/rails db:seedรัน db/seeds.rb ใส่ข้อมูลตัวอย่าง · สั่งซ้ำ = ข้อมูลเบิ้ล (ผมลองแล้ว ได้ 6 users จาก 3)ใช้ได้ระวังเบิ้ลแล้วแต่ทีม
bin/rails db:resetลบฐานข้อมูลทิ้งทั้งก้อน → สร้างใหม่จาก schema.rb → ใส่ seedsใช้ตอนอยากเริ่มใหม่สะอาด ๆห้ามเด็ดขาด — ลบข้อมูลลูกค้าทั้งร้าน
bin/rails db:migrate:resetตัวที่ใช้ในบท 107 · ลบฐานข้อมูลทิ้ง → สร้างใหม่ → ไล่ทำใบสั่งทุกใบตั้งแต่ใบแรก (จอขึ้น == … migrating ครบทุกใบ) · ไม่ใส่ seeds ให้ใช้ตอนอยากเช็คว่าใบสั่งทุกใบยังทำต่อกันได้ห้ามเด็ดขาด — เหมือน db:reset

วิธีจำง่าย ๆ: production คือร้านจริงที่ลูกค้ากำลังนั่งกินอยู่ · คำสั่งไหน "รื้อ" หรือ "ล้าง" ห้ามทำบนร้านจริง · บนร้านจริงถ้าใบสั่งผิด ให้เขียนใบใหม่มาแก้ ไม่ย้อนใบเก่า (กับดักท้ายบท)

เพื่อนใหม่เข้าทีม: ทำไม schema.rb ต้อง commit

เพื่อนใหม่ clone โปรเจกต์ไป เครื่องเขามีไฟล์ครบ แต่ฐานข้อมูลยังว่างเปล่า · สไลด์หน้า 12 บอกว่า bin/rails db:prepare จบ — จริง · ผมลองกับฐานข้อมูลใหม่เอี่ยม (ผลจริง):

$ bin/rails db:prepare
Created database 'w108_new'

ขึ้นบรรทัดเดียว — ไม่มีบรรทัด == … migrating สักใบ (w108_new คือชื่อฐานข้อมูลทดลองของผม) · แต่ตารางครบ status ขึ้น up ทั้ง 5 ใบ และข้อมูลตัวอย่างครบ (นับด้วย bin/rails runner แบบบท 107 ได้ users 3 · posts 6 · comments 7)

เพราะบนฐานข้อมูลใหม่ Rails ไม่ไล่ทำใบสั่งทีละใบ แต่สร้างตู้ตามแปลน schema.rb ทีเดียว · จดเลขทุกใบลงสมุดให้ แล้วค่อยรัน seeds

# ฐานข้อมูลใหม่อีกก้อน คราวนี้สั่งแยกทีละขั้น
$ bin/rails db:create
Created database 'w108_new'
$ bin/rails db:migrate

ฐานข้อมูลว่าง มีใบสั่ง 5 ใบ · สั่ง db:migrate แล้วจอจะขึ้น == … migrating 5 ก้อนเหมือนตอนเพิ่ม subtitle ไหม?

ผลจริง — ไม่ขึ้นอะไรเลยสักบรรทัด · แต่ตารางครบ status up ทั้ง 5 ใบ (และไม่มีข้อมูลตัวอย่าง เพราะ migrate ไม่รัน seeds)

ตั้งแต่ Rails 8.0 db:migrate บนฐานข้อมูลเปล่า (ยังไม่มีสมุด schema_migrations) ก็สร้างตู้ตามแปลน schema.rb ก่อนเหมือนกัน · ใบสั่งทุกใบเลยถือว่าทำแล้ว ไม่มีอะไรให้พิมพ์ (บันทึกการเปลี่ยนแปลงของ Rails ยืนยัน — ลิงก์ท้ายบท)

ผมลองย้าย schema.rb ออกไปชั่วคราว แล้วทำแบบเดิม — คราวนี้ขึ้น == … migrating ครบ 5 ใบ เพราะไม่มีแปลนให้ใช้ · ส่วน db:migrate:reset ที่ใช้ในบท 107 ตั้งใจไล่ทำทุกใบใหม่ จึงเห็นครบ 5 ใบเหมือนกัน (ผมลองแล้ว)

ข้อสรุป: เครื่องใหม่ทุกเครื่องสร้างฐานข้อมูลจาก schema.rb ไม่ใช่จากใบสั่ง · ถ้าลืม commit schema.rb หรือ commit ฉบับเก่า เพื่อนจะได้ตู้ไม่ตรงกับเรา

✅ ดังนั้นทุกครั้งที่สร้าง migration ใหม่ commit สองไฟล์คู่กันเสมอ: ไฟล์ใน db/migrate/ (ประวัติ) + db/schema.rb ที่ Rails เขียนให้หลัง migrate (แปลนล่าสุด)

กับดัก: แก้ไฟล์ migration ที่รันไปแล้ว

สมมติเราสั่ง db:migrate ให้ใบ subtitle กลับมาเป็น up แล้ว · จากนั้นนึกได้ว่า "อยากให้ค้นหาตาม subtitle ได้เร็วด้วย" มือใหม่ส่วนใหญ่จะเปิดไฟล์เดิมแล้วเติมบรรทัด add_index ลงไป:

# db/migrate/20261104114634_add_subtitle_to_posts.rb — แก้หลังจาก migrate ไปแล้ว
class AddSubtitleToPosts < ActiveRecord::Migration[8.1]
  def change
    add_column :posts, :subtitle, :string
    add_index :posts, :subtitle      # ← เติมบรรทัดนี้
  end
end
$ bin/rails db:migrate

สั่ง migrate หลังแก้ไฟล์ — จะเห็น -- add_index(:posts, :subtitle) ไหม?

ผลจริง — ไม่มีอะไรขึ้นเลย ไม่มี error · schema.rb ไม่เปลี่ยน · ไม่มี index

เพราะเลข 20261104114634 อยู่ในสมุด schema_migrations แล้ว Rails ถือว่า "ทำแล้ว" จบ ไม่เปิดอ่านเนื้อหาใหม่ (คู่มือของ Rails ก็เขียนไว้แบบนี้ — ลิงก์ท้ายบท)

แย่กว่านั้น: ถ้าคิดว่า "งั้น rollback แล้ว migrate ใหม่" ตอนนี้เลย — rollback พัง (ผลจริง ตัดบรรทัดที่ไม่จำเป็นเป็น …):

$ bin/rails db:rollback
== 20261104114634 AddSubtitleToPosts: reverting ===============================
-- remove_index(:posts, :subtitle)
bin/rails aborted!
StandardError: An error has occurred, this and all later migrations canceled: (StandardError)

No indexes found on posts with the options provided.
…
Tasks: TOP => db:rollback

⚠️ ถ้าเจอแบบนี้: แก้ไฟล์กลับให้เหมือนตอนที่รันไปก่อนเสมอ (ลบบรรทัด add_index ออก) แล้วค่อยเลือกวิธี ① หรือ ② ข้างล่าง · ถ้าปล่อยบรรทัดนั้นค้างไว้แล้วไปทำวิธีที่ ② เครื่องเพื่อนที่ยังไม่เคยรันทั้งสองใบจะสร้างสารบัญซ้ำสองรอบแล้วพัง (ผมลองแล้ว ได้ PG::DuplicateTable: ERROR: relation "index_posts_on_subtitle" already exists = "มีสารบัญชื่อนี้อยู่แล้ว")

วิธีที่ถูก มีสองทาง เลือกตามว่าใบนี้ไปถึงใครแล้ว (ทั้งสองทางเริ่มจากไฟล์ใบเดิมที่ยังไม่ได้แก้):

① ยังไม่ push ยังไม่มีใครเอาไปรัน — rollback ก่อน แล้วค่อยแก้

$ bin/rails db:rollback      # ไฟล์ยังเป็นแบบเดิม → ผ่าน
# ค่อยเติม add_index ในไฟล์
$ bin/rails db:migrate
== 20261104114634 AddSubtitleToPosts: migrating ===
-- add_column(:posts, :subtitle, :string)
   -> 0.0021s
-- add_index(:posts, :subtitle)
   -> 0.0025s
== 20261104114634 AddSubtitleToPosts: migrated (0.0047s) ===

ลำดับสำคัญ: ถอยก่อน แก้ทีหลัง · ใช้ได้แค่บนเครื่องตัวเอง

② push ไปแล้ว หรือขึ้นร้านจริงแล้ว — เขียนใบใหม่

$ bin/rails generate migration AddIndexToPostsSubtitle
      create    db/migrate/20261104115037_add_index_to_posts_subtitle.rb
# ได้ change เปล่า → พิมพ์ add_index :posts, :subtitle เอง
$ bin/rails db:migrate
== 20261104115037 AddIndexToPostsSubtitle: migrating ===
-- add_index(:posts, :subtitle)
   -> 0.0063s
== 20261104115037 AddIndexToPostsSubtitle: migrated (0.0063s) ===

ใบเก่าไม่แตะ · ทุกเครื่องได้ใบใหม่ไปทำต่อเอง · ปลอดภัยเสมอ

(ผลทั้งสองฝั่งรันจริง · ผมตัดเส้น ===== ท้ายบรรทัดให้สั้นลง และตัดบรรทัด invoke active_record ออก เพื่อให้พอดีกล่อง · ชื่อ AddIndexToPostsSubtitle ไม่ตรงแบบที่ generator เดาได้ จึงได้ใบเปล่า ต้องพิมพ์บรรทัดเอง)

📝 เทียบให้เห็นภาพ — แก้ใบสั่งที่ช่างเซ็นรับไปแล้ว

ใบสั่งที่จดในสมุดแล้ว = ช่างทำเสร็จแล้ว · ถ้าเราแอบแก้ข้อความในใบ ช่างไม่กลับมาทำเพิ่ม เพราะเลขใบอยู่ในสมุดแล้ว · ที่แย่คืออีก 4 สาขาที่ยังไม่ได้ทำใบนี้ จะทำตามใบที่แก้ — ตู้สาขาเราไม่มีสารบัญ แต่สาขาอื่นมี · ตู้ไม่เหมือนกันอีกแล้ว

วิธีที่ถูกคือออกใบสั่งใบใหม่ "ติดสารบัญเพิ่ม" · ทุกสาขาได้ใบเดียวกัน ทำตามลำดับเดียวกัน

ที่ไม่เหมือนกัน: ในชีวิตจริงใบสั่งที่ถูกแก้มักมีรอยลบให้เห็น · ไฟล์ migration ไม่มีรอยอะไรเลย ถ้าไม่มีใครเปิด git log ของไฟล์นั้นดู ก็ไม่มีใครรู้ว่าใบถูกแก้

🗣 ศัพท์ที่เพิ่งเรียนไป
edit a migration that already ran
กับดักคลาสสิก · ผล: ไม่มีอะไรเกิดในเครื่องเรา + เครื่องอื่นได้ผลต่างจากเรา + rollback อาจพัง · ประโยคที่ได้ยินใน code review บ่อย: “อย่าแก้ migration เดิม เขียนไฟล์ใหม่” — ถ้า AI เสนอให้แก้ไฟล์เก่า ให้พิมพ์ตอบว่า “that migration already ran, write a new one instead”
schema drift
อาการที่ฐานข้อมูลแต่ละเครื่อง/แต่ละ environment ค่อย ๆ ไม่ตรงกันโดยไม่มีใครรู้ตัว — ผลของการแก้ใบเก่าหรือแก้ฐานข้อมูลด้วยมือ (เคยเจอคำนี้ในบท 31) · ถาม senior ได้ว่า “สงสัยว่าเครื่องผมกับ staging drift กัน เช็คยังไงดี”

เก็บกวาดก่อนไปบทหน้า

บทหลังจากนี้ใช้ตาราง posts แบบเดิม (ไม่มี subtitle) · ดูก่อนว่าคุณหยุดอยู่ตรงไหน:

$ bin/rails db:rollback STEP=2
== 20261104115037 AddIndexToPostsSubtitle: reverting ==========================
-- remove_index(:posts, :subtitle)
   -> 0.0069s
== 20261104115037 AddIndexToPostsSubtitle: reverted (0.0083s) =================

== 20261104114634 AddSubtitleToPosts: reverting ===============================
-- remove_column(:posts, :subtitle, :string)
   -> 0.0027s
== 20261104114634 AddSubtitleToPosts: reverted (0.0028s) ======================

$ bin/rails destroy migration AddIndexToPostsSubtitle
      invoke  active_record
      remove    db/migrate/20261104115037_add_index_to_posts_subtitle.rb
$ bin/rails destroy migration AddSubtitleToPosts
      invoke  active_record
      remove    db/migrate/20261104114634_add_subtitle_to_posts.rb

🎯 ภารกิจ — เพิ่มช่อง "เวลาอ่าน (นาที)" แล้วถอยออก (~10 นาที)

ทำในแอป wongnok_blog ของคุณ (สถานะเริ่มต้น: db:migrate:status ขึ้น up 5 ใบ · git status ว่าง)

  1. สร้างใบสั่ง: bin/rails generate migration AddReadingMinutesToPosts reading_minutes:integer แล้วเปิดไฟล์ดูว่าใน change มีบรรทัดอะไร
  2. bin/rails db:migrate:status — ใบใหม่ขึ้นว่าอะไร
  3. bin/rails db:migrate แล้ว git diff db/schema.rb — บรรทัดที่ขึ้นต้น + มีอะไรบ้าง
  4. bin/rails runner 'p Post.column_names' — มี "reading_minutes" อยู่ท้ายสุดไหม (runner = รัน Ruby หนึ่งบรรทัดในแอป ไม่ต้องเปิด console)
  5. เก็บกวาด: bin/rails db:rollback → bin/rails destroy migration AddReadingMinutesToPosts → git status ต้องว่าง

ก่อนกดเฉลย: ข้อ 3 schema.rb เปลี่ยนกี่จุด? และถ้าข้อ 5 สลับเอา destroy ขึ้นก่อน rollback จะเกิดอะไร?

สิ่งที่ควรเห็น (ผลจริงจากเครื่องที่ใช้ตรวจ · เลขของคุณต่าง):

  • ข้อ 1 — ได้ add_column :posts, :reading_minutes, :integer บรรทัดเดียว
  • ข้อ 2 — บรรทัดสุดท้ายเป็น down … Add reading minutes to posts
  • ข้อ 3 — จอขึ้น -- add_column(:posts, :reading_minutes, :integer) · schema.rb เปลี่ยน 2 จุด: เลข version และบรรทัด + t.integer "reading_minutes"
  • ข้อ 4 — ["id", "title", "body", "created_at", "updated_at", "published", "views", "user_id", "reading_minutes"] · model Post ได้ช่องใหม่ทันทีโดยไม่ต้องแก้ไฟล์ model (Rails อ่านจากตาราง บท 106)
  • ข้อ 5 — rollback ขึ้น remove_column(:posts, :reading_minutes, :integer) · destroy ขึ้น remove db/migrate/… · git status ว่าง

ถ้าเจอ error นี้ แปลว่า…

  • Could not generate field 'reading_minutes' with unknown type 'integr'. — พิมพ์ชนิดข้อมูลผิด (ผมลองพิมพ์ผิดแบบนี้แล้ว ได้ข้อความนี้จริง) · ไม่มีไฟล์ถูกสร้าง สั่งใหม่ให้ถูกได้เลย
  • ไฟล์ที่ได้มี def change ว่างเปล่า — ชื่อใบผิดแบบ เช่นลืมคำว่า To · destroy ทิ้งแล้ว generate ใหม่ หรือพิมพ์ add_column เอง
  • เปิดหน้าเว็บแล้วเจอ ActiveRecord::PendingMigrationError · Migrations are pending. To resolve this issue, run: bin/rails db:migrate — มีใบที่ยัง down อยู่ (ข้าม ข้อ 3 ไป) · Rails ไม่ยอมเปิดหน้าเว็บในเครื่องเราจนกว่าจะ migrate · ทำตามที่มันบอก
  • db:migrate:status ขึ้น up … ********** NO FILE ********** และ rollback ขึ้น ActiveRecord::UnknownMigrationVersionError · No migration with version number … — คุณ destroy ก่อน rollback: สมุดบอกว่าทำแล้ว แต่ใบสั่งหายไป ช่างเลยไม่รู้จะรื้อยังไง (ผมลองแล้ว ได้สองข้อความนี้จริง) · แก้: สร้างไฟล์ใบเดิมกลับมาด้วยเลขเดิมเป๊ะ (db/migrate/<เลขที่ขึ้นใน status>_add_reading_minutes_to_posts.rb เนื้อหาตามข้อ 1) → rollback → destroy

ติดตรงไหน หรือผลของคุณไม่ตรงกับที่เขียนไว้ ถามกลับมาได้เลย พร้อมแปะสิ่งที่จอขึ้นมาทั้งก้อน

อยากรู้ว่า "ตอนนี้ตาราง posts มีคอลัมน์อะไรบ้าง" ให้เร็วที่สุด ควรเปิดอะไร?

ตัวเลือกที่ 2 ถูก — schema.rb คือแปลนตู้ฉบับล่าสุด รวมผลของทุกใบสั่งไว้ที่เดียว

ตัวเลือกที่ 1 ผิด เพราะไฟล์ model ไม่ได้เขียนรายชื่อคอลัมน์ไว้ (Rails อ่านจากตารางตอนรัน) · ตัวเลือกที่ 3 ผิด เพราะใบล่าสุดบอกแค่สิ่งที่เปลี่ยนครั้งล่าสุด ไม่ใช่ทั้งตาราง · ตัวเลือกที่ 4 ผิด เพราะ schema_migrations เก็บแค่เลขใบที่ทำแล้ว ไม่มีชื่อคอลัมน์

จุดที่ต้องจำ: ตอนนี้หน้าตาเป็นยังไง → schema.rb · เปลี่ยนมายังไง → db/migrate/ · ทำใบไหนแล้ว → schema_migrations

สั่ง bin/rails db:migrate ซ้ำหลายรอบ ทำไมคอลัมน์ไม่ถูกเพิ่มซ้ำ?

ตัวเลือกที่ 3 ถูก — schema_migrations คือสมุดเซ็นรับงาน เลขใบอยู่ในสมุด = ทำแล้ว ข้าม

ตัวเลือกที่ 1 ผิด เพราะบนฐานข้อมูลที่มีสมุดแล้ว Rails ไม่ได้เอา schema.rb มาเทียบทีละบรรทัด (มันแค่เขียน schema.rb ใหม่หลังทำเสร็จ · schema.rb ถูกใช้ตอนสร้างฐานข้อมูลใหม่เท่านั้น) · ตัวเลือกที่ 2 ผิด เพราะ Rails ไม่สนวันที่แก้ไฟล์ — นี่แหละเหตุผลที่แก้ไฟล์เก่าแล้วไม่มีผล · ตัวเลือกที่ 4 ผิด เพราะ Rails ไม่ได้ส่งคำสั่งไปเลยตั้งแต่แรก (จอว่างเปล่า ไม่ใช่ error ที่ถูกซ่อน)

จุดที่ต้องจำ: Rails ดูแค่ "เลขใบอยู่ในสมุดไหม" ไม่อ่านเนื้อหาใบซ้ำ

migration เพิ่มคอลัมน์ของคุณ push ไปแล้ว และขึ้นเว็บจริงไปแล้ว · เพิ่งเห็นว่าลืม add_index ควรทำอย่างไร?

ตัวเลือกที่ 1 ถูก — ใบใหม่ไม่แตะของเดิม ทุกเครื่องทุก environment ได้ไปทำต่อตามลำดับเดียวกัน

ตัวเลือกที่ 2 ผิด เพราะใบนี้ up แล้ว migrate ซ้ำไม่เกิดอะไร (เห็นแล้วในกับดัก) และเครื่องอื่นจะได้ผลต่างจากเรา · ตัวเลือกที่ 3 ผิด เพราะ rollback บนร้านจริงรื้อคอลัมน์ทิ้งพร้อมข้อมูลลูกค้า · ตัวเลือกที่ 4 ผิด เพราะ schema.rb เป็นไฟล์ที่ Rails เขียน ไม่ได้สร้าง index จริงในฐานข้อมูล และจะถูกเขียนทับรอบหน้า

จุดที่ต้องจำ: ใบที่ออกจากเครื่องเราไปแล้ว = แก้ด้วยใบใหม่เสมอ

คำสั่งไหนห้ามรันบน production (ร้านจริงที่ลูกค้ากำลังใช้) มากที่สุด?

ตัวเลือกที่ 4 ถูก — db:reset ลบฐานข้อมูลทั้งก้อนแล้วสร้างใหม่จาก schema.rb + seeds = ข้อมูลลูกค้าหายหมดทั้งร้าน

ตัวเลือกที่ 1 ผิด เพราะ status แค่ดู ไม่เปลี่ยนอะไร · ตัวเลือกที่ 2 ผิด เพราะ generate แค่สร้างไฟล์ในเครื่อง ไม่แตะฐานข้อมูล (และปกติไม่มีใครสั่งบนเครื่อง production) · ตัวเลือกที่ 3 ผิด เพราะ migrate คือขั้นตอนปกติตอน deploy · แค่ต้องระวังว่าใบนั้นทำร้านค้างหรือเปล่า (เรื่องของบท 109)

จุดที่ต้องจำ: คำสั่งที่ "รื้อ" หรือ "ล้าง" (reset · rollback) ใช้ได้แค่ในครัวทดลอง

ถ้าเปิดสไลด์ดูประกอบ

หน้าสไลด์เขียนของจริงต้องสนใจไหม
15db:prepare = "สร้าง DB + รัน migration ที่ขาด"บนฐานข้อมูลใหม่: สร้าง + โหลด schema.rb + ใส่ seeds ไม่ไล่ทำใบทีละใบ · ฐานข้อมูลที่มีตารางแล้ว: ถึงจะทำใบที่ขาดสนใจ — เหตุผลที่ schema.rb ต้อง commit
14แก้ไฟล์ที่รันแล้ว → "ต้อง rollback ก่อน หรือเขียนไฟล์ใหม่ทับ"ต้อง rollback ก่อนแก้ · แก้ก่อนแล้วค่อย rollback = พัง (No indexes found…) · "ไฟล์ใหม่" คือเพิ่มใบใหม่ ไม่ใช่เขียนทับใบเดิมสนใจมาก
14SELECT * FROM schema_migrations; พร้อมผลเป็นตารางผลหน้าตาแบบนั้นได้จากโปรแกรม psql (เปิดด้วย bin/rails dbconsole ถ้ามี psql ในเครื่อง) · บทนี้ใช้ console ส่ง SQL แทน ข้อมูลเหมือนกันไม่ต้อง
13ตัวอย่าง schema มี published_at · ใบ RemoveSubtitleเป็นตัวอย่างของสไลด์ · แอปเราใช้ published (จริง/เท็จ) และไม่มีใบ RemoveSubtitleไม่ต้อง
12เครื่องใหม่เข้าทีม: bin/rails db:prepare จบจริง · แต่ต้องเปิด PostgreSQL ไว้ก่อน และ schema.rb ใน git ต้องเป็นฉบับล่าสุดสนใจนิดหน่อย
15ไม่ได้บอกว่า db:seed สั่งซ้ำแล้วเป็นยังไงสั่งซ้ำ = ข้อมูลเบิ้ล (seeds ของเราไม่ได้เช็คว่ามีอยู่แล้ว)สนใจ — ใช้ db:reset ในเครื่องตัวเองแทนถ้าอยากเริ่มใหม่

อ้างอิง