how to check if has_one exists without loading associated model
Asked Answered
E

4

3

I have a simple has_one relationship

class User < ApplicationRecord
  has_one :detail
  has_many :courses
end

class Detail < ApplicationRecord
  belongs_to :user
end

I need to check to see if a user has a detail. Since I can do User.first.courses.exists? it was surprising that User.first.detail.exists? doesn't work. I could do !User.first.detail.nil? but I only need to check if a user detail exists and not load it's very hefty detail model.

What's a good way to check if a has_one association exists without retrieving the entire associated model?

Either answered 6/9, 2020 at 15:43 Comment(0)
P
0

You can use: User.first.detail.present?

Presentationism answered 6/9, 2020 at 16:31 Comment(2)
The OP asks to not load the detail model. This answer would query and allocate both User and Detail models.Crunode
@Crunode is correct in that this still does the DB query. I posted my answer above which I think is what OP wants: https://mcmap.net/q/260728/-how-to-check-if-has_one-exists-without-loading-associated-modelPyre
C
3

Detail.where(user_id: User.first.id).exists?

This doesn't allocate a Detail object if all you want to know is if a user has a detail or not.

Crunode answered 3/6, 2021 at 8:36 Comment(2)
This doesn't work because detail is a has_one association, so there is no detail_id on User.Lauzon
@Lauzon oh silly me! I'll fix my answerCrunode
P
2

To check if an association is loaded without hitting the DB you can use:

model.association(:association_name).loaded? for has_one/belongs_to associations and model.many_things.loaded? for has_many associations.

Checking if has_one/belongs_to association has been loaded:

user = User.find(1)

user.association(:detail).loaded? # => false

user.detail.id # Does DB request to get Detail

user.association(:detail).loaded? # => true
With Eager loading

Eager loading lets you load the data in advance, so as expected the association is marked as loaded:

user = User.eager_load(:detail).find(1)
user.detail.loaded? # => true

Checking if has_many association has been loaded

With has_many associations,

user = User.find(1)
user.association(:courses).loaded? # => false

user.courses.load # Does DB query to get all users' courses
user.association(:courses).loaded? # => true
Doing .first or some other queries won't always load

Keep in mind that it does not mark as loaded in all cases:

user.courses.first.id # Does DB query to get first course ID
user.association(:courses).loaded? # => false (still!)

This is because it is actually doing a SELECT * LIMIT 1 query when you do the .first for efficiency-sake.

With Eager loading
user = User.eager_load(:courses).find(1)
user.association(:courses).loaded? # => true

When to use the loaded? method?

The only use case I can think of is when you don't want to trigger a DB call but you want to check to see if the data has been loaded already and do something with it only if it has.

In my case I wanted to output some data for logging purposes, but only if it was not adding an extra DB query by calling the association.

Pyre answered 27/9, 2021 at 16:8 Comment(4)
This does not actually determine is the association exists, in the sense that a record is persisted in the database. This only confirms if the association has been loaded into memory. Additionally even when @loaded is true it does not confirm the existence of a record, meaning count > 0. The most efficient way to confirm both would be to use exists? which will run a query that returns a boolean without instantiating the object.Reasonable
@Reasonable as long as you're eager loading with a has_one relationship like OP then it does mean count > 0, so it solves the problem, albiet a bit differently l.Pyre
No query. loaded? is based on @loaded if the data is not loaded no query is executed and false is returned. One could have 1M records but this methodology will return false if the association has not been called (or loaded?) but one could have 0 records and this methodology will return true if the association has been called, therefore this does not solve the question posed.Reasonable
@Reasonable I see what you're saying. So yeah you'd need to use another check to see if the model data exists in addition to eager loading and the loaded?. Thanks for explaining! Upvoted your comments :)Pyre
P
0

You can use: User.first.detail.present?

Presentationism answered 6/9, 2020 at 16:31 Comment(2)
The OP asks to not load the detail model. This answer would query and allocate both User and Detail models.Crunode
@Crunode is correct in that this still does the DB query. I posted my answer above which I think is what OP wants: https://mcmap.net/q/260728/-how-to-check-if-has_one-exists-without-loading-associated-modelPyre
B
0

Rails way it to load the object and check if it's present. But still, you can access the association scope to use it without loading the associated object:

user = User.find(1)

user.association(:detail).loaded? # => false

# Triggers the SQL you need: just check if a Detail record associated with that user exists.
user.association(:detail).scope.exists?

user.association(:detail).loaded? # => false (we still didn't load the associated object)

user.detail.id # Does DB request to get Detail

user.association(:detail).loaded? # => true
Bosomy answered 20/2, 2023 at 16:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.