12/08/2018, 13:55

Rails: Dynamically Chain Scopes

Tôi đoán rằng đã rất nhiều lần các bạn phải viết một rails app với hàng đống các logic để viết ra được một scope sql query, ví dụ như xây dưng chuỗi sql thông qua các câu lệnh if else hoặc case when thế này: sql = "active= 1" if condition sql + = "and important=1" end if ...

Tôi đoán rằng đã rất nhiều lần các bạn phải viết một rails app với hàng đống các logic để viết ra được một scope sql query, ví dụ như xây dưng chuỗi sql thông qua các câu lệnh if else hoặc case when thế này:

sql = "active= 1"
if condition
  sql += "and important=1"
end
 if second_condition
  sql += "and author IS NOT NULL"
end
Article.where sql

Hoặc lặp lại một scope để chain trong các điều kiện if else:

if condition
  Article.active.important
end
if second_condition
  Article.active.with_author
end

Với việc viết kiểu này thì nếu logic yêu cầu nhiều điều kiện hơn thi các bạn sẽ phải thêm nhiều code hơn vào điều kiện trên, từ đó dẫn đến việc code dài dòng, khó debug và đọc hiểu và sẽ khó mà thêm được các logic, điều kiện mới vào trong app nữa.

Với bài viết này, tôi muốn hưỡng dẫn các bạn một kỹ thuật chain scope động sử dụng hàm senđ của Ruby để làm đơn giản hóa code của mình.

Hàm send của Ruby giúp các bạn có thể gọi một phương thức bất kỳ từ một object nào đó bằng string hoặc symbol thay vị gọi theo kiểu object.method_name. Ví dụ:

Article.send "active"

# tương đương với

Article.active

Để các bạn có thể thực hành luôn, ta sẽ xây dựng một ứng dụng test:

rails new blog

Khởi tạo một scaffold:

rails generate scaffold Article title:string description:text status:integer author:string website:string meta_title:string meta_description:text

Migrate database:

rake db:migrate

Sử dụng gem Faker để tạo dữ liệu:

10.times do
  Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 0)
  Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 1)
  Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 2)
  Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 3)
end

10.times do
 Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 0,:website => Faker::Internet.domain_name)
 Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 1,:author => Faker::Name.first_name)
 Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 2,:meta_title => Faker::Lorem.word)
 Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 3,:meta_description => Faker::Lorem.sentence)
end

Sau đó chạy seed:

rake db:seed

Bây giờ đã có dữ liệu, ta có thể viết các scope cần để truy vấn trong model:

class Article < ActiveRecord::Base
  enum status: [ :draft, :pending_review,:flagged, :published]

  scope :with_author, -> {(where("`author` IS NOT NULL ")) }
  scope :with_website, -> {(where("`website` IS NOT NULL ")) }
  scope :with_meta_title, -> {(where("`meta_title` IS NOT NULL ")) }
  scope :with_meta_description, -> {(where("`meta_description` IS NOT NULL")) }
end

Khi các bạn gọi scope mà được xây dựng từ where thì nó luôn trả ra instace của ActiveRecord::Relation, từ đó giúp bạn có thể gọi được phương thức where mới hoặc gọi scope mà return where. Ví dụ:

Article.with_author.with_website

Kết hợp với tính năng send của Ruby ta có thể viết một phương thức giúp chain một mảng các scope được truyền vào như sau:

class Article
  #...

  class << self
    def send_chain methods
      methods.inject self, :send
    end
  end
end

Ta có thể thực hiện việc chain scope như sau:

Article.send_chain [:with_author, :with_website]

# tương đương

Article.with_author.with_website

Hàm inject ở đây sẽ duyệt các phần tử trong methods và mỗi vòng lặp sẽ trả về giá trị từ việc gọi method send trên object khỏi tạo là self tương đương với Article và tham số truyền vào là phần tử hiện thời của methods.

Article.send_chain [:with_author, :with_website] == Article.send(:with_author).send(with_website)

Với kiểu viết này, nhiệm vụ của bạn là xây dựng mảng scope cần chain. Việc này có thể được thực hiện ở phía client (HTML, JS hoặc mobile app) và tứ đó có thể giúp controller nhỏ gọn hơn.

Giải pháp trên mới chỉ giải quyết được bài toán là scope không nhận thêm params từ ngoài. Nếu như scope có params từ ngoài thì viết thế nào ? Các bạn có thể thêm một class method như sau:

class Article
  #...

  class << self
    #...
    # methods sẽ có dạng hash: {name: method_name, params: [các tham số cần truyền]}
    def send_chain_params methods
      methods.inject do |relation, method|
        relation.send method[:name], method[:params]
      end
    end
  end
end

Công việc của bạn bây giờ là build một hash phù hợp với method trên. Việc này có thể sẽ khá phức tạ, tuy nhiên các bạn có thể tạo riêng một object chuyển để xây dựng hash này và dùng nó ở các controller cần thiết.

Hy vọng kỹ thuật này sẽ giúp rails app của các bạn gọn và đẹp hơn.

Reference

Bài viết tham khảo từ Rails: Dynamically Chain Scopes to Clean up SQL Queries

0