I. Tổng quan
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:
1 2 | 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.
1 2 | 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
.
1 2 3 4 5 6 7 8 9 10 | 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ứcchange
tạo một bảngusers
với một cộtname
dạngstring
.
4. t.timestamps
- Migration sử dụng code
t.timestamps
để thêm mốc thời giancreated_at
vàupdated_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.
II. Chi tiết
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
.
1 2 | 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
.
1 2 3 | 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.rb
và load
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.
1 2 3 4 5 6 7 8 9 10 | 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.
1 2 3 | 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ó.
1 2 3 4 5 6 7 8 | 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_at
và updated_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:
1 2 | 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 up
và down
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.
1 2 3 4 5 6 7 8 9 10 11 12 | 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:
1 2 3 4 5 6 7 8 9 | 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:
1 2 3 4 5 6 7 8 | 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.
1 2 3 4 5 6 7 8 9 10 | 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 |
III. Kết luận
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