13

I have the following example model structure:

class Category < ActiveRecord::Base
  has_many :posts

  scope :active, -> { where(active: true) }
end

class User < ActiveRecord::Base
  has_many :posts
  has_many :visible_posts, -> { joins(:category).merge(Category.active) }, class: Post
  has_many :visible_posts_comments, through: :visible_posts, source: :comments

  has_many :comments
end

class Post < ActiveRecord::Base
  belongs_to :category
  belongs_to :user
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
  belongs_to :user
end

Now a User.first.visible_posts_comments raises the following error:

ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "categories"
LINE 1: ..." = "posts"."id" WHERE "posts"."user_id" = $1 AND "categorie...

Which is because the SQL which is executed by this association is the following:

2.1.2 :009 > u.visible_posts_comments.to_sql
 => "SELECT \"comments\".* FROM \"comments\" INNER JOIN \"posts\" ON \"comments\".\"post_id\" = \"posts\".\"id\" WHERE \"posts\".\"user_id\" = $1 AND \"categories\".\"active\" = 't'"

While visible_posts works properly by adding the INNER JOIN on categories,

2.1.2 :010 > u.visible_posts.to_sql
 => "SELECT \"posts\".* FROM \"posts\" INNER JOIN \"categories\" ON \"categories\".\"id\" = \"posts\".\"category_id\" WHERE \"posts\".\"user_id\" = $1 AND \"categories\".\"active\" = 't'"

why does visible_posts_comments seem to "lose" the joins(:category) statement but keeps the merge(Category.active)? I see no reason to drop the joins of the through-association on purpose. Is this a bug or a feature?

I am using activerecord-4.1.8.

Could be related to this: https://github.com/rails/rails/issues/17904

Steve Beer
  • 1,318
  • 12
  • 16

1 Answers1

5

I've created a rails project the same as yours, found the same issue. Two points on this issue:

1. has_many :through will delete "joins" from the through relations, source code:

#lib/active_record/associations/through_association.rb  line 14
    def target_scope
      scope = super
      chain.drop(1).each do |reflection|
        relation = reflection.klass.all
        relation.merge!(reflection.scope) if reflection.scope

        scope.merge!(
          relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
        )

      end
      scope
    end

I think the reason they did like this is a consideration of record creating operations. ex. Maybe u.visible_posts_comments.create(...) will make ActiveRecord confused

2. A walkaround way for you:

class Category < ActiveRecord::Base
  has_many :posts
end

class User < ActiveRecord::Base
  has_many :posts
  has_many :visible_posts, -> { merge(Post.active) }, class: Post
  has_many :visible_posts_comments, -> { joins(:post).merge(Post.active) }, class: Comment

  has_many :comments
end

class Post < ActiveRecord::Base
  belongs_to :category
  belongs_to :user
  has_many :comments

  scope :active, -> { joins(:category).merge(Category.active) }
end

class Comment < ActiveRecord::Base
  belongs_to :post
  belongs_to :user
end
Community
  • 1
  • 1
brookz
  • 477
  • 5
  • 17
  • I accept the answer because it's in the code of ActiveRecord, though I still don't get why they did that. Calling `has_many_through_association.create(...)` does not make much sense to me at all because it would never be clear which of the (possibly many) "through" records the created one should be assigned to. – Steve Beer Jan 21 '15 at 10:38