0

The following query Model.first.model_relation.map(&:another_model).map(&:another_model) returns [#<ActiveRecord::Associations::CollectionProxy []>]

I also have a similar query that returns xx records.

When I do the above query and call .blank? it returns false. Why is this and how can i determine if these queries return records or not?

user14069773
  • 175
  • 1
  • 10
  • 2
    This sounds like a [textbook X&Y problem](https://en.wikipedia.org/wiki/XY_problem). You're asking us about a solution to a problem that clearly won't work (or even if it does results in horrible performance due to two levels of n+1 queries) instead of asking us about the real problem that you're attempting to solve. If what you want is to do Y, you should ask that question without pre-supposing the use of a method that may not be appropriate. – max Aug 18 '20 at 08:14
  • Instead show us an example of the models involved, the data and the expected result. I'm guessing in this case that the right answer here is really performing a join on the other table. See https://stackoverflow.com/a/5570221/544825. – max Aug 18 '20 at 08:17
  • 1
    This is a terrible, terrible, method of getting your relationships. It will take forever to load. You should add a scope on your last model that will return a list based on your first model. – Roc Khalil Aug 18 '20 at 08:20

2 Answers2

3

Given the following example models:

# rails g model country name
class Country < ApplicationRecord
  has_many :states
  has_many :cities, through: :states
end

# rails g model country name country:belongs_to
class State < ApplicationRecord
  belongs_to :country
  has_many :cities
end

# rails g model city name state:belongs_to
class City < ApplicationRecord
  belongs_to :state
  has_one :country, through: :state
end

We can get records with matches in joined tables by applying an INNER JOIN:

countries_with_states = Country.joins(:states)
countries_with_cities = Country.joins(states: :cities)
# can be shortened since we have an indirect association
countries_with_cities = Country.joins(:cities)

This will just returns rows with at least one match in the joined table.

We can also get records without matches in the joined table by using a LEFT OUTER JOIN with a condition on the joined table:

countries_with_no_states = Country.left_joins(:states)
                                  .where(states: { id: nil })
countries_with_no_cities = Country.left_joins(:cities)
                                  .where(cities: { id: nil })

Using #map on an association should not be done as its extremely ineffective and can lead to serious performance issues. You instead need to create meaningful associations between your models and use joins to filter the rows in the database.

ActiveSupport's #blank? and #empty? methods should really only be used when dealing with user input like strings and arrays of strings which is where its heuristics are actually useful.

For collections of records you have .exists? which will always create a query and .any? and .none? which will use the size if the relation has been loaded.

max
  • 96,212
  • 14
  • 104
  • 165
  • "extremely ineffective", "serious performance issues" - this all depends on the size of your db. For small tables, you probably wouldn't notice any difference. – claasz Aug 18 '20 at 10:33
  • @claasz almost anything will work if the scale is tiny enough. Doesn't mean that its a good way to build applications. Building an unscalable mess of n+1 queries is not simpler or have any merits on its own. And you don't really need to go to mega-scale before you start encountering problems - a few 1000 records will definitely throw a wrench in things on a free heroku tier for example. – max Aug 18 '20 at 11:02
  • @max Are you thinking of `#exists?`, which applies a `limit (1)` to the query and `select 1` to see if any rows exist? I think it is also worth mentioning that `countries_with_states = Country.joins(:states)` as a way of finding countries for which states exist is potentially very inefficient. – David Aldridge Aug 18 '20 at 12:43
  • 1
    @DavidAldridge you're right about `#exists?` I was just lazily writing from memory. I have edited the answer. The efficiency really depends on what you're comparing it to. A read optimization such as a counter cache will be more effective but joining still is not that expensive and will outperform looping through an association by a long shot. – max Aug 19 '20 at 15:35
  • Yeah inefficiency varies, but an `exists` predicate would insulate you from inefficiency, and still push to the database. e.g: `Country.where(City.where(City.arel_table[:country_id].eq(Country.arel_table[:id])).arel.exists`) (may have the parens wrong, but it can be hidden by implementing a `has_no cities` scope), which turns into a `not exists` very easily with `Country.where.not(etc`. This applies more on has_many than belongs_to of course, but there's a slight performance improvement (DB and Rails) through not retrieving the column values from the joins table even on `belongs_to`. – David Aldridge Aug 19 '20 at 22:20
1

I guess the method you are looking for is empty?, which is defined on ActiveRecord::Associations::CollectionProxy:

empty?() Returns true if the collection is empty.

https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-empty-3F

claasz
  • 2,059
  • 1
  • 14
  • 16