[Rails] Why using find_by in scope and returning all records instead of nil if there are no records that match the conditions

what's this

As the title suggests, I used find_by in scope when I was writing Rails for business. So, if the conditions are not met, I encountered an event that ** returns all records in the corresponding table instead of nil **, so I will leave the solution and cause as a memorandum.

Precautionary statement

The answer is in the documentation. Active Record Query Interface (https://railsguides.jp/active_record_querying.html) ~~ Search did not find similar articles. Is it because it is written in the document? ~~

Conclusion (solution)

1. 1. ** Use class methods instead of scope. ** **

That way, if none match the criteria, nil will be returned instead of all records.

product.rb


  # bad
  scope :find_by, ->(product_id) {
    find_by(product_id: product_id)
  } #=> ActiveRecord::Relation

  # good
  def self.find_by(product_id)
    find_by(product_id: product_id)
  end #=> nil

2. ** Change to find_by! method and throw an exception ** (see digression at the bottom)

Cause

Why does nil return for class methods?

To illustrate that, let's look at the difference between scope and class methods.

What exactly is a scope in the first place?

Rails Guide 14 Scope (https://railsguides.jp/active_record_querying.html#%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97) I will quote from the official.

By setting the scope, you can specify commonly used queries that are referenced as method calls to related objects and models. The scope can use all the methods that have appeared so far, such as where, joins, and includes. Every scope method always returns an ActiveRecord :: Relation object. You can also make other method calls to this object that contain different scopes.

To summarize what I want to say, the scope always returns a ** ActiveRecord :: Relationobject. ** ** Returns aActiveRecord :: Relation object even if the search result is nil`. Absolutely. definitely! !! ~~ I said it twice because it's so important. ~~

So what is ActiveRecord :: Relation?

I have a good article about ActiveRecord :: Relation that is easily organized, so I will quote it. What is ActiveRecord :: Relation?

I will summarize from the above article. If there is a model called Product, the one returned by Product.all or Product.where (name:" hoge ") is an instance of ActiveRecord :: Relation.

Roughly speaking, ActiveRecord :: Relation is the one with multiple records (instances?).

If you do where.class, you can see that Product :: ActiveRecord_Relation is returned. find_by.class returns the Product class itself.

[31] pry(main)> Product.where(id: 1).class
=> Product::ActiveRecord_Relation
[32] pry(main)> Product.find_by(id: 1).class
  Product Load (2.1ms)  SELECT `products`.* FROM `products` WHERE `products`.`id` = 1 LIMIT 1
=> Product(id: integer, name: string)

Difference between class method and scope

The article below was very easy to understand, so let me quote it. Returning nil in ActiveRecord scope ... Class methods work exactly with scope. (Scope is not an instance method. ~~ I misunderstood here. ~~)

The operations performed by the following two are almost the same. The only difference is the return value for nil and User.none.

user.rb


class User < ActiveRecord::Base
  scope :hoge, -> (fuga) { find_by(fuga: fuga) }
end

class User < ActiveRecord::Base
  def self.hoge(fuga)
    find_by(fuga: fuga)
  end
end

After all, why shouldn't it be a scope?

The reason it returns all records in scope is that it always returns a ActiveRecord :: Relation object. ** ** It always returns ActiveRecord :: Relation, so if it is nil, it will automatically return all records.

If you use a class method, nil is returned because it behaves as it should. This is the answer.

Chaban drama

Scope-kun "Yeah, access the DB and narrow down the records ~"

⬇️ SQL execution

Scope-kun "What !? There is no match, nil ... "

Scope-kun "But Wai has decided to return an absolute ActiveRecord :: Relation object ... "

Scope-kun "Hmm, if you return nil, it's a violation of the rules, and it can't be helped, so return all the records! "

I think that something like that is unfolding. Lol (If you execute the same SQL that was executed in scope, it will be displayed as 0 when it is nil.)

Impressions

I didn't know anything about ActiveRecord :: Relation, so when I first encountered this event, I thought," It's a Rails bug !? " ~~ I'm sorry to doubt Rails. It was a crunchy specification. ~~

I think it's a rare case that none of the records match in find_by, but I'd be happy if you could put such an event in the corner of your head.

Alternative: where + take

find_by haswhere (wheres) .limit (1)in terms of internal movement in the first place. You can reproduce find_by by calling the take method after narrowing down by where.

[32] pry(main)> Product.where(id: 1).take.class
  Product Load (2.1ms)  SELECT `products`.* FROM `products` WHERE `products`.`id` = 1 LIMIT 1
=> Product(id: integer, name: string)

Depending on the situation, there may be situations where where + take is more appropriate (such as when you wantnil to be returned).

The confusing thing here is that if where + take does not hit any records in the scope, all the records in the corresponding table are also returned. nil is returned + Scope is due to the specification that ActiveRecord :: Relation is returned.

As an aside, the take! Method throws a ActiveRecord :: RecordNotFound exception, so it looks like you can substitutefind_by!.

Digression (recommended for find_by!)

I've written this for a long time, If there is a possibility that there is no match in find_by, it is better to use thefind_by!Method and throw the ActiveRecord :: RecordNotFound exception for error handling and processing is better? (In the first place, it seems abnormal that the find_by does not match)

# product.rb
  scope :hoge, ->(product_id) {
    find_by!(product_id: product_id)
  }

# product_controller.rb
def fuga
    product = Product.hoge(params[:product_id])
    render json: product, status: :ok
rescue ActiveRecord::RecordNotFound => e
    render json: e, status: :not_found
end

Recommended Posts

[Rails] Why using find_by in scope and returning all records instead of nil if there are no records that match the conditions
The problem that the contents of params are completely displayed in the [Rails] view
[Java] What to do if the contents saved in the DB and the name of the enum are different in the enum that reflects the DB definition
[Ruby on Rails] Implementation of validation that works only when the conditions are met
Introducing the settings and reference articles that are first done in Rails new development