在Rails 4.2

时间:2016-10-12 18:12:52

标签: ruby-on-rails ruby activerecord

我有一个需要一些连接/自定义查询的关联。在试图弄清楚如何实现这一点时,重复的响应是finder_sql。但是在Rails 4.2(及更高版本)中:

  

ArgumentError:未知密钥:: finder_sql

我进行连接的查询如下所示:

'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}'

我知道这可以通过以下方式实现:

has_many :tags, through: :articles

但是,如果联接的基数很大(即用户有数千篇文章 - 但系统只有几个标签),则需要加载所有文章/标签:

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

当然也对一般情况感到好奇。

注意:也尝试使用proc语法,但似乎无法弄明白:

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:列tags.user_id不存在

     

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)

看起来它似乎试图将user_id自动注入标签(并且该列仅存在于文章上)。注意:我正在为多个用户预加载,因此在没有其他修复的情况下无法使用user.tags(我正在使用的SQL粘贴就是这样!)。想法?

6 个答案:

答案 0 :(得分:3)

虽然这不能直接解决您的问题 - 如果您只需要数据的子集,则可以通过子选择预加载它:

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

以上是针对Postgres的特定数据库(由于ARRAY_AGG),但其他数据库可能存在等效解决方案。

另一种选择可能是将视图设置为伪连接表(再次需要数据库支持):

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"
)

然后你可以使用has_and_belongs_to_many :tags(尚未测试 - 可能想要设置为readonly并且可以删除一些连接并使用,如果你有适当的外键约束设置)。

答案 1 :(得分:0)

所以我猜您在尝试访问@user.tags时收到错误,因为您在user.rb内有该关联。

所以我认为当我们尝试访问@user.tags时会发生什么,我们正在尝试获取用户的tags,并且该导轨会搜索其Tags user_id与当前提供的用户ID匹配。由于rails默认情况下将关联名称设置为modelname_id格式,即使您没有user_id,它也会尝试在该列中进行搜索,并且无论如何都会搜索(或添加WHERE "tags"."user_id")您是否想要它,因为最终目标是找到属于当前用户的tags

当然,我的答案可能无法100%解释。随意评论您的想法或如果您发现任何错误,请告诉我。

答案 2 :(得分:0)

简答

好的,如果我理解正确,我认为我有解决方案,只使用核心ActiveRecord实用程序而不使用finder_sql。

可以使用:

user.tags.all.distinct

或者,在用户模型中,将has_many标记更改为

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

您可以在用户中创建一个帮助方法来检索它:

def distinct_tags
  self.tags.all.distinct
end

证明

从您的问题中我相信您有以下情况:

  1. 用户可以有很多文章。
  2. 文章属于单个用户。
  3. 标签可以属于很多文章。
  4. 文章可以有很多标签。
  5. 您想要检索用户与其创建的文章相关联的所有不同标记。
  6. 考虑到这一点,我创建了以下迁移:

    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
    

    模特:

    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
    

    接下来使用大量数据为数据库播种:

    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
    

    最后在rails console中:

    user = User.find(3)
    user.distinct_tags
    

    导致以下输出:

      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">]> 
    

答案 3 :(得分:0)

使用eager_load强制ActiveRecord执行连接可能会很有帮助。它的作用是includes(:tags).references(:tags)

以下是代码段:

users.eager_load(:tags).map { |user| user.tag.inspect }
# equal to
users.includes(:tags).references(:tags).map { |user| user.tag.inspect }

其中users - 是ActiveRecord关系。

此代码将至少两次访问数据库:

  1. 仅选择用户ID(希望,不要太多)
  2. 通过article_tags避免
  3. 选择具有联接标记的用户
      

    SELECT * FROM article_tags WHERE article_id IN(1,2,3 ...) - 很多

答案 4 :(得分:0)

你正走在has_many :tags, through: :articles的正确道路上(或者凯文建议的更好has_many :tags, -> {distinct}, through: :articles)。但是你应该读一下includes vs preload vs eager_load。你这样做:

User.preload(:tags).each {|u| ... }

但你应该这样做:

User.eager_load(:tags).each {|u| ... }

或者这个:

User.includes(:tags).references(:tags).each {|u| ... }

当我这样做时,我得到了这个问题:

SELECT  "users"."id" AS t0_r0,
        "tags"."id" AS t1_r0,
        "tags"."name" AS t1_r1
FROM    "users"
LEFT OUTER JOIN "articles"
ON      "articles"."user_id" = "users"."id"
LEFT OUTER JOIN "articles_tags"
ON      "articles_tags"."article_id" = "articles"."id"
LEFT OUTER JOIN "tags"
ON      "tags"."id" = "articles_tags"."tag_id"

但这仍然会从数据库向您的应用发送大量冗余内容。这会更快:

User.eager_load(:tags).distinct.each {|u| ... }

,并提供:

SELECT  DISTINCT "users"."id" AS t0_r0,
        "tags"."id" AS t1_r0,
        "tags"."name" AS t1_r1
FROM    "users"
LEFT OUTER JOIN "articles"
ON      "articles"."user_id" = "users"."id"
LEFT OUTER JOIN "articles_tags"
ON       "articles_tags"."article_id" = "articles"."id"
LEFT OUTER JOIN "tags"
ON "tags"."id" = "articles_tags"."tag_id"

只做User.first.tags.map &:name让我加入:

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" = ?

有关详细信息,请参阅此github repo并使用rspec测试查看SQL Rails正在使用的内容。

答案 5 :(得分:0)

有三种可能的解决方案:

1)继续使用has_many关联

假冒user_id列,将其添加到所选列。

  class User < ActiveRecord::Base
    has_many :tags, -> (user) {
      select(%Q{DISTINCT "tags".*, #{user_id} AS user_id })
        .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"
  end

2)在User类

上添加实例方法

如果您仅使用tags进行查询而未在联接中使用它,则可以使用此方法:

class User
  def tags
   select(%Q{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" = ?', id)
  end
end

现在user.tags在所有实际用途中都像一个关联。

3)使用EXISTS的OTOH可能比使用不同的

更高效
  class User < ActiveRecord::Base
    def tags
      exists_sql = %Q{
        SELECT  1
        FROM    articles,
                articles_tags
        WHERE   "articles"."user_id" = #{id} AND
                "articles_tags"."article_id" = "article"."id" AND
                "articles_tags"."tag_id" = "tags.id"
      }
      Tag.where(%Q{ EXISTS ( #{exists_sql} ) })
    end
  end