Hi các bạn, là một Rails developer chắc hẳn bạn không còn xa lạ với khái niệm migration.

Migration trong Rails cho phép chúng ta phát triển database (cơ sở dữ liệu) trong suốt vòng đời của một ứng dụng. Migration cho phép chúng ta viết code Ruby đơn giản để thay đổi trạng thái của database bằng cách cung cấp elegant DSL. Chúng ta không cần phải viết SQL dành riêng cho database vì việc migration cung cấp các abstractions để thao tác database và quan tâm đến các chi tiết ... khi chuyển đổi DSL thành các truy vấn SQL dành riêng cho database. Migration cũng cung cấp các cách để thực thi raw SQL trên database, nếu có nhu cầu đó.

Khi tạo một ứng dụng bằng Rails nó luôn sinh ra một thư mục là db/migrate, thư mục này chứa tất cả các migrations chúng ta sẽ tạo.

Bắt đầu thôi, chúng ta sẽ tạo một rails app:

rails new migration_demo

Tiếp theo, chúng ta sẽ migration một bảng user với column là: name vào trong database.

rails g model User name:string

Lệnh trên sẽ tạo ra một file trong thư mục db/migrate có chứa timestamp như sau: 20200521135455_create_users.rb.

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name

      t.timestamps
    end
  end
end

Cùng mình tìm hiểu xem file migration này xem có gì nha:

1. Timestamp trong tên file:

  • Mọi migration file được tạo ra bởi Rails sẽ có timestamp trong tên file.

  • Mốc thời gian này rất quan trọng và được sử dụng bởi Rails để xác nhận khi nào một migration được chạy hoặc không. Chút nữa mình sẽ đi chi tiết vào phần này.

2. Phiên bản của Rails xuất hiện trong superclass

  • Migration chứa một class được kế thừa từ ActiveRecord::Migration[6.0]. Như mình đang sử dụng Rails 6, migration superclass chứa [6.0]. Nếu bạn sử dụng rails 5.2, thì superclass sẽ là ActiveRecord::Migration[5.2]. Chúng ta sẽ cùng thảo luận xem Tại sao version của Rails lại là một phần trong tên của superclass ở dưới nha.

3. Phương thức change

  • Migration có một phương thức change chứa DSL code thao tác với database. Trong ví dụ trên, phương thức change tạo một bảng users với một cột name dạng string.

4. t.timestamps

  • Migration sử dụng code t.timestamps để thêm mốc thời gian created_atupdated_at vào bảng trong database.

Khi chúng ta chạy migration bằng lệnh rails db:migrate, nó sẽ tạo một bảng users với cột name với type dạng string và cột created_at, updated_at với type dạng datetime.

Kiểu cột database thực tế sẽ là varchar hoặc text, tùy thuộc vào database.

1. Tầm quan trọng của timestamps và bảng schema_migration.

1.1. timestamps

Mỗi khi một migration được tạo bởi lệnh rails g migration, Rails sẽ tạo ra file migration với một timestamp duy nhất. Timestamp theo dạng YYYYMMDDHHMMSS. Khi một migration chạy, Rails inserts migration timestamp vào trong bảng schema_migrations. Bảng này được tạo bởi Rails khi chúng ta chạy migraion lần đầu tiên. Bảng này chỉ chứa cột version, nó cũng là primary key. Đây là kiến trúc của bảng schema_migrations.

CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY);

Bây giờ chúng ta chạy migration để tạo bảng users, hãy nhìn bên dưới, nếu Rails có chứa timestamp của migration này trong bảng schema_migrations.

sqlite> select * from schema_migrations;
20200405103635

Nếu chúng ta chạy lại migrations. Rails sẽ kiểm tra xem timestamp đó đã tồn tại trong bảng schema_migrations chưa, nếu đã tồn tại thì nó sẽ không được thực thi. Điều này đảm bảo rằng chúng ta có thể tăng dần các thay đổi cho database theo thời gian và việc migration sẽ chỉ chạy một lần trên database.

1.2. schema migration

Khi chúng ta chạy migrations nhiều lần. Database schema sẽ tiếp tục được mở rộng. Rails lưu trữ database schema gần nhất trong file db/schema.db. File này là đại diện của tất cả các lần migration chạy trên database trong suốt vòng đời của ứng dụng.

Vì file này, chúng ta không cần giữ các file migrations cũ trong codebase. Rails cung cấp tác vụ để dump schema cuối cùng từ database vào schema.rbload schema vào database từ schema.rb. Vì thế, các migrations cũ hơn có thể được xóa an toàn khỏi codebase. Việc load schema vào database cũng nhanh hơn so với việc chạy từng migration mỗi khi chúng ta setup ứng dụng.

Như trên chúng ta đã đề cập đến verson của rails trong file migation, bây giờ hãy cùng tôi tìm hiểu lý do vì sao nhé.

2. Rails version trong file migration

Mọi migration mà chúng ta tạo ra đều có phiên bản của Rails như một phần của superclass. Vì vậy nếu ứng dụng của chúng ta đang dùng Rails 6, thì tất nhiên trong file migration của chúng ta sẽ có dạng ActiveRecord::Migration[6.0], nếu ứng dụng sử dụng Rails 5.2 thì nó sẽ có dạng ActiveRecord::Migration[5.2],

Còn nếu ứng dụng của bạn sử dụng Rails 4.2 trở xuống, bạn sẽ thấy không có phiên bản của Rails được thêm trong file migration, nó sẽ có dạng như sau: ActiveRecord::Migration

Phiên bản của Rails được thêm vào trong migation từ Rails 5. Điều này về cơ bản đảm bảo rằng migration API có thể phát triển theo thời gian mà không phá vỡ migrations được tạo bởi các phiên bản cũ hơn của Rails.

Để hiểu sâu hơn về vấn đề này, chúng ta hãy cùng xem cùng một migration để tạo ra bảng users trong Rails 4.2.

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name

      t.timestamps null: false
    end
  end
end

Nếu chúng ta nhìn vào schema của bảng users được tạo bởi Rails 6, chúng ta có thể thấy ràng buộc NOT NULL cho các cột timestamps tồn tại.

sqlite> .schema users
CREATE TABLE IF NOT EXISTS "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);

Điều này là do, bắt đầu từ Rails 5 trở đi, migration API sẽ tự động thêm ràng buộc NOT NULL vào các cột timestamps mà không cần thêm nó vào trong file migration. Phiên bản của Rails trong tên superclass đảm bảo rằng việc migration sử dụng migration API của phiên bản Rails mà quá trình migration được tạo. Điều này cho phép Rails duy trì khả năng tương thích, ngược với các lần migration cũ hơn, đồng thời phát triển migation API.

3. Phương thức change

Phương thức change là một phương thức chính trong một migration. Khi migration được chạy, nó gọi phương thức change và thực thi code bên trong nó.

Cùng với create_table, Rails cũng cung cấp phương thức khác là change_table. Như tên cho thấy nó được sử dụng để thay đổi schema của một bảng hiện có.

def change
  change_table :users do |t|
    t.remove :name
    t.string :email
    t.boolean :active, default: false
  end
end

Trong ví dụ trên mình bỏ trường name và thêm 2 trường là email dạng string và active dạng boolean với giá trị mặc định là false.

Rails cũng cung cấp rất nhiều phương thức hỗ trợ khác có thể sử dụng như:

  • add_column
  • remove_column
  • add_timestamps
  • rename_table

Và một số phương thức khác bạn có thể tìm ở đây

4. t.timestamps

TIMESTAMPS

Chúng ta đã thấy t.timestamps được thêm bên trong migration bởi Rails và tạo ra 2 cột là created_atupdated_at. Các cột đặc biệt này được Rails sử dụng để theo dõi khi nào bản ghi được tạo mới hay cập nhật. Rails thêm giá trị cho các cột này khi bản ghi được tạo và đảm bảo cập nhật chúng khi bản ghi được cập nhật. Các cột này giúp chúng ta theo dõi thời gian tồn tại của một bản ghi trong database.

Note: Cột updated_at không được update khi chúng ta thực thi phương thức update_all trong Rails. Vì phương thức update_all nó sẽ thực thi cập nhật theo cột chứ không cập nhật theo hàng, nên nó chỉ cập nhật cột chúng ta chỉ định mà thôi.

5. Revert migrations

Rails cho phép chúng ta khôi phục (rollback) các thay đổi cho database bằng lệnh sau:

rails db:rollback

Lệnh này reverts migration cuối cùng đã được chạy trên database. Như ví dụ trên đưa ra, nếu migration đã xóa cột name thì sau khi chạy lệnh này, nó sẽ thêm lại cột đó.

Ngoài ra có một lệnh khác để rollback migration trước đó và chạy nó là: rails db:redo.

Rails đủ thông minh để biết cách reverse hầu hết các migrations. Nhưng chúng ta cũng có thể cung cấp các gợi ý cho Rails để revert một migration bằng các phương thức updown thay vì sử dụng phương thức change. Phương thức up sẽ được sử dụng khi migration chạy trong khi phương thức down sẽ được sử dụng khi migration được rollback.

def up
  change_table :users do |t|
    t.change :phone_number, :string
  end
end

def down
  change_table :users do |t|
    t.change :phone_number, :integer
  end
end

Trong ví dụ trên, chúng ta thay đổi cột phone_number từ integer sang string.

Tương tự chúng ta cũng có thể viết trong phương thức change như sau:

def change
  reversible do |direction|
    change_table :users do |t|
      direction.up { t.change :phone_number, :string }
      direction.down { t.change :phone_number, :integer }
    end
  end
end

Rails cũng cung cấp một cách khác để revert migration trước đó bằng cách sủ dụng phương thức revert như sau:

def change
  revert CreateUsers

  create_table :users do
   ...
  end
end

Phương thức revert cũng chấp nhận một block để revert một phần migration.

def change
  revert do
    reversible do |direction|
      change_table :users do |t|
        direction.up { t.remove :name }
        direction.down { t.string :name }
      end
    end
  end

Trên đây là tìm hiểu của mình về migration trong Rails, hy vọng hữu ích cho các bạn. Và nếu các bạn biết hơn về migration thì đừng quên comment bên dưới để chúng ta cùng hiểu sâu hơn về migration nha. Cảm ơn các bạn.

Link tham khảo: https://guides.rubyonrails.org/active_record_migrations.html https://guides.rubyonrails.org/v3.2/migrations.html