我有一个需要一些连接/自定义查询的关联。在试图弄清楚如何实现这一点时,重复的响应是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粘贴就是这样!)。想法?
答案 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
证明
从您的问题中我相信您有以下情况:
考虑到这一点,我创建了以下迁移:
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关系。
此代码将至少两次访问数据库:
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