Hôm nay, chúng ta sẽ deep dive vào Rails migrations. Chúng ta sẽ break down migration thành các pieces khác nhau, và trong quá trình, học cách viết effective migration. Học cách viết migrations cho multiple databases, cũng như handle failed migrations và techniques rollback.
Để hiểu toàn bộ bài, cần basic understanding của databases và Rails.
Migrations 101
Migrations trong Rails cho phép evolve database over lifetime của application. Migrations cho phép viết plain Ruby code alter state của database bằng cách cung cấp elegant DSL. Không phải viết database-specific SQL vì migrations cung cấp abstractions manipulate database và take care nitty-gritty details convert DSL thành database-specific SQL queries behind the scene. Migrations cũng get out of our way và cung cấp ways execute raw SQL trên database, nếu need arises.
Hai Mươi Ngàn Dặm Dưới Đáy Rails Database Migration
Chúng ta có thể create tables, add/remove columns, add indexes on columns dùng migrations.
Mọi Rails app có special directory —
db/migrate— nơi tất cả migrations stored.
Bắt đầu với migration tạo table events vào database.
$ rails g migration CreateEvents category:string
Lệnh này generate timestamped file 20200405103635_create_events.rb trong db/migrate. Nội dung file:
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 này.
- Mọi migration file Rails generate có timestamp trong filename. Timestamp quan trọng và dùng bởi Rails confirm migration đã run hay chưa, như sau.
- Migration chứa class inherit từ
ActiveRecord::Migration[6.0]. Vì dùng Rails 6, migration superclass có[6.0]. Nếu dùng Rails 5.2, superclass làActiveRecord::Migration[5.2]. Sau sẽ discuss tại sao Rails version là part của superclass name. - Migration có method
changechứa DSL code manipulate database. Trong case này,changetạoeventstable với cộtcategorytypestring. - Migration dùng code
t.timestampsthêm timestampscreated_atvàupdated_atvàoeventstable.
Khi migration run dùng rails db:migrate, sẽ tạo events table với cột category type string và timestamp columns created_at và updated_at.
Actual database column type sẽ là varchar hoặc text, depending on database.
Quan Trọng Của Migration Timestamps Và Bảng schema_migration
Mỗi lần migration generated dùng rails g migration, Rails generate migration file với unique timestamp. Timestamp format YYYYMMDDHHMMSS. Khi migration run, Rails insert migration timestamp vào internal table schema_migrations. Bảng này created by Rails khi run first migration. Bảng chỉ có column version, cũng là primary key. Structure của schema_migrations table:
CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY);
Giờ đã run migration tạo events table, xem Rails đã store timestamp migration này trong schema_migrations chưa.
sqlite> select * from schema_migrations; 20200405103635
Nếu run migrations lại, Rails first check nếu entry exists trong schema_migrations với timestamp của migration file, và chỉ execute nếu không có entry đó. Đảm bảo incrementally add changes to database over time và migration chỉ run once trên database.
Database Schema
Khi run nhiều migrations hơn, database schema keep evolving. Rails store most recent database schema trong file db/schema.rb. File này là Ruby representation của all migrations run trên database over life của application. Vì file này, không cần keep old migrations files trong codebase. Rails cung cấp tasks dump latest schema từ database vào schema.rb và load schema vào database từ schema.rb. Vì vậy older migrations có thể safely deleted từ codebase. Loading schema vào database cũng faster so với chạy từng migration mỗi lần setup application.
Rails cũng cung cấp cách store database schema trong SQL format. Đã có bài so sánh hai formats. Đọc thêm tại đây.
Rails Version Trong Migration
Mọi migration generate có Rails version as part của superclass. Migration generated by Rails 6 app có superclass ActiveRecord::Migration[6.0] trong khi Rails 5.2 app có ActiveRecord::Migration[5.2]. Nếu old app Rails 4.2 hoặc dưới, notice no version in superclass. Superclass chỉ ActiveRecord::Migration.
Rails version added to migration superclass in Rails 5. Cơ bản đảm bảo migration API có thể evolve over time không break migrations generated by older Rails versions.
Look deeper vào này bằng cách xem same migration tạo events table trong 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
Nếu look schema của events table generated by Rails 6 migration, thấy NOT NULL constraint cho timestamps columns exist.
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);
Điều này vì, từ Rails 5 trở đi, migration API tự động add NOT NULL constraint cho timestamp columns không cần explicitly add trong migration file. Rails version trong superclass name đảm bảo migration dùng migration API của Rails version mà migration generated for. Cho phép Rails maintain backward compatibility với older migrations, đồng thời evolve migrations API.
Thay Đổi Database Schema
Method change là primary method trong migration. Khi migration run, nó call change và execute code inside.
Cùng với create_table, Rails cung cấp powerful method khác — change_table. Như tên gợi ý, dùng alter schema của existing table.
def change
change_table :events do |t|
t.remove :category
t.string :event_type
t.boolean :active, default: false
end
end
Migration này remove category column từ events table, add new string column events_type và boolean column active với default false.
Rails cũng cung cấp nhiều helper methods khác dùng trong migration như:
change_columnadd_indexremove_indexrename_table
và nhiều hơn. Tất cả methods dùng với change tìm được ở đây
Timestamps
Thấy t.timestamps added to migration by Rails và thêm columns created_at và updated_at vào events table. Special columns này dùng bởi Rails track khi record created và updated. Rails add values vào columns khi record created và đảm bảo update khi record updated. Columns này help track lifetime của database record.
Column
updated_atkhông updated khi executeupdated_allmethod từ Rails.
Handling Failures
Migrations không bulletproof. Chúng có thể fail. Reason có thể wrong syntax hoặc invalid database query. Whatever reason, phải handle failure và recover để database không vào inconsistent state. Rails solve problem bằng cách run each migration inside a transaction. Nếu migration fail, transaction rolled back. Đảm bảo database không vào inconsistent state.
Chỉ done cho databases support transactions update database schema. Chúng gọi là Data Definition Language (DDL) transactions. MySQL và PostgreSQL đều support DDL transactions.
Đôi khi, không muốn execute certain migrations inside transaction. Ví dụ đơn giản: add concurrent index trong PostgreSQL. Những migrations không thể execute inside DDL transaction vì PostgreSQL thử add index không acquire locks trên table để add index trên live production database không down database. Rails cung cấp cách opt-out transactions trong migration dạng disable_ddl_transactions!.
def change
disable_ddl_transactions!
add_index :events, :user_id, algorithm: :concurrently
Này không run migration inside transaction. Nếu migration fail, cần recover ourselves. Trong case này, có thể REINDEX hoặc remove index và thử add lại.
Reversible Migrations
Rails cho phép rollback changes to database với command:
rails db:rollback
Command này revert last migration run on database. Nếu migration added column event_type thì rollback remove column đó. Nếu migration added index, rollback remove index đó.
Cũng có command rollback previous migration và run nó. Là
rails db:redo.
Rails đủ smart biết reverse most migrations. Nhưng cũng provide hints cho Rails how to revert migration bằng cách cung cấp up và down methods thay vì dùng change. up method dùng khi migration run, down method dùng khi 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
Trong example này, change price column của events từ integer sang string. Specify how rollback trong down method.
Same migration cũng viết được dùng 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 cũng cung cấp cách revert previous migration hoàn toàn dùng revert method.
def change
revert CreateEvents
create_table :events do
...
end
end
revert method cũng accept 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
Đôi khi, muốn execute complex SQL trong migration. Trong cases đó, quên typical migration DSL và execute raw SQL như sau.
def change
execute <<-SQL
....
SQL
end
Multiple Databases Và Migrations
Rails 6 added support dùng multiple databases trong single Rails application. Nếu muốn dùng multiple databases, configure chúng trong database.yml.
development:
primary:
<<: *default
database: db/development.sqlite3
analytics:
adapter: sqlite3
database: db/analytics_dev.sqlite3
Config này nói Rails muốn dùng hai databases — primary và analytics. Như thấy trước, migrations stored in db/migrate by default. Nhưng case này, không thể add migrations của both databases vào single directory. Không muốn run migrations của analytics database trên primary và ngược lại. Nếu dùng multiple databases, required provide path store migrations cho second database. Làm bằng cách provide migrations_paths trong database.yml.
development:
primary:
<<: *default
database: db/development.sqlite3
analytics:
adapter: sqlite3
database: db/analytics_dev.sqlite3
migrations_paths: db/analytics_migrate
Sau đó tạo migrations cho analytics database như sau.
rails generate migration AddExperiments rule:string active:boolean --db=analytics
Này tạo migration inside db/analytics_migrate, run nó như sau.
rails db:migrate --db=analytics
Nếu chỉ run rails db:migrate, sẽ execute migrations cho all databases.
Database
analyticssẽ có ownschema_migrationstable track migrations nào run và nào không.
Chạy Migrations Trong Deployment
Vì migrations có thể change state của database, và code của chúng ta might depend on those changes, extremely important migrations run first trước khi new code applied.
Trong Heroku-based deployments, migrations run in release phase của Procfile.
# Profile
web: bin/puma -C config/puma.rb
release: bundle exec rake db:migrate
Đảm bảo migrations run trước khi app dynos restarted.
Trong Capistrano-based deployments, migrations should run before server restarted.
Trong docker-based deployments, run sidecar container run migrations first trước khi app restarted. Rất quan trọng vì otherwise, new containers có thể vào inconsistent state nếu start dùng new code trước apply database changes cho new code đó.
Kết Luận
Trong bài này, thấy various aspects của writing database migration trong Rails. Cũng thấy migration constitute gì cũng như handle failures và rollback migrations nếu cần. Rails 6 cho phép multiple databases và migrations cho mỗi database cần added separately. Cuối, briefly saw cách run migrations trong deployment để database changes applied properly trước khi new code start using them.
P.S. Nếu muốn đọc bài Ruby Magic ngay khi xuất bản, đăng ký newsletter Ruby Magic để không bỏ lỡ!