sql' Replacement in Rails 4.2

I've got an association that needs a few joins / custom queries. When trying to figure out how to implement this the repeated response is finder_sql . However in Rails 4.2 (and above):

ArgumentError: Unknown key: :finder_sql

My query to do the join looks like this:

'SELECT DISTINCT "tags".*' 
' FROM "tags"' 
' JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"' 
' JOIN "articles" ON "article_tags"."article_id" = "articles"."id"' 
' WHERE articles"."user_id" = #{id}'

I understand that this can be achieved via:

has_many :tags, through: :articles

However if the cardinality of the join is large (ie a user has thousands of articles - but the system only has a few tags) it requires loading all the articles / tags:

SELECT * FROM articles WHERE user_id IN (1,2,...)
SELECT * FROM article_tags WHERE article_id IN (1,2,3...) -- a lot
SELECT * FROM tags WHERE id IN (1,2,3) -- a few

And of course also curious about the general case.

Note: also tried using the proc syntax but can't seem to figure that out:

has_many :tags, -> (user) {
  select('DISTINCT "tags".*')
    .joins('JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"')
    .joins('JOIN "articles" ON "article_tags"."article_id" = "articles"."id"')
    .where('"articles"."user_id" = ?', user.id)
}, class_name: "Tag"

ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column tags.user_id does not exist

SELECT DISTINCT "tags".* FROM "tags" JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id" JOIN "articles" ON "article_tags"."article_id" = "articles"."id" WHERE "tags"."user_id" = $1 AND ("articles"."user_id" = 1)

That is it looks like it is trying to inject the user_id onto tags automatically (and that column only exists on articles). Note: I'm preloading for multiple users so can't use user.tags without other fixes (the SQL pasted is what I'm seeing using exactly that!). Thoughts?


While this doesn't fix your problem directly - if you only need a subset of your data you can potentially preload it via a subselect:

users = User.select('"users".*"').select('COALESCE((SELECT ARRAY_AGG(DISTINCT "tags"."name") ... WHERE "articles"."user_id" = "users"."id"), '{}') AS tag_names')
users.each do |user|
  puts user[:tag_names].join(' ')
end

The above is DB specific for Postgres (due to ARRAY_AGG ) but an equivalent solution probably exists for other databases.

An alternative option might be to setup a view as a fake join table (again requires database support):

CREATE OR REPLACE VIEW tags_users AS (
  SELECT 
    "users"."id" AS "user_id", 
    "tags"."id" AS "tag_id"
  FROM "users"
    JOIN "articles" ON "users"."id" = "articles"."user_id"
    JOIN "articles_tags" ON "articles"."id" = "articles_tags"."article_id"
    JOIN "tags" ON "articles_tags"."tag_id" = "tags"."id"
  GROUP BY "user_id", "tag_id"
)

Then you can use has_and_belongs_to_many :tags (haven't tested - may want to set to readonly and can remove some of the joins and use if you have proper foreign key constraints setup).


So my guess is you are getting the error when you try to access @user.tags since you have that association inside the user.rb .

So I think what happens is when we try to access the @user.tags , we are trying to fetch the tags of the user and to that rails will search Tags whose user_id matches with currently supplied user's id. Since rails takes association name as modelname_id format by default, even if you don't have user_id it will try to search in that column and it will search (or add WHERE "tags"."user_id" ) no matter you want it to or not since ultimate goal is to find tags that are belongs to current user.

Of course my answer may not explain it 100%. Feel free to comment your thought or If you find anything wrong, let me know.


Short Answer

Ok, if I understand this correctly I think I have the solution, that just uses the core ActiveRecord utilities and does not use finder_sql.

Could potentially use:

user.tags.all.distinct

Or alternatively, in the user model change the has_many tags to

has_many :tags, -> {distinct}, through: :articles

You could create a helper method in user to retrieve this:

def distinct_tags
  self.tags.all.distinct
end

The Proof

From your question I believe you have the following scenario:

  • A user can have many articles.
  • An article belongs to a single user.
  • Tags can belong to many articles.
  • Articles can have many tags.
  • You want to retrieve all the distinct tags a user has associated with the articles they have created.
  • With that in mind I created the following migrations:

    class CreateUsers < ActiveRecord::Migration
      def change
        create_table :users do |t|
          t.string :name, limit: 255
    
          t.timestamps null: false
        end
      end
    end
    
    class CreateArticles < ActiveRecord::Migration
      def change
        create_table :articles do |t|
          t.string :name, limit: 255
          t.references :user, index: true, null: false
    
          t.timestamps null: false
        end
    
        add_foreign_key :articles, :users
      end
    end
    
    class CreateTags < ActiveRecord::Migration
      def change
        create_table :tags do |t|
          t.string :name, limit: 255
    
          t.timestamps null: false
        end
      end
    end
    
    class CreateArticlesTagsJoinTable < ActiveRecord::Migration
      def change
        create_table :articles_tags do |t|
          t.references  :article, index: true, null:false
          t.references  :tag, index: true, null: false
        end
    
        add_index :articles_tags, [:tag_id, :article_id], unique: true
        add_foreign_key :articles_tags, :articles
        add_foreign_key :articles_tags, :tags
      end
    end
    

    And the models:

    class User < ActiveRecord::Base
      has_many :articles
      has_many :tags, through: :articles
    
      def distinct_tags
        self.tags.all.distinct
      end
    end
    
    class Article < ActiveRecord::Base
      belongs_to :user
      has_and_belongs_to_many :tags
    end
    
    class Tag < ActiveRecord::Base
      has_and_belongs_to_many :articles
    end
    

    Next seed the database with a lot of data:

    10.times do |tagcount|
      Tag.create(name: "tag #{tagcount+1}")
    end
    
    5.times do |usercount|
      user = User.create(name: "user #{usercount+1}")
    
      1000.times do |articlecount|
        article = Article.new(user: user)
        5.times do |tagcount|
          article.tags << Tag.find(tagcount+usercount+1)
        end
        article.save
      end
    end
    

    Finally in rails console:

    user = User.find(3)
    user.distinct_tags
    

    results in following output:

      Tag Load (0.4ms)  SELECT DISTINCT `tags`.* FROM `tags` INNER JOIN `articles_tags` ON `tags`.`id` = `articles_tags`.`tag_id` INNER JOIN `articles` ON `articles_tags`.`article_id` = `articles`.`id` WHERE `articles`.`user_id` = 3
     => #<ActiveRecord::AssociationRelation [#<Tag id: 3, name: "tag 3", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 4, name: "tag 4", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 5, name: "tag 5", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 6, name: "tag 6", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 7, name: "tag 7", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">]> 
    
    链接地址: http://www.djcxy.com/p/36688.html

    上一篇: 如何在express js中执行res.redirect时传递标头

    下一篇: sql'替换在Rails 4.2中