037. Refactor where in Rails controller

Thach
Written by Thach on
037. Refactor where in Rails controller

Một tips nhỏ để refactor khi sử dụng nhiều lệnh where trong controller.

1. Fresher code

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    users = User.where(nil)
    users = users.where(role: params[:role]) if params[:role]
    users = users.where(status: params[:status]) if params[:status]
    users = users.where(public_id: params[:public]) if params[:public]
    render json: users, status: 200
  end
end

# app/controllers/companies_controller.rb
class CompaniesController < ApplicationController
  def index
    companies = Company.where(nil)
    companies = companies.where("name like ?", "#{params[:name]}%") if params[:name]
    companies = companies.where(tax_id: params[:tax]) if params[:tax]
    render json: companies, status: 200
  end
end

Code này hoàn toàn ổn, chạy tốt, vì chúng ta đều biết Arel trong Rails sẽ giúp chúng ta tạo ra một câu query duy nhất. Tôi dùng cách này suốt, nhưng sau khi đúng là việc cảm thấy viết nhiều câu lệnh where theo params nhìn nó cứ ngu ngu thế nào ấy.

2. Scope + Metaprogramming

# app/models/user.rb
class User < ApplicationRecord
  scope :role,   -> (role) { where(role: role) }
  scope :status, -> (status) { where(status: status) }
  scope :public, -> (public_id) { where(public_id: public_id) }
end

# app/models/company.rb
class Company < ApplicationRecord
  scope :name, -> (name) { where("name like ?", "#{name}%") }
  scope :tax,  -> (tax_id) { where(tax_id: tax_id) }
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    users = User.where(nil)
    users = users.role(params[:role]) if params[:role]
    users = users.status(params[:status]) if params[:status]
    users = users.public(params[:public]) if params[:public]
    render json: users, status: 200
  end
end

# app/controllers/companies_controller.rb
class CompaniesController < ApplicationController
  def index
    companies = Company.where(nil)
    companies = companies.name(params[:name]) if params[:name]
    companies = companies.tax(params[:tax]) if params[:tax]
    render json: companies, status: 200
  end
end

Sau đó áp dụng thêm Metaprogramming, ta có thể rút gọn như sau.

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    users = User.where(nil)
    params.slice(:role, :status, :public).each do |key, value|
      users = users.send(key, value) if value
    end
    render json: users, status: 200
  end
end

# app/controllers/companies_controller.rb
class CompaniesController < ApplicationController
  def index
    companies = Company.where(nil)
    params.slice(:name, :tax).each do |key, value|
      companies = companies.send(key, value) if value
    end
    render json: companies, status: 200
  end
end

3. Module

Với cách này thì ta không cần define scope ở nhiều model nữa, ta chỉ cần define module Filterable trong models/concerns và include nó vào model cần dùng.

# app/models/concerns/filterable.rb
module Filterable
  extend ActiveSupport::Concern

  module ClassMethods
    def filter(filtering_params)
      results = self.where(nil)
      filtering_params.each do |key, value|
        results = results.send(key, value) if value
      end
      results
    end
  end
end
# app/models/user.rb
class User < ApplicationRecord
  include Filterable
  ...
end

# app/models/company.rb
class Company < ApplicationRecord
  include Filterable
  ...
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    users = User.filter(filtering_params(params))
    render json: users, status: 200
  end

  private

  def filtering_params(params)
    params.slice(:role, :status, :public)
  end
end

# app/controllers/companies_controller.rb
class CompaniesController < ApplicationController
  def index
    companies = Company.filter(filtering_params(params))
    render json: companies, status: 200
  end

  private

  def filtering_params(params)
    params.slice(:name, :tax)
  end
end

3.1 Note

params = { destroy: 1 }

User.filter(params)

Nếu không filter params, rất có thể bạn sẽ gặp những params hiểm ác thế này.

Trong Ruby 2.6.0, một phương thức mới được thêm vào các enumerables, đó là filter, nhận vào một block. Bạn có thể kiểm tra mã dưới đây. Tuy nhiên, tôi khuyên bạn nên thay đổi tên phương thức từ filter sang filter_by.

# This will throw error, because ruby use filter for enumerables.
def index
    @companies = Company.includes(:agency).order(Company.sortable(params[:sort]))
    @companies = @companies.filter(params.slice(:ferret, :geo, :status))
end

# This won't throw error, because we call filter for class Company
def index
    @companies = Company.filter(params.slice(:ferret, :geo, :status))
    @companies = @companies.includes(:agency).order(Company.sortable(params[:sort]))
end

Tham khảo hoàn toàn từ K Putra

Comments

comments powered by Disqus