Daily Archives: 09/12/2016

Rails – Callback

Trong quá trình chạy ứng dụng, một đối tượng có thể được tạo ra, được cập nhật hoặc bị hủy thông qua các thao tác CRUD. Rails cung cấp cơ chế callback để chúng ta có thể kiểm soát trạng thái của các đối tượng này.

Callback là các phương thức/hàm được gọi trước hoặc sau khi có sự thay đổi trạng thái (như tạo, lưu, xóa, cập nhật, validate…) của đối tượng.

Ví dụ

Chúng ta sẽ không cho thực hiện chức năng xóa user nếu trong bảng chỉ còn lại một user.

Đầu tiên chúng ta sửa lại file layout một tí như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title> 
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart"> 
                <%= hide_cart_if(current_cart.line_items.empty?, :id => "cart") do %>
                    <%= render current_cart %> 
                <% end %>
            </div>
 
            <a href="#">Home</a><br />
            <a href="#">FAQ</a><br />
            <a href="#">News</a><br />
            <a href="#">Contact</a><br />
 
            <% if session[:user_id] %>
                <br/>
                <%= link_to 'Orders', '/orders' %><br />
                <%= link_to 'Products', '/products' %><br />
                <%= link_to 'Users', '/users' %><br />
                <%= button_to 'Logout', '/logout', :method => :delete %>
                <br/>
            <% else %>
                <br/>
                <%= link_to 'Log In', login_path %><br />
            <% end %>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Chúng ta tạo thêm mấy nút bấm dẫn đến trang /products, /users, /orders, /logout nếu người dùng đã đăng nhập, nếu chưa thì hiển thị nút dẫn đến trang /login.

Tiếp theo chúng ta sửa lại lớp User như sau:

require 'digest/sha2'

class User < ActiveRecord::Base
    validates :name, :presence => true, :uniqueness => true
    validates :password, :confirmation => true
    attr_accessor :password_confirmation
    attr_reader :password 
 
    validate :password_must_be_present
 
    def User.encrypt_password(password, salt) 
        Digest::SHA2.hexdigest(password + salt)
    end
 
    def password=(password) 
        @password = password
 
        if password.present?
            generate_salt
            self.hashed_password = self.class.encrypt_password(password, salt)
        end
    end
 
    def User.authenticate(name, password) 
        if user = find_by_name(name) 
            puts encrypt_password(password, user.salt)
            if user.hashed_password == encrypt_password(password, user.salt) 
                user
            end
        end
    end
 
    after_destroy :check_user_empty
 
    def check_user_empty
        if User.count.zero?
            raise "Can't delete last user"
        end
    end
 
  private
    def password_must_be_present 
        if hashed_password.present? == false
            errors.add(:password, "Missing password")
        end
    end
 
    def generate_salt
        self.salt = self.object_id.to_s + rand.to_s
    end
end

Chúng ta định nghĩa phương thức check_user_empty, phương thức này kiểm tra xem trong bảng User có rỗng hay không, nếu rỗng thì giải phóng một lỗi exception.

Sau đó ở trên chúng ta gọi phương thức after_destroy :check_user_empty. Phương thức after_destroy là một phương thức callback, phương thức này sẽ gọi phương thức :check_user_empty mỗi khi có một thao tác nào đó liên quan đến câu lệnh DELETE trong cơ sở dữ liệu xảy ra. Tức là ở đây nếu người dùng bấm nút ‘Destroy’ để xóa user thì phương thức check_user_exist sẽ được gọi, và nếu bảng users trong CSDL không còn bản ghi nào thì một lỗi exception sẽ được sinh ra.

Và nếu sau lời gọi hàm callback mà có lỗi exception nào đó thì lỗi này sẽ được gửi ngược về nơi đã gọi ra nó, tức là ở đây tương ứng với lời gọi @user.destroy trong phương thức destroy của lớp UsersController. Ngoài ra lỗi exception cũng sẽ bắt buộc Rails phải “đảo ngược” câu truy vấn vừa thực hiện, tức là nếu xóa user mà có lỗi exception đó thì user đó sẽ được phục hồi nguyên vẹn trong CSDL.

Bây giờ chúng ta sửa lại phương thức destroy trong lớp UsersController để bắt exception như sau:

class UsersController < ApplicationController
    .
    .
    .
    # DELETE /users/1
    # DELETE /users/1.json
    def destroy 
        begin
            @user.destroy
            flash[:notice] = "User #{@user.name} deleted"
        rescue Exception => e
            flash[:notice] = e.message
        end
        respond_to do |format|
            format.html { redirect_to users_url }
            format.json { head :no_content }
        end
    end
    .
    .
    .
end

Nếu có lỗi exception xảy ra thì chúng ta chỉ đơn giản là thêm câu thông báo vào biến flash.

Nếu bạn muốn Rails chỉ thực hiện phục hồi dữ liệu chứ không muốn tạo một đối tượng exception nào thì trong lớp User, chúng ta cho giải phóng một đối tượng ActiveRecord::Rollback là được.

Ngoài ra còn có rất nhiều phương thức callback khác như:

CREATE

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create
  • around_create
  • after_create
  • after_save
  • after_commit/after_rollback

UPDATE

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_update
  • around_update
  • after_update
  • after_save
  • after_commit/after_rollback

DELETE

  • before_destroy
  • around_destroy
  • after_destroy
  • after_commit/after_rollback

Bạn có thể tìm hiểu thêm tại đây.