12/08/2018, 14:10

Transactions trong Rails

Transactions trong Rails Transaction (giao dịch) được dùng để đảm bảo tính toàn vẹn dữ liệu khi xảy ra cập nhật (cập nhật xin được hiểu theo nghĩa rộng là các hành động sửa đổi dữ liệu, như INSERT, UPDATE, DELETE…). Khi một transaction bao gồm nhiều lệnh cập nhật, nó đảm bảo tất cả các ...

Transactions trong Rails

Transaction (giao dịch) được dùng để đảm bảo tính toàn vẹn dữ liệu khi xảy ra cập nhật (cập nhật xin được hiểu theo nghĩa rộng là các hành động sửa đổi dữ liệu, như INSERT, UPDATE, DELETE…). Khi một transaction bao gồm nhiều lệnh cập nhật, nó đảm bảo tất cả các cập nhật đều được thực hiện thành công, hoặc trong trường hợp một lệnh gặp sự cố toàn bộ transaction bị hủy bỏ. Khi đó dữ liệu trở về trạng thái như trước khi xảy ra transaction. Nói cách khác transaction ngăn chặn tình huống dữ liệu được cập nhật nửa chừng, trong đó một phần được cập nhật còn một phần bị bỏ qua

I. Lý do thực hiện transactions

Chúng ta sử dụng transactions như là một wrapper bảo vệ các câu lệnh SQL để đảm bảo những thay đổi trong cơ sở dữ liệu chỉ xảy ra khi tất cả các hành động thành công với nhau. Transactions giúp các lập trình thực thi toàn vẹn dữ liệu trong ứng dụng. Ví dụ kinh điển của giao dịch là phương pháp ngân hàng nơi tiền được rút từ một tài khoản và gửi vào tiếp theo. Nếu một trong các bước sau thất bại, sau đó toàn bộ quá trình sẽ thiết lập lại. Ví dụ này có thể được mô tả trong code theo cách này:

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

Trong Rails transaction có sẵn phương thức instance hay phương thức của lớp cho tất cả các ActiveRecord. Điều này có nghĩa là một trong hai cách trên đều giá trị như nhau:

Client.transaction do
  @client.users.create!
  @user.clients(true).first.destroy!
  Product.first.destroy!
end

@client.transaction do
  @client.users.create!
  @user.clients(true).first.destroy!
  Product.first.destroy!
end

Bạn cũng có thể nhận thấy rằng có những lớp mô hình khác nhau được tham chiếu trong transactions này. Nó là hoàn hảo để kết hợp các loại mô hình bên trong một khối giao dịch duy nhất. Điều này là do transactions được liên kết với các kết nối cơ sở dữ liệu không phải là mô hình ví dụ. Theo quy định, transactions chỉ cần thiết khi thay đổi cho nhiều bản ghi phải thành công như một đơn vị. Ngoài ra, Rails đã kết gói gọn các phương pháp #save và #destroy trong một giao dịch, do đó một transaction là không bao giờ cần thiết khi cập nhật một bản ghi duy nhất.

II. Transaction Rollback Triggers

Transactions thiết lập lại tình trạng của các bản ghi thông qua một quá trình được gọi là một rollback. Trong Rails, rollbacks chỉ được kích hoạt bởi một ngoại lệ. Đây là một điểm rất quan trọng để hiểu được; Tôi thấy một số khối giao dịch đó sẽ không bao giờ rollback vì mã có chứa không thể ném ra một ngoại lệ. Ở đây chúng ta có một chút biến đổi ví dụ ngân hàng:

ActiveRecord::Base.transaction do
  david.update_attribute(:amount, david.amount -100)
  mary.update_attribute(:amount, 100)
end

Trong Rails #update_attribute được thiết kế không phải để ném một ngoại lệ khi một bản ghi cập nhật lỗi. Nó trả về false, và vì lý do này, bạn nên đảm bảo rằng các phương pháp sử dụng ném một ngoại lệ khi thất bại. Một cách tốt hơn để viết các ví dụ trên sẽ là:

ActiveRecord::Base.transaction do
  david.update_attributes!(:amount => -100)
  mary.update_attributes!(:amount => 100)
end

Lưu ý: (!) là một quy ước trong rails cho một phương pháp mà sẽ ném một ngoại lệ khi thất bại.

Chúng ta cũng thấy các ví dụ trong đoạn code nơi mà các bản ghi đã được tìm thấy bên trong transactions sử dụng tìm kiếm bản ghi bằng #findby. Theo thiết hàm tìm kiếm của rails sẽ trả về nil khi không có bản ghi được trả về. Điều này trái ngược với phương pháp #find bình thường mà ném một ngoại lệ ActiveRecord :: RecordNotFound. Hãy xem xét ví dụ này:

ActiveRecord::Base.transaction do
  david = User.find_by_name("david")
  if(david.id != john.id)
    john.update_attributes!(:amount => -100)
    mary.update_attributes!(:amount => 100)
  end
end

Bạn có thấy lỗi logic? Các đối tượng rống sẽ có một thuộc tính id và nó sẽ che dấu một thực tế rằng các bản ghi mong muốn không được trả lại. Trong khi điều này không gây ra một lỗi trong transaction vẫn sẽ dẫn đến mất tính toàn vẹn dữ liệu, bởi vì các ứng dụng được cập nhật mà nó không nên. Nhớ rằng một transaction định nghĩa một khối mã đó phải thành công như là một đơn vị nguyên tử. Điều này có nghĩa chúng ta phải bắn ra một ngoại lệ khi một phụ thuộc logic như thế này không được đáp ứng. Dưới đây là cách nên viết:

ActiveRecord::Base.transaction do
  david = User.find_by_name("david")
  raise ActiveRecord::RecordNotFound if david.nil?
  if(david.id != john.id)
    john.update_attributes!(:amount => -100)
    mary.update_attributes!(:amount => 100)
  end
end

Khi ngoại lệ xảy ra nó sẽ được đưa ra bên ngoài của khối transaction sau khi rollback đã hoàn thành. Đoạn code trong ứng dụng của bạn phải sẵn sàng để bắt ngoại lệ này ngay khi nó được nổi lên qua stack của ứng dụng.

Nó cũng có thể làm mất hiệu lực một transaction mà không cần bắn ra một ngoại lệ đó sẽ truyền lên bằng cách sử dụng ActiveRecord :: Rollback. Ngoại lệ đặc biệt này cho phép bạn làm mất hiệu lực một transaction và thiết lập lại các bản ghi cơ sở dữ liệu mà không cần phải xử lý những nơi khác trong mã của bạn.

III. Khi nào nên sử dụng Nested Transactions

Một trong những sai lầm phổ biến thường thấy trong codebase là sử dụng sai hoặc lạm dụng các giao dịch lồng nhau. Khi bạn lồng một transaction bên trong một transaction khác, điều này có ảnh hưởng tới transactions con. Điều này có thể có những kết quả đáng ngạc nhiên, đưa ví dụ này từ tài liệu API Rails:

User.transaction do
  User.create(:username => 'Kotori')
  User.transaction do
    User.create(:username => 'Nemu')
    raise ActiveRecord::Rollback
  end
end

Như đã đề cập trước đó ActiveRecord :: Rollback không đưa ra bên ngoài của khối transaction và vì vậy các transaction cha không nhận được ngoại lệ lồng vào bên trong các transaction con. Từ các nội dung của transaction con được gộp vào các giao dịch cha cùng khi các bản ghi được tạo ra! Chúng ta cảm thấy nó dễ dàng hơn để nghĩ về các giao dịch lồng nhau.

Để đảm bảo một rollback nhận được transactions cha, bạn phải thêm: requires_new => true. tùy chọn để transaction con. Một lần nữa bằng cách sử dụng ví dụ từ nguồn Rails bạn sẽ kích hoạt Rollback của cha như sau:

User.transaction do
  User.create(:username => 'Kotori')
  User.transaction(:requires_new => true) do
    User.create(:username => 'Nemu')
    raise ActiveRecord::Rollback
  end
end

Một transaction này chỉ hoạt động khi kết nối cơ sở dữ liệu hiện hành. Nếu ứng dụng của bạn viết cho nhiều cơ sở dữ liệu cùng một lúc bạn sẽ cần để bọc các phương pháp bên trong một giao dịch lồng nhau. Ví dụ:

Client.transaction do
  Product.transaction do
    product.buy(@quantity)
    client.update_attributes!(:sales_count => @sales_count + 1)
  end
end

IV. Callback trong Transactions

Như đã đề cập trước đó #save và #destroy được gói bên trong một transaction. Điều này có nghĩa rằng callbacks như #after_save vẫn là một phần của transaction hoạt động mà vẫn có thể được rollback! Vì vậy nếu bạn muốn code để thực thi được đảm bảo để thực hiện bên ngoài của transaction sử dụng một trong các callbacks transaction cụ thể như #after_commit hoặc #after_rollback.

V. Transaction Gotchas

Không bắt một ngoại lệ ActiveRecord :: RecordInvalid bên trong một transaction vì các ngoại lệ này làm mất hiệu lực của transaction trên một số cơ sở dữ liệu như Postgres. Một khi các transaction không hợp lệ, bạn phải khởi động lại nó từ đầu để nó hoạt động một cách chính xác.

Khi kiểm tra rollbacks hoặc callbacks transaction liên quan xảy ra sau rollback bạn sẽ muốn tắt transactional_fixtures mà là mặc định trong hầu hết các test framework.

VI. Một số partterns nên tránh khi viết ứng dụng

Sử dụng transaction khi chỉ có 1 bản ghi được cập nhật Những nested transaction không cần thiết Transaction chứa đoạn code không gây ra một rollback Sử dụng transaction trong 1 controller

Bài viết được tham khảo tại: http://markdaggett.com/blog/2011/12/01/transactions-in-rails/

0