Active Record Migrations

1. Migration là gì?

Migrations là một tính năng của Active Record cho phép bạn thay đổi cấu trúc của database và cả dữ liệu trong database. Migrations sử dụng Ruby DSL để mô tả sự thay đổi trong database thay thế cho việc viết những đoạn SQL gốc, bạn có thể dễ dàng đọc và thao tác chỉnh sửa nhanh hơn.

Bạn có thể nghĩ mỗi migration như một phiên bản (version) của database. Ban đầu bạn có một schema trống sau đó mỗi migratio sẽ sửa lại schema ấy bằng cách thêm, sửa hoặc xóa các bảng, cột hoặc cả dữ liệu trong database. Mỗi khi một migration được chạy Active Record sẽ cập nhật lại file db/schema tương ứng với cấu trúc của database hiện tại để tiện cho việc theo dõi. Vậy tạo một migration như thế nào?

2 Tạo migration

Dưới đây tôi sẽ hướng dẫn dùng Migration để tạo một bảng là products với các cột là name và description.

Đầu tiên bạn chạy lệnh rails generate migration create_products để tạo ra file migration db/migrate/20160126075236_create_products.rb

* Chú ý: tất cả các file migrate đều được tạo trong thư mục db/migrate và bắt đầu bằng một dãy số (như ở trên là 20160126075236) đây chính là version của migration này. Dãy số này được sinh ra khi bạn chạy lệnh tạo ra migration với giá trị là timestamps (utc) theo định dạng YYYYMMDDHHMMSS.

Bạn sẽ có được file migrate với nội dung như sau

class CreateTableProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
    end
  end
end

Bạn có thể thấy trong migration trên chỉ có lệnh tạo bảng products bạn cần thêm các cột name và descript tion cho bảng này. Việc thêm 2 cột này cũng rất đơn giản như sau:

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
    end
  end
end

* Migration cho phép bạn có thể tạo migration nhanh hơn với việc truyền các thông tin về tên cột và dữ liệu cho cột bằng cách sau:

rails generate migration create_products name:string description:text

Sau khi chạy lệnh này bạn cũng có được kết quả tương tự như nội dung ở trên. Trong Rails các bảng trong Active Record thường được thêm 2 trường là created_atupdated_at kiểu dữ liệu datetime để lưu lại thời gian tạo và cập nhật (update) cho bản ghi. Để thêm 2 cột này ta thêm t.timestamps null: false trong file migration khi tạo bảng

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps null: false
    end
  end
end

Như vậy là bạn đã tạo thành công một file migration tạo ra một bảng là products với 2 cột là name, description và timestamps (created_at, updated_at). Để thực thi migration này ta chỉ cần chạy

rake db:migrate

Sau khi chạy file migration này bạn sẽ thấy trong database thay đổi. Và Active Record cũng thay đổi trong db/schema.rb tương ứng với database. Mặc định thì file schema này sẽ lưu cấu trúc của database dưới dạng code ruby. Nội dung của schema sẽ được thay đổi như sau

ActiveRecord::Schema.define(version: 20160126075236) do

  create_table "table_products", force: :cascade do |t|
    t.string   "name",        limit: 255
    t.text     "description", limit: 65535
    t.datetime "created_at",                null: false
    t.datetime "updated_at",                null: false
  end

end

Bạn có thể thấy nội dung trong schema này cũng khá giống với migration chúng ta vừa tạo trên. Dòng lệnh đầu tiên trong file này là ActiveRecord::Schema.define(version: 20160126075236) do và nhận thấy số 20160126075236 giống với số đầu tiên trong tên file migration ta vừa tạo 20160126075236_create_products.rb. Đây là version hiện tại của schema cũng như version của database. Vậy Active Record quản lí các version như thế nào?

3 Quản lí các version

Active Record quản lí các versions trong database bằng cách tạo ra một bảng có tên là schema_migrations. Khi chạy file migration đầu tiên nếu trong database vẫn chưa có bảng này thì Active Record sẽ tự động tạo ra bảng này và cập nhật lại version mới nhất vừa được chạy. Bảng schema_migrations này có cấu trúc như sau

mysql> desc schema_migrations;
+---------+--------------+------+-----+---------+-------+
| Field   | Type         | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| version | varchar(255) | NO   | PRI | NULL    |       |
+---------+--------------+------+-----+---------+-------+
1 row in set (0.00 sec)

Bảng này có một trường là version lưu lại lịch sử các version. Chúng ta thử kiểm tra dữ liệu trong bảng này

mysql> select * from schema_migrations;
+----------------+
| version        |
+----------------+
| 20160126075236 |
+----------------+
1 row in set (0.00 sec)

Có thể thấy giá trị lưu trữ trong bảng này là 20160126075236 trùng với version trong schema và trùng với version của file migrate chúng ta vừa tạo ở trên. Cũng nhờ bảng này mà Active Record có thể biết được các file migration đã được chạy chưa. Khi chạy lệnh rake db:migrate Active Record sẽ tìm những file migration nào chưa được chạy và chạy theo thứ tự version từ nhỏ đến lớn (nghĩa là có nhiều file migration chưa được chạy thì chúng sẽ chạy theo thứ tự version từ nhỏ đến lớn). Để kiểm tra chúng ta tạo thêm một migration nữa thực hiện thêm cột price cho products và một migration khác thêm code cho products với nội dung như sau

rails generate migration add_price_to_products
rails generate migration add_code_to_products
# db/migrate/20160126094450_add_price_to_products.rb
class AddPriceToProducts < ActiveRecord::Migration
  def change
    add_column :products, :price, :integer
  end
end

# db/migrate/20160126094701_add_code_to_products.rb
class AddCodeToProducts < ActiveRecord::Migration
  def change
    add_column :products, :code, :string
  end
end

Có thể thấy version của db/migrate/20160126094450_add_price_to_products.rb(20160126094450) nhỏ hơn version của db/migrate/20160126094701_add_code_to_products.rb(20160126094701). Như vậy thứ tự chạy sẽ là file db/migrate/20160126094450_add_price_to_products.rb chạy trước và db/migrate/20160126094701_add_code_to_products.rb chạy sau. Kiểm tra lại ta sẽ thấy

rake db:migrate

Kiểm tra trong db/schema.rb ta thấy schema mới có nội dung như sau

ActiveRecord::Schema.define(version: 20160126094701) do

  create_table "products", force: :cascade do |t|
    t.string   "name",        limit: 255
    t.text     "description", limit: 65535
    t.datetime "created_at",                null: false
    t.datetime "updated_at",                null: false
    t.integer  "price",       limit: 4
    t.string   "code",        limit: 255
  end

end

Như vậy version hiện tại trong schema là 20160126094701 chính là version cuối của của migration và nội dung của schema cũng được cập nhật sau khi chạy các migrations.

Kiểm tra trong bảng schema_migrations

mysql> select * from schema_migrations;
+----------------+
| version        |
+----------------+
| 20160126075236 |
| 20160126094450 |
| 20160126094701 |
+----------------+
3 rows in set (0.00 sec)

Ta thấy trong bảng schema_migrations có 3 bản ghi lưu lại các version theo thứ tự tăng dần. Giá trị cuối cùng là 20160126094701 cũng đúng với giá trị của version trong file db/schema.rb và version của file migrate cuối cùng được chạy.

4 Rollback to old version

Để rollback tới các phiên bản cũ hơn. Active Record cung cấp lệnh rake db:rollback. Khi chạy lệnh này thì Active Record sẽ tìm trong bảng schema_migrations bản ghi cuối cùng (version cao nhất) rồi tìm đến migration tương ứng với version này. Nếu trong migration này có method down thì method này sẽ được chạy. Còn như ví dụ trên thì chỉ có method change, Active Record sẽ cố gắng đọc nội dung trong method change và thực hiện ngược lại trong trường hợp rollback. Ví dụ create_table ngược lại sẽ là drop_table, add_column thì ngược lại sẽ là remove_column. Trong một số trường hợp Active Record không thể thực hiện được rollback với method change, ví dụ như change_column hay thực hiện một số lệnh thay đổi dữ liệu trong database bạn cần viết rõ method up và down như vậy việc rollback sẽ dễ dàng hơn.

Giờ chúng ta sẽ thử chức năng rollback của Active Record, chạy thử lệnh

rake db:rollback
== 20160126094701 AddCodeToProducts: reverting ================================
-- remove_column(:products, :code, :string)
   -> 0.7768s
== 20160126094701 AddCodeToProducts: reverted (0.7958s) =======================

Quay trở lại db/schema.rb ta sẽ thấy nội dung trong file này đã thay đổi. Version được thay đổi thành 20160126094450 giống với version ngay trước đó. Cấu trúc của bảng trong schema cũng thay đổi xóa đi cột code giống với cấu trúc của database. Như vậy cấu trúc của database đã quay về giống với phiên bản trước khi chạy migration có version 20160126094701.

ActiveRecord::Schema.define(version: 20160126094450) do

  create_table "products", force: :cascade do |t|
    t.string   "name",        limit: 255
    t.text     "description", limit: 65535
    t.datetime "created_at",                null: false
    t.datetime "updated_at",                null: false
    t.integer  "price",       limit: 4
  end

end

Kiểm tra lại bảng schema_migrations ta thấy bản ghi có version là 20160126094701 đã mất và bản ghi mới nhất hiện tại là 20160126094450

mysql> select * from schema_migrations;
+----------------+
| version        |
+----------------+
| 20160126075236 |
| 20160126094450 |
+----------------+
2 rows in set (0.00 sec)

Bạn có thể rollback tới các version xa hơn thông qua options STEP=<number>. Giá trị number tương ứng với số lượng rollback được chạy. Ví dụ STEP=3 cho kết quả tương đương với 3 lần chạy rake db:rollback

rake db:rollback STEP=1

Qua các ví dụ trên có thể thấy việc quản lý thay đổi cấu trúc của database với Active Record Migration khá là đơn giản và hiệu quả. Thay thế cho việc cập nhật cấu trúc dữ liệu một cách thủ công rất khó quản lý.

Còn rất nhiều lệnh khác để thay đổi cấu trúc database với Active Record Migration bạn có thể tham khảo chi tiết hơn tại active_record_migrations

Cảm ơn đã theo dõi bài viết!!! 😃


All Rights Reserved