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