Ruby on Rails ActiveRecord Areas and Class Methods

Rails internally converts scope to class methods, so why can't we use class methods themselves instead of going with scopes.

+6
source share
3 answers

From the excellent guide :

14 Areas
[...]
To define a simple scope, we use the scope method inside the class, passing the request that we would like to run when this scope is called:

 class Article < ActiveRecord::Base scope :published, -> { where(published: true) } end 

This is exactly the same as defining a class method, and which you use is a personal preference:

 class Article < ActiveRecord::Base def self.published where(published: true) end end 

Please note in particular:

This is exactly the same as defining a class method, and which you use is a personal preference

And a little further (the Rails3 manual says the same thing here BTW):

14.1 Passing Arguments
[...]
Using a class method is the preferred way to accept arguments for areas.

So, you are using this preference, and it is even recommended that you use class methods for areas that take arguments.

Using scope is basically a notation. If you say scope :whatever , then you explicitly say that whatever designed to build queries; if you say def self.whatever you mean nothing about the intent of the whatever method, you simply define a class method that may or may not behave like a scope.

Of course, 14.1 makes a mess of this distinguishing difference, recommending not to use scope when your region accepts arguments. Also keep in mind that in Rails3 you can say:

 scope :published, where(published: true) 

so the undisputed area was visually β€œclean” and concise, but adding a lambda to handle the arguments would make it look more messy:

 scope :pancakes, ->(x) { where(things: x) } 

But Rails4 wants lambdas even for contactless areas, the difference now makes even less sense.

I suspect the difference is historical. Scomens were probably something special in previous times, but became the usual old class methods in the Rails3 era to reduce duplication and improve the grid with the new query interface that came with Rails3.


So you can skip scope and go to class methods if you want. You are even encouraged to do this when the scope accepts arguments.

+3
source

Scopes are just class methods. Internal Active Record converts a region to a class method.

"There is no difference between them" or "it is a matter of taste." I tend to agree with both suggestions, but I like to show some minor differences that exist between them. This blog post very well explains the difference.

0
source

Why should I use scope if it's just syntactic sugar for a class method? "So, here are some interesting examples for you.

Areas are always chain => β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’ We use the following scenario: users will be able to filter messages by status, ordering the most recent ones. Simple enough, let me write for this:

  class Post < ActiveRecord::Base scope :by_status, -> status { where(status: status) } scope :recent, -> { order("posts.updated_at DESC") } end And we can call them freely like this: Post.by_status('published').recent # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' # ORDER BY posts.updated_at DESC Or with a user provided param: Post.by_status(params[:status]).recent # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' # ORDER BY posts.updated_at DESC So far, so good. Now lets move them to class methods, just for the sake of comparing: class Post < ActiveRecord::Base def self.by_status(status) where(status: status) end def self.recent order("posts.updated_at DESC") end end 

Besides using a few extra lines, no big improvements. But now, what happens if the status parameter is zero or empty?

  Post.by_status(nil).recent # SELECT "posts".* FROM "posts" WHERE "posts"."status" IS NULL # ORDER BY posts.updated_at DESC Post.by_status('').recent # SELECT "posts".* FROM "posts" WHERE "posts"."status" = '' # ORDER BY posts.updated_at DESC Oooops, I don't think we wanted to allow these queries, did we? With scopes, we can easily fix that by adding a presence condition to our scope: scope :by_status, -> status { where(status: status) if status.present? } There we go: Post.by_status(nil).recent # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC Post.by_status('').recent # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC Awesome. Now lets try to do the same with our beloved class method: class Post < ActiveRecord::Base def self.by_status(status) where(status: status) if status.present? end end Running this: Post.by_status('').recent NoMethodError: undefined method `recent' for nil:NilClass And . The difference is that a scope will always return a relation, whereas our simple class method implementation will not. The class method should look like this instead: def self.by_status(status) if status.present? where(status: status) else all end end 

Note that I am returning everything for the nil / blank case, which in Rails 4 returns a relation (it previously returned Array of elements from the database). In Rails 3.2.x, you should use a scope instead. And here we go:

  Post.by_status('').recent # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC 

So, the advice here is: never return zero from a class method that should work as a scope, otherwise you will violate the integrity condition implied by scopes that always return a relation.

Areas are expandable => β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’ Let's consider pagination as our next example, and I'm going to use stone mantels in as a basis. The most important thing you need to do when paginating the collection is to specify which page you want to extract:

  Post.page(2) After doing that you might want to say how many records per page you want: Post.page(2).per(15) And you may to know the total number of pages, or whether you are in the first or last page: posts = Post.page(2) posts.total_pages # => 2 posts.first_page? # => false posts.last_page? # => true 

It all makes sense when we call things in that order, but it makes no sense to call these methods in a collection that is not paginated, right? When you write areas, you can add specific extensions that will only be available in your object if that area is called. In the case of kaminari, it only adds the page area to your Active Record models and relies on the scope extension function to add all the other functions when the page is called. In theory, the code would look like this:

  scope :page, -> num { # some limit + offset logic here for pagination } do def per(num) # more logic here end def total_pages # some more here end def first_page? # and a bit more end def last_page? # and so on end end 

Scope extensions are a powerful and flexible technique for our tool chain. But, of course, we can always figure it out and get it all using class methods:

  def self.page(num) scope = # some limit + offset logic here for pagination scope.extend PaginationExtensions scope end module PaginationExtensions def per(num) # more logic here end def total_pages # some more here end def first_page? # and a bit more end def last_page? # and so on end end 

This is a little more verbose than using the area, but gives the same results. And the tip here: choose what works best for you, but make sure you know what the infrastructure provides before you reinvent the wheel.

-4
source

All Articles