ในโพสต์ของวันนี้ เราจะเจาะลึกการโยกย้าย Rails เราจะแบ่งการโยกย้ายออกเป็นส่วนๆ และในกระบวนการนี้ คุณจะได้เรียนรู้วิธีการเขียนการย้ายถิ่นอย่างมีประสิทธิภาพ เราจะเรียนรู้วิธีเขียนการย้ายข้อมูลสำหรับหลายฐานข้อมูล ตลอดจนวิธีจัดการกับการย้ายที่ล้มเหลวและเทคนิคการย้อนกลับ
เพื่อให้เข้าใจโพสต์ทั้งหมด คุณจะต้องมีความเข้าใจพื้นฐานเกี่ยวกับฐานข้อมูลและ Rails
การย้ายถิ่น 101
การย้ายข้อมูลใน Rails ช่วยให้เราสามารถพัฒนาฐานข้อมูลตลอดอายุของแอปพลิเคชัน การย้ายข้อมูลช่วยให้เราสามารถเขียนโค้ด Ruby ธรรมดาเพื่อเปลี่ยนสถานะของฐานข้อมูลโดยจัดเตรียม DSL ที่สง่างาม เราไม่จำเป็นต้องเขียน SQL เฉพาะฐานข้อมูล เนื่องจากการย้ายข้อมูลให้ abstractions เพื่อจัดการฐานข้อมูลและดูแลรายละเอียดที่สำคัญของการแปลง DSL เป็นการสืบค้น SQL เฉพาะฐานข้อมูลเบื้องหลัง การย้ายข้อมูลยังหลีกทางให้กับเราและให้วิธีการดำเนินการ SQL แบบดิบบนฐานข้อมูล หากมีความจำเป็นดังกล่าวเกิดขึ้น
สองหมื่นลีกสู่การย้ายฐานข้อมูล Rails
เราสามารถสร้างตาราง เพิ่มหรือลบคอลัมน์ และเพิ่มดัชนีในคอลัมน์โดยใช้การย้ายข้อมูล
ทุกแอป Rails มีไดเร็กทอรีพิเศษ—
db/migrate
—ที่จัดเก็บการโยกย้ายทั้งหมด
เริ่มจากการย้ายข้อมูลที่สร้างตาราง events
ลงในฐานข้อมูลของเรา
$ rails g migration CreateEvents category:string
คำสั่งนี้สร้างไฟล์ประทับเวลา 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
มาแยกย่อยไฟล์การโยกย้ายนี้กันเถอะ
- ทุกไฟล์การโยกย้ายที่ Rails สร้างมีการประทับเวลาที่มีอยู่ในชื่อไฟล์ การประทับเวลานี้มีความสำคัญและ Rails ใช้เพื่อยืนยันว่ามีการย้ายข้อมูลหรือไม่ ดังที่เราเห็นในภายหลัง
- การย้ายข้อมูลมีคลาสที่สืบทอดมาจาก
ActiveRecord::Migration[6.0]
. ขณะที่ฉันใช้ Rails 6 ซูเปอร์คลาสการโยกย้ายมี[6.0]
. ถ้าฉันใช้ Rails 5.2 อยู่ ซูเปอร์คลาสจะเป็นActiveRecord::Migration[5.2]
. ต่อมา เราจะพูดถึงสาเหตุที่เวอร์ชัน Rails เป็นส่วนหนึ่งของชื่อซูเปอร์คลาส - การโยกย้ายมีวิธีการ
change
ซึ่งมีรหัส DSL ที่จัดการฐานข้อมูล ในกรณีนี้change
เมธอดกำลังสร้างevents
ตารางที่มีคอลัมน์category
ประเภทstring
. - การย้ายข้อมูลใช้รหัส
t.timestamps
เพื่อเพิ่มการประทับเวลาcreated_at
และupdated_at
ไปที่events
ตาราง.
เมื่อย้ายข้อมูลนี้โดยใช้ rails db:migrate
คำสั่งจะสร้าง events
ตารางที่มี category
คอลัมน์ประเภท string
และคอลัมน์ประทับเวลา created_at
และ updated_at
.
ประเภทคอลัมน์ฐานข้อมูลจริงจะเป็น varchar หรือข้อความ ขึ้นอยู่กับฐานข้อมูล
ความสำคัญของการประทับเวลาการย้ายข้อมูลและตาราง schema_migration
ทุกครั้งที่มีการสร้างการย้ายข้อมูลโดยใช้ rails g migration
คำสั่ง Rails สร้างไฟล์การโยกย้ายด้วยการประทับเวลาที่ไม่ซ้ำกัน การประทับเวลาอยู่ในรูปแบบ YYYYMMDDHHMMSS
.เมื่อใดก็ตามที่มีการเรียกใช้การย้ายข้อมูล Rails จะแทรกการประทับเวลาการย้ายข้อมูลลงในตารางภายใน schema_migrations
. Rails สร้างตารางนี้เมื่อเราเรียกใช้การย้ายข้อมูลครั้งแรก ตารางมีเฉพาะคอลัมน์ version
ซึ่งเป็นคีย์หลักด้วย นี่คือโครงสร้างของ schema_migrations
ตาราง
CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY);
ตอนนี้เราได้เรียกใช้การย้ายข้อมูลเพื่อสร้าง events
มาดูกันว่า Rails ได้เก็บบันทึกเวลาของการย้ายข้อมูลนี้ไว้ใน schema_migrations
หรือไม่ ตาราง
sqlite> select * from schema_migrations;
20200405103635
ถ้าเราเรียกใช้การโยกย้ายอีกครั้ง Rails จะตรวจสอบก่อนว่ามีรายการอยู่ใน schema_migrations
ตารางที่มีการประทับเวลาของไฟล์การโยกย้าย และดำเนินการก็ต่อเมื่อไม่มีรายการดังกล่าว เพื่อให้แน่ใจว่าเราสามารถเพิ่มการเปลี่ยนแปลงในฐานข้อมูลได้เรื่อยๆ และการย้ายจะทำงานเพียงครั้งเดียวในฐานข้อมูล
สคีมาฐานข้อมูล
เมื่อเราเรียกใช้การย้ายข้อมูลมากขึ้นเรื่อยๆ สคีมาฐานข้อมูลก็พัฒนาขึ้นเรื่อยๆ Rails เก็บสคีมาฐานข้อมูลล่าสุดในไฟล์ db/schema.rb
. ไฟล์นี้เป็นตัวแทน Ruby ของการย้ายข้อมูลทั้งหมดบนฐานข้อมูลของคุณตลอดอายุของแอปพลิเคชัน เนื่องจากไฟล์นี้ เราจึงไม่จำเป็นต้องเก็บไฟล์การโยกย้ายแบบเก่าไว้ใน codebase Rails จัดเตรียมงานเพื่อ dump
สคีมาล่าสุดจากฐานข้อมูลไปยัง schema.rb
และ load
สคีมาลงในฐานข้อมูลจาก schema.rb
. ดังนั้นการโยกย้ายที่เก่ากว่าจึงสามารถลบออกจากฐานรหัสได้อย่างปลอดภัย การโหลดสคีมาลงในฐานข้อมูลนั้นเร็วกว่าเมื่อเทียบกับการเรียกใช้การย้ายแต่ละครั้งทุกครั้งที่เราตั้งค่าแอปพลิเคชัน
Rails ยังจัดเตรียมวิธีการเก็บสคีมาฐานข้อมูลในรูปแบบ SQL เรามีบทความเปรียบเทียบทั้งสองรูปแบบอยู่แล้ว คุณสามารถอ่านเพิ่มเติมได้ที่นี่
เวอร์ชัน Rails ในการย้ายข้อมูล
การย้ายข้อมูลทั้งหมดที่เราสร้างมีเวอร์ชัน Rails เป็นส่วนหนึ่งของซูเปอร์คลาส ดังนั้นการย้ายข้อมูลที่สร้างโดยแอป Rails 6 จึงมี ActiveRecord::Migration[6.0]
ระดับซูเปอร์คลาส ในขณะที่การโยกย้ายที่สร้างโดยแอป Rails 5.2 มีซูเปอร์คลาส ActiveRecord::Migration[5.2]
. หากคุณมีแอปเก่าที่มี Rails 4.2 หรือต่ำกว่า คุณจะสังเกตเห็นว่าไม่มีเวอร์ชันในซูเปอร์คลาส ซูเปอร์คลาสเป็นเพียง ActiveRecord::Migration
.
เพิ่มเวอร์ชัน Rails ลงในซูเปอร์คลาสการโยกย้ายใน Rails 5 โดยพื้นฐานแล้วทำให้มั่นใจได้ว่า migrationAPI สามารถพัฒนาได้ตลอดเวลาโดยไม่ทำลายการโยกย้ายที่สร้างโดย Rails เวอร์ชันเก่า
มาเจาะลึกเรื่องนี้กันโดยดูที่การย้ายข้อมูลเดียวกันสำหรับการสร้าง events
ตารางในแอป Rails 4.2
class CreateEvents < ActiveRecord::Migration
def change
create_table :events do |t|
t.string :category
t.timestamps null: false
end
end
end
ถ้าเราดูที่สคีมาของ events
ตารางที่สร้างโดยการย้ายข้อมูล Rails 6 เราจะเห็นว่า NOT NULL
มีข้อจำกัดสำหรับคอลัมน์ประทับเวลา
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 เป็นต้นไป API การโยกย้ายจะเพิ่ม NOT NULL
โดยอัตโนมัติ ข้อจำกัดของคอลัมน์ประทับเวลาโดยไม่จำเป็นต้องเพิ่มอย่างชัดเจนในไฟล์การโยกย้าย เวอร์ชัน Rails ในชื่อซูเปอร์คลาสช่วยให้มั่นใจว่าการย้ายข้อมูลจะใช้ API การย้ายข้อมูลของเวอร์ชัน Rails ที่สร้างการย้ายข้อมูล วิธีนี้ช่วยให้ Rails รักษาความเข้ากันได้แบบย้อนหลังกับการย้ายข้อมูลแบบเก่า ในขณะเดียวกันก็พัฒนา API การย้ายข้อมูล
การเปลี่ยนรูปแบบฐานข้อมูล
change
วิธีเป็นวิธีหลักในการโยกย้าย เมื่อทำการโยกย้าย จะเรียก change
เมธอดและรันโค้ดที่อยู่ภายใน
พร้อมกับ create_table
, Rails ยังมีวิธีที่มีประสิทธิภาพอีกวิธีหนึ่ง—change_table
.ตามชื่อของมัน มันถูกใช้เพื่อเปลี่ยนสคีมาของตารางที่มีอยู่
def change
change_table :events do |t|
t.remove :category
t.string :event_type
t.boolean :active, default: false
end
end
การย้ายข้อมูลนี้จะลบ category
คอลัมน์จาก events
ตาราง เพิ่มคอลัมน์สตริงใหม่ events_type
และคอลัมน์บูลีนใหม่ active
ด้วยค่าเริ่มต้น false
.
Rails ยังมีวิธีการช่วยเหลืออื่นๆ อีกมากที่สามารถนำมาใช้ในการย้ายข้อมูลได้ เช่น:
change_column
add_index
remove_index
rename_table
และอื่น ๆ อีกมากมาย. ดูวิธีการทั้งหมดที่สามารถใช้กับการเปลี่ยนแปลงได้ที่นี่
การประทับเวลา
เราเห็นว่า t.timestamps
ถูกเพิ่มในการโยกย้ายโดย Rails และเพิ่มคอลัมน์created_at
และ updated_at
ไปที่ events
โต๊ะ. คอลัมน์พิเศษเหล่านี้ถูกใช้โดย Railsto เพื่อติดตามว่าเร็กคอร์ดถูกสร้างขึ้นและอัปเดตเมื่อใด Rails จะเพิ่มค่าให้กับคอลัมน์เหล่านี้เมื่อมีการสร้างเรกคอร์ดและต้องแน่ใจว่าได้อัปเดตเมื่อมีการอัพเดตเรคคอร์ด คอลัมน์เหล่านี้ช่วยเราในการติดตามอายุของบันทึกฐานข้อมูล
updated_at
คอลัมน์ไม่ได้รับการอัปเดตเมื่อเราดำเนินการupdated_all
วิธีการจาก Rails
การจัดการกับความล้มเหลว
การย้ายถิ่นไม่ใช่ระบบกันกระสุน พวกเขาสามารถล้มเหลว เหตุผลอาจเป็นไวยากรณ์ที่ไม่ถูกต้องหรือแบบสอบถามฐานข้อมูลที่ไม่ถูกต้อง ไม่ว่าด้วยเหตุผลใด เราต้องจัดการกับความล้มเหลวและกู้คืนจากมัน เพื่อไม่ให้ฐานข้อมูลอยู่ในสถานะที่ไม่สอดคล้องกัน Rails แก้ปัญหานี้ด้วยการเรียกใช้แต่ละการย้ายข้อมูลภายในธุรกรรม หากการย้ายล้มเหลว ธุรกรรมจะถูกย้อนกลับ เพื่อให้แน่ใจว่าฐานข้อมูลไม่เข้าสู่สถานะที่ไม่สอดคล้องกัน
ทำได้เฉพาะกับฐานข้อมูลที่สนับสนุนธุรกรรมสำหรับการอัพเดตสคีมาฐานข้อมูล พวกเขาเรียกว่าธุรกรรม Data Definition Language (DDL) MySQL และ PostgreSQL รองรับธุรกรรม DDL
บางครั้ง เราไม่ต้องการดำเนินการย้ายข้อมูลบางอย่างภายในธุรกรรม ตัวอย่างง่ายๆ คือ เมื่อเพิ่มดัชนีที่เกิดขึ้นพร้อมกันใน PostgreSQL ไม่สามารถดำเนินการย้ายดังกล่าวภายในธุรกรรม DDL เช่น PostgreSQLtries เพื่อเพิ่มดัชนีโดยไม่ต้องรับการล็อกบนโต๊ะ เพื่อให้เราสามารถเพิ่มดัชนีในฐานข้อมูลที่ใช้งานจริงโดยไม่ต้องลบฐานข้อมูล Rails มอบวิธีการยกเลิกการทำธุรกรรมภายในการย้ายข้อมูลในรูปแบบของ disable_ddl_transactions!
.
def change
disable_ddl_transactions!
add_index :events, :user_id, algorithm: :concurrently
การดำเนินการนี้จะไม่เรียกใช้การย้ายข้อมูลภายในธุรกรรม หากการย้ายถิ่นล้มเหลว เราต้องกู้คืนด้วยตนเอง ในกรณีนี้ เราสามารถ REINDEX
หรือลบดัชนีแล้วลองเพิ่มอีกครั้ง
การย้ายถิ่นแบบย้อนกลับได้
Rails ช่วยให้เราสามารถย้อนกลับการเปลี่ยนแปลงฐานข้อมูลด้วยคำสั่งต่อไปนี้
rails db:rollback
คำสั่งนี้จะคืนค่าการย้ายข้อมูลล่าสุดที่รันบนฐานข้อมูล หากการย้ายข้อมูลเพิ่มคอลัมน์event_type
จากนั้นการย้อนกลับจะลบคอลัมน์นั้น หากการย้ายข้อมูลเพิ่มดัชนี การย้อนกลับจะลบดัชนีนั้นออก
นอกจากนี้ยังมีคำสั่งสำหรับการย้อนกลับการโยกย้ายก่อนหน้าและเรียกใช้ มันคือ
rails db:redo
.
Rails ฉลาดพอที่จะรู้วิธีย้อนกลับการโยกย้ายส่วนใหญ่ แต่เรายังสามารถให้คำแนะนำแก่ Railson เกี่ยวกับวิธีคืนค่าการย้ายข้อมูลด้วยการให้ up
และ down
แทนการใช้ change
method.The up
เมธอดจะถูกใช้เมื่อมีการเรียกใช้การโยกย้ายในขณะที่ down
เมธอดจะถูกใช้เมื่อมีการย้อนกลับการย้ายข้อมูล
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
. เราระบุวิธีการย้อนกลับใน down
วิธีการ
การย้ายข้อมูลแบบเดียวกันนี้สามารถเขียนได้โดยใช้ change
วิธีการ
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
วิธีการ
def change
revert CreateEvents
create_table :events do
...
end
end
revert
เมธอดยังยอมรับการบล็อกเพื่อคืนค่าการย้ายข้อมูลบางส่วนด้วย
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
การดำเนินการแบบดิบ
บางครั้ง เราต้องการรัน SQL ที่ซับซ้อนภายในการย้ายข้อมูล ในกรณีเช่นนี้ เราสามารถลืม DSL การย้ายข้อมูลทั่วไปและรัน SQL ดิบแทนได้ดังนี้
def change
execute <<-SQL
....
SQL
end
หลายฐานข้อมูลและการย้ายข้อมูล
Rails 6 เพิ่มการรองรับสำหรับการใช้หลายฐานข้อมูลภายในแอปพลิเคชัน Rails เดียว หากเราต้องการใช้หลายฐานข้อมูล เรากำหนดค่าฐานข้อมูลเหล่านั้นใน database.yml
ไฟล์.
development:
primary:
<<: *default
database: db/development.sqlite3
analytics:
adapter: sqlite3
database: db/analytics_dev.sqlite3
การกำหนดค่านี้บอก Rails ว่าเราต้องการใช้สองฐานข้อมูล—primary
และ analytics
ดังที่เราเห็นก่อนหน้านี้ การย้ายข้อมูลจะถูกเก็บไว้ใน db/migrate
ไดเรกทอรีโดยค่าเริ่มต้น แต่ในกรณีนี้ เราไม่สามารถเพิ่มการโยกย้ายของฐานข้อมูลทั้งสองภายในไดเร็กทอรีเดียว เราไม่ต้องการเรียกใช้การโยกย้ายของ analytics
ฐานข้อมูลใน primary
ฐานข้อมูลและในทางกลับกัน หากเราใช้หลายฐานข้อมูล จำเป็นต้องจัดเตรียมเส้นทางสำหรับจัดเก็บการย้ายข้อมูลสำหรับฐานข้อมูลที่สอง ซึ่งสามารถทำได้โดยระบุ migrations_paths
ใน database.yml
.
development:
primary:
<<: *default
database: db/development.sqlite3
analytics:
adapter: sqlite3
database: db/analytics_dev.sqlite3
migrations_paths: db/analytics_migrate
จากนั้น เราสามารถสร้างการย้ายข้อมูลสำหรับ analytics
ฐานข้อมูลดังนี้
rails generate migration AddExperiments rule:string active:boolean --db=analytics
สิ่งนี้จะสร้างการย้ายข้อมูลภายใน db/analytics_migrate
และเราสามารถเรียกใช้ได้ดังนี้
rails db:migrate --db=analytics
หากเรารันเฉพาะ rails db:migrate
จะทำการย้ายฐานข้อมูลทั้งหมด
analytics
ฐานข้อมูลจะมีschema_migrations
. เป็นของตัวเอง ตารางเพื่อติดตามว่าการย้ายข้อมูลใดที่เรียกใช้และรายการใดที่ไม่ได้ดำเนินการ
เรียกใช้การย้ายข้อมูลระหว่างการปรับใช้
เนื่องจากการย้ายข้อมูลสามารถเปลี่ยนสถานะของฐานข้อมูล และรหัสของเราอาจขึ้นอยู่กับการเปลี่ยนแปลงเหล่านั้น จึงเป็นเรื่องสำคัญอย่างยิ่งที่จะต้องมีการเรียกใช้การย้ายก่อนที่จะใช้รหัสใหม่
ในการปรับใช้ตาม Heroku การย้ายข้อมูลสามารถทำได้ใน version
เฟสของ Procfile
.
# Profile
web: bin/puma -C config/puma.rb
release: bundle exec rake db:migrate
เพื่อให้แน่ใจว่ามีการเรียกใช้การย้ายข้อมูลก่อนที่จะรีสตาร์ทไดโนของแอป
ในการปรับใช้ตาม Capistrano การย้ายข้อมูลควรทำงานก่อนที่เซิร์ฟเวอร์จะรีสตาร์ท
ในการปรับใช้ตามนักเทียบท่า เราสามารถเรียกใช้คอนเทนเนอร์ข้างเคียงเพื่อเรียกใช้การย้ายข้อมูลก่อนที่จะรีสตาร์ทแอป สิ่งนี้สำคัญมาก มิฉะนั้น คอนเทนเนอร์ใหม่อาจเข้าสู่สถานะไม่สอดคล้องกันหากพวกเขาเริ่มใช้รหัสใหม่ก่อนที่จะใช้การเปลี่ยนแปลงฐานข้อมูลสำหรับรหัสใหม่นั้น
บทสรุป
ในโพสต์นี้ เราได้เห็นแง่มุมต่างๆ ของการเขียนการย้ายฐานข้อมูลใน Rails เรายังเห็นสิ่งที่ถือเป็นการโยกย้าย ตลอดจนวิธีจัดการกับความล้มเหลวและย้อนกลับการย้ายข้อมูลหากจำเป็น Rails 6 ช่วยให้เราใช้หลายฐานข้อมูลและต้องเพิ่มการโยกย้ายสำหรับแต่ละฐานข้อมูลแยกกัน สุดท้ายนี้ เราได้เห็นวิธีการเรียกใช้การย้ายข้อมูลระหว่างการปรับใช้งานโดยสังเขป เพื่อให้การเปลี่ยนแปลงฐานข้อมูลถูกนำไปใช้อย่างเหมาะสมก่อนที่โค้ดใหม่จะเริ่มใช้งาน
ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!