[Scope] Một vài chia sẻ về scope trong Ruby on Rails
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, ...
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 EVIL
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!