DB Migrations và Push an toàn trong Rails
Bài đăng này đã không được cập nhật trong 5 năm
Vấn đề
Bạn muốn thêm một cột mới vào bảng cơ sở dữ liệu để ActiveRecord có thể đọc và cập nhật một thuộc tính mới cho model tương ứng. Bạn chỉ cần gửi một pull request làm 2 điều:
- Tạo một DB migration thêm trường mới vào bảng.
- Thêm code đọc và ghi dữ liệu vào cột mới.
Ví dụ:
class AddSerialNumberToWidgets < ActiveRecord::Migration
def change
add_column :widgets, :serial_number, :integer
end
end
class WidgetsController < ApplicationController
# other methods omitted from this example
def show
widget = Widget.find(params.require(:id))
@serial_number = widget.serial_number
end
end
Bạn commit thay đổi và sau khi deploy code lên production, bạn nhận được lỗi sau:
NoMethodError: undefined method ‘serial_number’ for #<Widget:0x007fc18bd8fb68>
Chuyện gì đã xảy ra?
Lỗi trên thông báo rất rõ ràng rằng code đã truy cập vào một method không tồn tại trên object Widget
. Bạn biết rằng một class kế thừa từ ActiveRecord::Base
sẽ tự động có các instance methods tương ứng với các cột trong cơ sở dữ liệu, do đó lỗi trên chỉ xảy ra khi cột serial_number
không tồn tại trong cơ sở dữ liệu. Nhưng trong code mới của ta đã có thêm cột trên vào cơ sở dữ liệu. Vậy điều gì sai ở đây?
Vấn đề là việc deploy code mới và chạy các migration tương ứng là non-atomic:
- Môi trường production có thể bao gồm nhiều webserver ảo. Trong vài phút khi deploy code, một số server có code mới trong khi số khác vẫn chạy code cũ.
- DB migration có thể hoàn tất sau khi một số server chay code mới và bắt đầu phục vụ người dùng.
Nói cách khác, khi bạn deploy sự thay đổi, môi trường production không biến đổi ngay lập tức từ thế giới cũ sang mới. Cụ thể, cả hai tình huống sau có thể xảy ra trong khoảng thời gian không hề nhỏ trong quá trình triển khai:
- Một server chạy code mới, nhưng DB migration vẫn chưa thực hiện.
- Một server chạy code cũ, nhưng DB migration đã hoàn thành.
Trong ví dụ trên, lỗi xảy ra ở trường hợp 1, một hoặc nhiều server chạy code mới tham chiếu đến serial_number
của Widget
, nhưng trường đó vẫn chưa được thêm vào DB.
Giải pháp cho ví dụ
Trong ví dụ này, tất cả những gì bạn phải làm là chia sự thay đổi của bạn thành hai lần push riêng biệt:
- Đầu tiên, push code migrate và chạy migration.
- Push code có tham chiếu đến cột mới.
Một ví dụ khác
Đôi khi việc thay đổi cấu trúc cơ sở dữ liệu phức tạp hơn việc chỉ thêm một vài cột. Xem xét tình huống giả định sau: Ta có một model Widget
có quan hệ has_one :widget_configuration
. Model WidgetConfiguration
có chứa một số thuộc tính liên quan đến Widget
. Nhưng theo thời gian, một số thuộc tính không cần thiết nữa và các cột được loại bỏ và chỉ còn lại widget_shape
. Để đơn giản code và khiến các truy vấn cơ sở dữ liệu hiệu quả hơn, ta sẽ loại bỏ bảng widget_configurations
và thêm cột widget_shape
vào bảng widgets
.
Vấn đề là mỗi giây, người dùng web đọc, tạo, sửa, xóa hàng ngàn bản ghi widget
và widget_configurations
tương ứng. Làm thế nào để ta có thể thay đổi cấu trúc này mà không làm trang web ngừng hoạt động.
Những điều gì chúng ta muốn xảy ra?
- Đầu tiên, ta cần dữ liệu của
widget_shape
được chuyển sang bảng widgets và nó được up-to-date mà không phá vỡ các chức năng đọc và ghiwidget_configurations
khi ta vẫn đang phụ thuộc vào bảng này. - Chuyển việc đọc từ
widget_configurations
sang đọc từ cột mới. - Dừng việc ghi vào bảng
widget_configurations
. - Drop bảng
widget_configurations
.
Giải pháp:
- Tạo cột mới
idget_shape
cho bảngwidgets
. - Thay đổi những chỗ sửa vào bảng
widget_configurations
thì sửa cả vào cộtidget_shape
trongwidgets
. - Chạy DB migration và Rake task để chuyển dữ liệu từ bảng cũ sang cột mới.
- Thay đổi những chỗ đọc từ bảng
widget_configurations
thì đọc từ cộtidget_shape
trongwidgets
. - Drop bảng
idget_configurations
.
Các tình huống khác với DB migration trong đó push an toàn có thể có liên quan
Bất cứ khi nào bạn có hai điều cần phải làm với nhau và cách nhau theo thời gian, bạn có khả năng gặp vấn đề về push an toàn. Ví dụ:
- Job bất đồng bộ: một job có thể được thêm vào hàng đợi, nhưng khi nó được lấy ra để thực thi, callback method của nó không còn tồn tại nữa.
- Web forms: Giả sử người dùng mở một form trước khi deploy, form chỉ có hai trường email và password. Và khi submit form, code mới được deploy và controller yêu cầu một trường mới.
- ...
Tham khảo
https://medium.com/czi-technology/db-migrations-and-push-safety-in-rails-508bc877dd7e
All rights reserved