Active Record scopes
Bài đăng này đã không được cập nhật trong 8 năm
Scope được dùng khá phổ biến trong Rails. Scope khá giống với class methods khiến nhiều bạn nhầm lẫn, vậy scope là gì và sử dụng như thế nào cho đúng?
Scope là một phần được support bởi Active Record. Scope thường định nghĩa các query dùng chung và có thể gọi từ association objects hoặc model. Về bản chất thì scope là class method được định nghĩa dưới dạng động thông qua một method tên là scope
dùng để định nghĩa ra các scope khác nhau. Thông qua một vài ví dụ dưới đây hi vọng bạn có thể hiểu rõ hơn về scope.
Để làm việc với scope đầu tiên bạn cần khởi tạo Active Record model. Dưới đây tôi sẽ tạo một model đơn giản là Post với một trường là status.
rails g model post status:string
rake db:migrate
Chúng ta sẽ thử thêm một scope có tên là draft vào trong model này. Bạn có thể tìm hiểu thêm về cách dùng scope ở đây active_record_querying#scope, cũng khá đơn giản
class Post < ActiveRecord::Base
scope :draft, -> { where(status: 'draft') }
end
Chạy thử
pry(main)> Post.draft
Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`status` = 'draf
Nhìn có vẻ rất bí ẩn, chỉ thêm một dòng ngắn gọn scope :draft, -> { where(status: 'draft') }
là ta có thể dùng Post.draft
để lấy ra các post ở trạng thái draft một cách nhanh chóng. Thực ra cũng khá đơn giản, Active Record cung cấp một class method có tên là scope
nhận 2 parameters. Parameter thứ nhất là tên của scope bạn muốn tạo, parameter thứ 2 là một lambda function đây chính là phần thân của scope (phần thực thi). Vậy class method tên là scope
kia làm những gì? bạn có thể hiểu đơn giản là nó tạo ra một class method có tên giống với parameter thứ nhất và kết quả trả về có vẻ giống với kết quả của lambda function (parameter thứ 2). Như vậy scope trên có tương đương với class method này không?
class Post < ActiveRecord::Base
def self.draft
where(status: 'draft')
end
end
Câu trả lời là kết quả của scope draft
và class method draft
là tường đương. Tuy trong trường hợp này là tương đương, nhưng trường hợp khác thì sao? chúng ta cùng xem một ví dụ thú vị sau.
class Post < ActiveRecord::Base
scope :draft, -> { where(status: 'draft') }
scope :empty_scope, -> {}
end
Chạy thử
pry(main)> Post.empty_scope
Post Load (0.3ms) SELECT `posts`.* FROM `posts`
Thật ngạc nhiên, hẳn là bạn đang nghĩ Post.empty_scope
đáng ra phải trả về kết quả nil chứ nhỉ sao lại trả về kết quả giống câu lệnh Post.all
thế. Thử thêm lần nữa
pry(main)> Post.draft.empty_scope
Post Load (0.2ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`status` = 'draft'
Ồ cũng không phải Product.empty_scope
luôn trả về kết quả giống với Product.all
. Như vậy, nếu kết quả của lambda function (ở đây lambda function là -> {}
) là nil hoặc false thì scope này sẽ được coi như scope all
. Vậy Post.draft.empty_scope
sẽ có kết quả giống với Post.draft.all
. Để tránh những lỗi kiểu như thế này nên khi viết scope bạn luôn phải trả về là một ActiveRecord::Relation
, việc trong scope trả về kết quả không phải ActiveRecord::Relation
là tuyệt đối nghiêm cấm. Điều này là vô cùng quan trọng khi sử dụng scope và đây cũng là điểm khác biệt của scope so với class method thông thường. Vói class method thì bạn có thể trả về bất cứ giá trị nào cũng đụng còn scope thì không bắt buộc phải là ActiveRecord::Relation
!
Dưới đây là một ví dụ cho thấy sự sai lầm khi trả về kết quả không phải ActiveRecord::Relation
trong scope, điều này có thể gây ra những lỗi khá nguy hại cho hệ thống bạn cần phải chú ý
class Post < ActiveRecord::Base
scope :draft, -> { where(status: 'draft') }
scope :empty_scope, -> {}
scope :first_draft, -> {where(status: 'draft').first}
end
Nhìn vào nội dung của scope first_draft
ta thấy scope này có vẻ đúng đấy, nó lấy ra phần tử đầu tiên được published. Nhìn kĩ lại một chút bạn sẽ thấy scope này vi phạm quy tắc là phải, nó không trả về kết quả là ActiveRecord::Relation
. Có thể bạn đang nghĩ tại sao lại phải trả về ActiveRecord::Relation
. Bởi vì nếu scope không trả về ActiveRecord::Relation
thì ta sẽ không thể gọi nhiều scope một cách liên tục được (scope chainable). Giờ chúng ta sẽ thử scope first_draft
xem sao
pry(main)> Post.create(status: "draft")
SQL (82.3ms) INSERT INTO `posts` (`status`, `created_at`, `updated_at`) VALUES ('draft', '2016-05-25 17:25:05', '2016-05-25 17:25:05')
pry(main)> Post.first_draft
Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`status` = 'draft' ORDER BY `posts`.`id` ASC LIMIT 1
pry(main)> Post.empty_scope.first_draft
Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`status` = 'draft' ORDER BY `posts`.`id` ASC LIMIT 1
pry(main)> Post.first_draft.empty_scope
NoMethodError: undefined method `first_draft' for #<Class:0x000000073527d8>
pry(main)> Post.destroy_all
SQL (61.5ms) DELETE FROM `posts` WHERE `posts`.`id` = 1
pry(main)> Post.first_draft
Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`status` = 'draft' ORDER BY `posts`.`id` ASC LIMIT 1
Post Load (0.2ms) SELECT `posts`.* FROM `posts`
Bạn có thể thấy khi có bản ghi draft thì Post.first_draft
sẽ lấy ra một bản ghi draft đầu tiên. Nhưng Post.first_draft.empty_scope
lại bị lỗi bởi vì first_draft trả về là một object nên không thể gọi tiếp scope được. Trong trường hợp không có bản ghi draft nào thì Post.first_draft
lại trả về tất cả các bản ghi chứ không phải là không có bản ghi nào, điều này hoàn toàn khác với mong đợi của scope này.
Hi vọng qua bài viết này bạn đã hiểu rõ thêm về scope và sử dụng scope đúng cách!
Cảm ơn đã theo dõi bài viết!
All rights reserved