Niels B.
Niels B.

Reputation: 6310

Ideal way to get association of ActiveRecord::Relation

Consider the following:

class User < ActiveRecord::Base
  has_many :posts
end

Now I want to get posts for some banned users.

User.where(is_banned: true).posts

This produces a NoMethodError as posts is not defined on ActiveRecord::Relation.

What is the slickest way of making the code above work?

I can think of

User.where(is_banned: true).map(&:posts).flatten.uniq

But this is inefficient.

I can also think of

user_scope = User.where(is_banned: true)
Post.where(user: user_scope)

This requires the user association to be set up in the Post model and it appears to generate a nested select. I don't know about the efficiency.

Ideally, I would like a technique that allows traversing multiple relations, so I can write something like:

User.where(is_banned: true).posts.comments.votes.voters

which should give me every voter (user) who has voted for a comment on a post written by a banned user.

Upvotes: 0

Views: 107

Answers (3)

Jacob Brown
Jacob Brown

Reputation: 7561

Here's a start of a solution for your ideal technique. It probably doesn't work as written with extended chaining, and performance would probably be pretty bad. It would also require that you define the inverse_of for each association —

module LocalRelationExtensions

  def method_missing(meth, *args, &blk)
    if (assoc = self.klass.reflect_on_association(meth)) && (inverse = assoc.inverse_of)
      assoc.klass.joins(inverse.name).merge(self)
    else
      super
    end
  end

end

ActiveRecord::Relation.include(LocalRelationExtensions)

But really you should use the comment of @engineersmnky.

Upvotes: 0

nzajt
nzajt

Reputation: 1985

In your code:

User.where(is_banned: true)

will be and ActiveRecord::Relation and you need one record. So doing if from the User model would be more complicated. Depending on how the relationship is set up you could add a scope in your Post model.

scope :banned_users, -> { joins(:users).where('is_banned = ?', true) }

Then you would just call Post.banned_users to get all the post created by banned users.

Upvotes: 1

engineersmnky
engineersmnky

Reputation: 29318

Why not just use joins?

Post.joins(:user).where(users: {is_banned: true})

This will generate SQL to the effect of

SELECT * 
FROM posts
INNER JOIN users ON posts.user_id = users.id
WHERE users.is_banned = true

This seems to be exactly what you are looking for. As far as your long chain goes you can do the same thing just with a much deeper join.

Upvotes: 2

Related Questions