Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

Master Rails Migrations: Writing, Managing, and Rolling Back Effectively

ในโพสต์วันนี้จะ 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 ด้วยคอลัมน์ category type string
  • Migration ใช้ t.timestamps เพิ่ม timestamps created_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_column
  • add_index
  • remove_index
  • rename_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_at column ไม่ update ตอน execute updated_all method จาก 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_migrations table ของตัวเอง 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 และไม่พลาดโพสต์เด็ดๆ!