[Scope] Một vài chia sẻ về scope trong Ruby on Rails
Bài đăng này đã không được cập nhật trong 6 năm
Scope là gì? Nó dùng để làm gì? Định nghĩa nó như thế nào?
Đầu tiên scope bản chất là class method, scope dùng để tạo ra các class method khác dùng để truy xuất dữ liệu.
Vậy, định nghĩa một scope như thế nào?
Một ví dụ:
class Product < ActiveRecord::Base
scope :latest_product, ->{order(created_at: :desc).limit 3}
end
Cách gọi một scope như 1 class method:
Product.latest_product
# SELECT `products`.* FROM `products` ORDER BY `products`.`created_at` DESC LIMIT 3
Như các bạn thấy trong ví dụ trên một scope gồm 3 phần:
1. scope keyword
2. tên method
3. 1 block
Để hiểu chi tiết hơn các bạn có thể xem ở đây
Scope và class method
Từ ví dụ trên, ta có thể thấy scope là class method. Vậy thì scope khác gì class method thông thường?
1. scope gọi liên tiếp được(chainable)
Ví dụ:
class User < ActiveRecord::Base
scope :scope_id, ->(id){where(id: id)}
scope :scope_email, ->{where(email: "abc@gmail.com")}
class << self
def class_method_id(id)
where(id: id)
end
def class_method_email
where(email: "abc@gmail.com")
end
end
end
Ta cùng thử nghiệm tính chainable của scope và class method:
# scope
User.scope_id(1).scope_email
# SELECT 1 AS one FROM `users` WHERE `users`.`id` = 1 AND `users`.`email` = 'abc@gmail.com' LIMIT 1
User.scope_id(1).class_method_email
# SELECT 1 AS one FROM `users` WHERE `users`.`id` = 1 AND `users`.`email` = 'abc@gmail.com' LIMIT 1
----------------------------------------------------------------------------------------------------
# class method
User.class_method_id(1).class_method_email
# SELECT 1 AS one FROM `users` WHERE `users`.`id` = 1 AND `users`.`email` = 'abc@gmail.com' LIMIT 1
User.class_method_id(1).scope_email
# SELECT 1 AS one FROM `users` WHERE `users`.`id` = 1 AND `users`.`email` = 'abc@gmail.com' LIMIT 1
Hmm, ta có thể thấy cả scope và class method đều ra kết quả giống nhau. Vậy ta thử vs input đặc biệt như nil/blank xem sao:
# scope
User.scope_id("").scope_email
# SELECT 1 AS one FROM `users` WHERE `users`.`id` IS NULL AND `users`.`email` = 'abc@gmail.com' LIMIT 1
User.scope_id(nil).scope_email
# SELECT 1 AS one FROM `users` WHERE `users`.`id` IS NULL AND `users`.`email` = 'abc@gmail.com' LIMIT 1
--------------------------------------------------------------------------------------------------------
#class method
User.class_method_id("").class_method_email
# SELECT 1 AS one FROM `users` WHERE `users`.`id` IS NULL AND `users`.`email` = 'abc@gmail.com' LIMIT 1
User.class_method_id(nil).class_method_email
# SELECT 1 AS one FROM `users` WHERE `users`.`id` IS NULL AND `users`.`email` = 'abc@gmail.com' LIMIT 1
Mọi thứ vẫn chạy tốt .... Thử thay đổi một chút xem sao:
classs User < ActiveRecord::Base
scope :scope_id, ->(id){where(id: id).first}
def self.class_method_id(id)
where(id: id).first
end
end
Và cho input là nil/blank:
# scope
User.scope_id(nil)
# SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL ORDER BY `users`.`id` ASC LIMIT 1
# SELECT `users`.* FROM `users`
=> all User
---------------------------------------------------------------------------------------------
# class method
User.class_method_id(nil)
# SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL ORDER BY `users`.`id` ASC LIMIT 1
=> nil
Ta đã thấy sự khác biệt, vậy sự khác biệt này từ đâu mà có?
Thử đọc lại doc xem sao. Có thể thấy trong doc trong hàm định nghĩa scope có rất nhiều if .. else condition. Vậy nếu ta bắt exception trong class method thì có thể làm cho nó có tính năng chainable được không:
class User < ActiveRecord::Base
def self.class_method_id(id)
return if where(id: id).first
all
end
end
Và kết quả:
User.class_method_id(nil)
# SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL ORDER BY `users`.`id` ASC LIMIT 1
# SELECT `users`.* FROM `users`
2. Scope mở rộng được(extensible)
Ví dụ:
class User < ActiveRecord::Base
scope :scope_id, ->{where(id: 1)} do
def print_text
"say oh yeah"
end
end
def self.class_method_id
where(id: 1) do
def print_text
"I believe I can fly"
end
end
end
end
# scope
User.scope_id.print_text
=> "say oh yeah"
---------------------------
# class method
User.class_method_id.print_text
=> NoMethodError
User.print_text
=> NoMethodError
Chúng ta có thể thấy khi viết scope, chúng ta có thể thêm các thành phần mở rộng bên trong scope và những thành phần mở rộng này chỉ có tác dụng với object nếu như scope được gọi.
Chúng ta cũng có thể làm tương tự với class method bằng việc sử dụng module:
class User < ActiveRecord::Base
def self.class_method_id
where(id: 1).extend Pritable
end
module Printable
def print_text
"I believe I can fly"
end
end
end
User.class_method_id.print_text
=> "I believe I can fly"
Một vài điều cần lưu ý khi sử dụng scope
1. default_scope is DEVIL
Không nên sử dụng default_scope vì:
default_scope không thể bị ghi đè
Nếu định nghĩa default_scope thì mọi câu query đều phải chạy qua default_scope từ đó trả về kết quả không như mong muốn:
class User < ActiveRecord::Base
default_scope {where(admin: true)}
scope :sort_by_created, ->{order(created_at: :desc)}
end
User.sort_by_created
# SELECT `users`.* FROM `users` WHERE `users`.`admin` = TRUE ORDER BY `users`.`created_at` DESC
Khi gọi scope :sort_by_created câu query trả về tự động thêm điều kiện của default_scope. Điều này rất nguy hiểm khi nhiều người làm chung một dự án mà không rõ được hành vi của code.
default_scope còn ảnh hưởng đến khi khởi tạo instance của một model
Vẫn với ví dụ trên:
User.new
#<User:0x00005625eecc9f10> {
:address => nil,
:admin => true,
:created_at => nil,
:email => nil,
:id => nil,
:name => nil,
:password_digest => nil,
:updated_at => nil
}
Ta có thể thấy rằng default_scope tự động thêm giá trị vào trường admin khi khởi tạo instance.
2. Vậy khi nào thì chúng ta nên sử dụng scope?
Theo quan điểm của tôi chúng ta dùng scope khi:
- Sử dụng một câu query nhiều lần
- Khi logic không quá phức tạp
- Khi code cần đến khả năng chainable hoặc extensible
Kết luận
Trên đây là một vài chia sẻ của tôi về scope trong khi học tập về Rails. Tùy vào mục đích để các bạn có thể sử dụng scope hoặc class method sao cho hợp lí và dễ maintain code nhất có thể.
Thank for reading and happy coding!
All rights reserved