Active Record scopes

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