Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm Chủ Rails Migrations: Viết, Quản Lý, Và Rollback Hiệu Quả

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 change chứa DSL code manipulate database. Trong case này, change tạo events table với cột category type string.
  • Migration dùng code t.timestamps thêm timestamps created_at và updated_at vào events table.

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_column
  • add_index
  • remove_index
  • rename_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_at không updated khi execute updated_all method 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 analytics sẽ có own schema_migrations table 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ỡ!