Data Migrations in Rails
Bài đăng này đã không được cập nhật trong 8 năm
Bất cứ lúc nào, khi chúng ta cần phải thay đổi dữ liệu thực tế trên môi trường production. Dĩ nhiên tùy chọn đầu tiên xuất hiện trong đầu là sử dụng Rails migration
, đặc biệt kể từ khi migration
xuất hiện trong các task chính của, data migration
. Nhưng chúng ta nên nói chi tiết hơn và hãy để tôi cố gắng ngăn cản bạn làm như vậy.
Hãy nhìn vào Rails Guides for Active Records Migration, mở đầu phần đầu tiên là câu nói:
Migrations là những tính năng của
Active Record
cho phép bạn phát triểndatabase schema
theo thời gian. Thay vì viết thay đổischema
trong SQL thuần, thìmigrations
sẽ cho phép bạn dễ dàng sử dụngRuby DSL
để mô tả những thay đổi trong các table của bạn.
Bạn có nhận thấy rằng từ data
đã bị vắng mặt trong đoạn văn trên? Theo định nghĩa Rails migrations
chỉ nên được sử dụng để thay đổi schema
và không làm thay đổi dữ liệu thực tế của database.
Nói chung, việc thao tác dữ liệu trong migrations
là một ý tưởng tồi cho một số nguyên nhân. Thứ nhất, file data migrations sẽ ở trong thư mục db/migrate
và sẽ chạy bất cứ khi nào một developer mới cài đặt cho project của họ trên môi trường development của họ. Và có thể tương lai, chúng ta sẽ thay đổi một class và logic của nó có thể phá vỡ migration
sau này. Hơn nữa, đây không phải là business logic, do đó không nên ở lại trong code base mãi mãi.
Vấn đề thứ hai là data migrations
có thể bị từ chỗi trong tương lai bởi các nhà phát triển nếu thay vì chạy rake db:migrate
các developers chạy rake db:schema:load
or rake db:reset
. Cả hai lệnh trên chỉ đơn thuần là tải phiên bản mới nhất của cấu trúc database sử dụng file schema.rb
mà không hề động gì đến migrations
.
Thứ ba là việc triển khai ứng dụng của bạn lên môi trường production phụ thuộc vào data migration
được hoàn thành. Điều này không phải là vấn đề khi mà ứng dụng của bạn là mới (lần đầu được triển khai lên production) và dữ liệu của bạn nhỏ. Nhưng làm thế nào đối với một database lớn với hàng triệu bản ghi? Quá trình triển khai của bạn bây giờ phải chờ các thao tacs dữ liệu hoàn thành và có thể sẽ có một số rắc rối xảy ra như là có thể bị treo hoặc migration failed.
Tôi muốn gợi ý một thay thế tốt hơn bằng cách sử dụng cáctemporary rake tasks
. Temporary rake tasks
cho phép chúng ta tách quá trình deployment ra khỏi quá trình hoàn thiện migrations. Nó cho chúng ta kiểm soát nhiều hơn quá trình thao tác dữ liệu bằng cách đóng gói chúng vào một nơi nào đó. Nhược điểm là chúng ta cần nhớ và add rake task đó vào trong script deploy, hoặc chạy rake task đó thường xuyên sau khi deployment. Chúng ta cũng cần làm sạch sau khi deploy và cần xóa temporary rake task
khi các thay đổi đã được deploy và thực hiện.
Khi tạo ra một temporary rake task
cho một data migration
, có thể bạn sẽ bị cám dỗ viết cái gì đó giống như đoạn code dưới đây:
# lib/tasks/temporary/users.rake
namespace :users do
task :set_newsletter => :environment do
User.all.each do |user|
if user.confirmed?
user.receive_newsletter = true
user.save
end
end
end
end
Chúng ta có năm vấn đề với đoạn code như trên:
- Task đó sẽ đi qua tất cả các user
- Nó gọi
validations
vàcallbacks
, trong đó có thể gây ra những hậu quả ngoài ý muốn. - Nó sử dụng
if
block để kiểm tra xem người dùng nào cần update. - Nó không cho chúng ta một chỉ dẫn trực quan rằng nó thực sự làm việc
- Nó không bao gồm một mô tả và do đó chúng ta không thể thấy các task khi mà chúng ta chạy
rake -T
Đây là một gợi ý cho một rake task
tốt hơn
# lib/tasks/temporary/users.rake
namespace :users do
desc "Update confirmed users to receive newsletter"
task set_newsletter: :environment do
users = User.confirmed
puts "Going to update #{users.count} users"
ActiveRecord::Base.transaction do
users.each do |user|
user.mark_newsletter_received!
print "."
end
end
puts " All done now!"
end
end
Trong trường hợp này:
- Nó có đính kèm một mô tả, do vậy chúng ta sẽ thấy task đó thực thi và mô tả của nó khi chúng ta chạy
rake -T
- Nó dùng một
scope
để lấy hết tất cả các records cần update, vì vậy chúng ta có thể remove đượcif
block và giới hạn được số lượng records cần sử dụng. - Nó nói cho chúng ta biết trước được sẽ có bao nhiêu records sẽ được update, cho chúng ta một cách nhìn trực quan thực tế là nó đang làm việc, và khi nào thì nó thực thi xong.
- Nó bao bọc các thay đổi trong một giao dịch (transaction).
Nếu database của bạn hỗ trợ transaction
, nó luôn luôn là một ý tưởng tốt để bọc code mà bên trong là những dữ liệu thực sự cần thay đổi vào một transaction
. Transactions
giúp chúng ta đối phó được với crashes
, failures
, và data consistency
. Chúng rất quan trọng khi làm việc với nhiều object cùng phải thay đổi và chúng tôi muốn đảm bảo tính toàn vẹn của dữ liệu. Ví dụ, khi thực hiện một giao dịch chuyển tiền, chúng tôi muốn đảm bảo rằng sẽ không có một tình huống mà tiền được rút ra từ một tài khoản, nhưng không gửi vào một tài khoản khác.
Những ví dụ trên là rất ngắn. Nếu sự thay đổi dữ liệu đòi hỏi cần nhiều hành động hơn (actions), hãy xem xét việc kéo các hành vi đó vào method của chính nó.
Nếu database là khá lớn, nó cũng được đề nghị chạy data migration
theo batches. Bạn có thể tìm hiểu thêm về batches
trên Rails Guides tại Retrieving Multiple Objects in Batches.
Cuối cùng nhưng không kém phần quan trọng, nếu như bạn sử dụng gem Suspenders để khởi tạo base application với tiêu chuẩn mặc định (standard defaults) của thoughtbot, bạn có thể thấy rằng nó đã thêm file lib/tasks/dev.rake
và rake dev:prime
trong rake tasks
của bạn. Task này được dùng để add dữ liệu vào môi trường phát triển development vì thế seeds.rb
có thể được dành cho data cần thiết cho các môi trường phát triển khác (như staging hay production).
Kết luận, bằng cách tạo ra các temporary rake task
, chúng ta có được những lợi ích của việc deployments, migration
không bị phá hủy, và chúng ta có thể kiểm soát nhiều hơn trong quá trình thao tác dữ liệu. Và như tôi nhớ đã nhắc nhỡ rằng chúng ta nên nhớ chạy các migration rake task trên môi trường development và staging trước.
All rights reserved