ในโพสต์วันนี้จะ deep dive เข้า Rails migrations จะ break down migration เป็นชิ้นๆ และเรียนรู้เขียน effective migration จะเรียนรู้เขียน migrations สำหรับ multiple databases และจัดการ failed migrations พร้อม techniques การ rollback
เข้าใจทั้งโพสต์ต้องมี basic understanding ของ databases และ Rails
Migrations 101
Migrations ใน Rails ให้ evolve database ตลอด lifetime ของแอปพลิเคชัน Migrations ให้เขียน plain Ruby code เปลี่ยน state ของ database โดยให้ elegant DSL ไม่ต้องเขียน database-specific SQL เพราะ migrations ให้ abstractions manipulate database และจัดการ nitty-gritty details การ convert DSL เป็น database-specific SQL queries ข้างหลัง Migrations ยังให้ทาง execute raw SQL บน database ถ้าจำเป็น
Twenty Thousand Leagues Into a Rails Database Migration
สร้างตาราง เพิ่ม/ลบ columns และเพิ่ม indexes บน columns ด้วย migrations
ทุก Rails app มี special directory —
db/migrate— ที่เก็บ migrations ทั้งหมด
เริ่ม migration สร้างตาราง events เข้า database:
$ rails g migration CreateEvents category:string
Command สร้าง timestamped file 20200405103635_create_events.rb ใน db/migrate เนื้อหา:
class CreateEvents < ActiveRecord::Migration[6.0]
def change
create_table :events do |t|
t.string :category
t.timestamps
end
end
end
Break down migration file นี้:
- ทุก migration file ที่ Rails generate มี timestamp ใน filename ใช้โดย Rails confirm ว่า migration รันหรือยัง
- Migration มี class inherit จาก
ActiveRecord::Migration[6.0]ใช้ Rails 6 superclass มี[6.0]ถ้า Rails 5.2 superclass คือActiveRecord::Migration[5.2]จะอภิปรายทีหลังว่าทำไม Rails version เป็นส่วนของ superclass name - Migration มี method
changeมี DSL code manipulate database กรณีนี้changeสร้างตารางeventsด้วยคอลัมน์categorytypestring - Migration ใช้
t.timestampsเพิ่ม timestampscreated_atและupdated_atให้ตารางevents
รัน migration ด้วย rails db:migrate จะสร้างตาราง events ด้วยคอลัมน์ category type string และ timestamp columns
Actual database column type จะเป็น varchar หรือ text ขึ้นกับ database
ความสำคัญของ Migration Timestamps และ schema_migrations Table
ทุกครั้ง generate migration ด้วย rails g migration Rails สร้าง migration file ด้วย unique timestamp format YYYYMMDDHHMMSS ทุกครั้งรัน migration Rails insert timestamp เข้า internal table schema_migrations Table นี้สร้างโดย Rails ตอนรัน migration แรก มีแค่คอลัมน์ version ซึ่งเป็น primary key โครงสร้าง schema_migrations table:
CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY);
ตอนรัน migration สร้าง events table แล้ว เช็คว่า Rails เก็บ timestamp ใน schema_migrations:
sqlite> select * from schema_migrations; 20200405103635
รัน migrations อีกครั้ง Rails เช็ค entry ใน schema_migrations ด้วย timestamp ของ migration file execute เฉพาะถ้าไม่มี entry นี้ ensure เพิ่ม changes เข้า database incrementally migration รันครั้งเดียวต่อ database
Database Schema
รัน migrations มากขึ้น database schema evolve Rails เก็บ recent database schema ใน file db/schema.rb File นี้คือ Ruby representation ของ migrations ทั้งหมดที่รันบน database ตลอดชีวิตแอป เพราะไฟล์นี้ ไม่ต้องเก็บ migration files เก่าใน codebase Rails มี tasks dump latest schema จาก database เข้า schema.rb และ load schema เข้า database จาก schema.rb ดังนั้น migrations เก่าลบจาก codebase ได้ โหลด schema เข้า database เร็วกว่ารันทุก migration ทุกครั้ง setup app
Rails ยังให้ทางเก็บ database schema ใน SQL format มี article เปรียบเทียบสอง formats อ่านเพิ่มได้ที่นี่
Rails Version ใน Migration
ทุก migration ที่ generate มี Rails version เป็นส่วนของ superclass Migration generated by Rails 6 app มี superclass ActiveRecord::Migration[6.0] ในขณะที่ Rails 5.2 app มี ActiveRecord::Migration[5.2] ถ้า app เก่า Rails 4.2 หรือต่ำกว่า จะสังเกตไม่มี version ใน superclass เพียง ActiveRecord::Migration
Rails version เพิ่มใน migration superclass ใน Rails 5 basic ensure migration API evolve ตามเวลาโดยไม่ breaking migrations generated by older Rails versions
ลองดูลึกด้วย migration เดียวกันสร้างตาราง events ใน Rails 4.2 app:
class CreateEvents < ActiveRecord::Migration
def change
create_table :events do |t|
t.string :category
t.timestamps null: false
end
end
end
ดู schema ของ events table generated by Rails 6 migration เห็น NOT NULL constraint สำหรับ timestamps columns มีอยู่:
sqlite> .schema events
CREATE TABLE IF NOT EXISTS "events" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "category" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
เพราะตั้งแต่ Rails 5 ขึ้นไป migration API auto adds NOT NULL constraint ให้ timestamp columns โดยไม่ต้อง add explicitly ใน migration file Rails version ใน superclass name ensure migration ใช้ migration API ของ Rails version ที่ migration generated นี่ allow Rails maintain backward compatibility กับ older migrations พร้อม evolve migrations API
เปลี่ยน Database Schema
Method change เป็น primary method ใน migration ตอน migration รัน จะ call change และ execute code ภายใน
นอกจาก create_table Rails มี powerful method อีก — change_table ใช้ alter schema ของ existing table
def change
change_table :events do |t|
t.remove :category
t.string :event_type
t.boolean :active, default: false
end
end
Migration นี้ remove category column จาก events table เพิ่ม string column events_type ใหม่ และ boolean column active ใหม่ default false
Rails มี helper methods หลายอันใช้ใน migration ได้ เช่น:
change_columnadd_indexremove_indexrename_table
และอีกมาก หา methods ทั้งหมดที่ใช้กับ change ได้ที่นี่
Timestamps
เห็น t.timestamps เพิ่มโดย Rails เพิ่มคอลัมน์ created_at และ updated_at ให้ events table คอลัมน์พิเศษเหล่านี้ Rails ใช้ track record สร้างและอัปเดตเมื่อไหร่ Rails add values ตอนสร้าง record และ update ตอน record updated คอลัมน์เหล่านี้ช่วย track lifetime ของ database record
updated_atcolumn ไม่ update ตอน executeupdated_allmethod จาก Rails
จัดการ Failures
Migrations ไม่ bulletproof อาจ fail เหตุผลอาจ wrong syntax หรือ invalid database query ไม่ว่าสาเหตุ ต้อง handle failure และ recover ไม่ให้ database ไป inconsistent state Rails แก้ปัญหานี้โดยรันแต่ละ migration ภายใน transaction ถ้า migration fail transaction rollback ensure database ไม่ไป inconsistent state
ทำได้เฉพาะ databases ที่ support transactions สำหรับ update database schema เรียกว่า Data Definition Language (DDL) transactions MySQL และ PostgreSQL รองรับ DDL transactions ทั้งคู่
บางครั้งไม่อยาก execute migrations ภายใน transaction ตัวอย่าง simple คือเพิ่ม concurrent index ใน PostgreSQL migrations แบบนี้ execute ภายใน DDL transaction ไม่ได้ เพราะ PostgreSQL พยายามเพิ่ม index โดยไม่ acquire locks บน table เพื่อเพิ่ม index บน production database live โดยไม่ down database Rails ให้ opt-out ของ transactions ใน migration ด้วย disable_ddl_transactions!
def change
disable_ddl_transactions!
add_index :events, :user_id, algorithm: :concurrently
end
นี่ไม่รัน migration ใน transaction ถ้า migration แบบนี้ fail ต้อง recover เอง กรณีนี้ REINDEX หรือ remove index และลองเพิ่มใหม่
Reversible Migrations
Rails ให้ rollback changes database ด้วย command:
rails db:rollback
Command นี้ revert migration ล่าสุดที่รันบน database ถ้า migration เพิ่มคอลัมน์ event_type rollback จะ remove คอลัมน์นั้น ถ้า migration เพิ่ม index rollback จะ remove index
มี command rollback previous migration และรันมัน คือ
rails db:redo
Rails smart พอ know how to reverse migrations ส่วนใหญ่ แต่ให้ hints ให้ Rails ว่า revert migration ยังไง โดย provide up และ down methods แทน change method up ใช้ตอน migration รัน down ใช้ตอน rollback
def up
change_table :events do |t|
t.change :price, :string
end
end
def down
change_table :events do |t|
t.change :price, :integer
end
end
ตัวอย่างนี้เปลี่ยนคอลัมน์ price ของ events จาก integer เป็น string ระบุ rollback ยังไงใน down method
Migration เดียวกันเขียนด้วย change method ได้:
def change
reversible do |direction|
change_table :events do |t|
direction.up { t.change :price, :string }
direction.down { t.change :price, :integer }
end
end
end
Rails ยังให้ revert previous migration completely ด้วย revert method:
def change
revert CreateEvents
create_table :events do
...
end
end
revert method ยังรับ block revert migration partially:
def change
revert do
reversible do |direction|
change_table :events do |t|
direction.up { t.remove :event_type }
direction.down { t.string :event_type }
end
end
end
end
Executing It Raw (รัน Raw)
บางครั้งอยาก execute complex SQL ใน migration กรณีนี้ลืม migration DSL ทั่วไป execute raw SQL แทน:
def change
execute <<-SQL
...
SQL
end
Multiple Databases และ Migrations
Rails 6 เพิ่ม support multiple databases ภายใน single Rails application ถ้าอยากใช้ multiple databases config ใน database.yml:
development:
primary: <<: *default
database: db/development.sqlite3
analytics:
adapter: sqlite3
database: db/analytics_dev.sqlite3
Config บอก Rails ใช้สอง databases — primary และ analytics ตอนแรกเห็น migrations stored in db/migrate by default แตในกรณีนี้ไมเพิ่ม migrations ของทั้งสอง databases ใน single directory ไมอยากรัน migrations ของ analytics database บน primary database และ vice versa ถ้าใช้ multiple databases ต้อง provide path เก็บ migrations สำหรับ database ที่สอง ด้วย migrations_paths ใน database.yml:
development:
primary: <<: *default
database: db/development.sqlite3
analytics:
adapter: sqlite3
database: db/analytics_dev.sqlite3
migrations_paths: db/analytics_migrate
สร้าง migrations สำหรับ analytics database:
rails generate migration AddExperiments rule:string active:boolean --db=analytics
สร้าง migration ภายใน db/analytics_migrate รันได้:
rails db:migrate --db=analytics
รันแค่ rails db:migrate จะ execute migrations สำหรับทุก databases
Database
analyticsจะมีschema_migrationstable ของตัวเอง track migrations ที่รันและยังไม่รัน
รัน Migrations ระหว่าง Deployment
เนื่องจาก migrations เปลี่ยน state ของ database และ code อาจ depend on changes เหล่านั้น สำคัญมากที่ migrations รันก่อน new code applied
Heroku-based deployments migrations รันใน release phase ของ Procfile:
# Profile
web: bin/puma -C config/puma.rb
release: bundle exec rake db:migrate
Ensure migrations รันก่อน app dynos restart
Capistrano-based deployments migrations ควร run ก่อน server restart
Docker-based deployments รัน sidecar container รัน migrations ก่อน app restart สำคัญมาก มิฉะนั้น new containers อาจไป inconsistent state ถ้าเริ่มใช้ new code ก่อน apply database changes สำหรับ new code นั้น
สรุป
ในโพสต์นี้เห็น various aspects ของการเขียน database migration ใน Rails เห็นอะไร constitute a migration และจัดการ failures และ roll back migrations ถ้าจำเป็น Rails 6 ให้ใช้ multiple databases และ migrations สำหรับแต่ละ database ต้องเพิงแยกกัน สุดท้ายสั้นๆ ดูวิธีรัน migrations ระหว่ deployment ให้ database changes applied properly ก่อน new code เริ่มใช้พวกมัน
P.S. หากอยากอ่าน Ruby Magic posts ทันทีที่เผยแพร่ สมัครสมาชิก Ruby Magic newsletter และไม่พลาดโพสต์เด็ดๆ!