Alien
Alien

Reputation: 1172

How to create a new association in Rails which eager loads other associations?

So I have this sort of system currently:

class Tag < ActiveRecord::Base
  # attr :name
end

class Article < ActiveRecord::Base
  has_many :tag_associations, dependent:  :destroy
  has_many :tags, through: :tag_associations
end

class Company < ActiveRecord::Base
  has_many :articles
end

class User < ActiveRecord::Base
  belongs_to :company
end

I would like to do this:

user.company.articles.includes(:tags).all

I am going to use it like this (I already have a question on the eager-loading aspect of this):

company.articles.all.select do |article|
  article.tags.any? do |tag|
    tag.name == 'foo'
  end
end

How can I make a new association on the company object for articles_by_tag?

class Company < ActiveRecord::Base
  has_many :articles
  has_many :articles_by_tag, scope -> { |tag| ... }
end

And how would I end up using it?

company.articles_by_tag(tag: 'foo').all

Upvotes: 0

Views: 30

Answers (1)

max
max

Reputation: 102213

This is a textbook X & Y question. Eager loading everything and then using .select with a block to loop through it in Ruby is basically like ordering every pizza at a restaurant just to find the one that has anchovies on it and tossing everything else.

Instead you use a INNER JOIN to filter the rows:

Acticle.joins(:tags)
       .where(tags: { name: 'foo' })

This will only return rows with a match in the tags table.

If you want to write this into a scope you would do:

class Article < ApplicationRecord
  scope :with_tag, ->(name){ joins(:tags).where(tags: { name: name }) }
end

As a bonus you can use GROUP and HAVING to find articles with a list of tags:

class Acticle  < ApplicationRecord
  def self.with_tags(*tags)
    left_joins(:tags)
      .group(:id)
      .where(tags: { name: tag })
      .having(Tag.arel_table[Arel.star].count.eq(tags.length))
  end
end 

Upvotes: 1

Related Questions