Rails: Dynamically Chain Scopes
Bài đăng này đã không được cập nhật trong 8 năm
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
All rights reserved