Ответ 1
Parent.joins(:children).uniq.all
После поиска в Интернете, просмотра SO и чтения, похоже, не существует стиля Rails для эффективного получения только тех Parent
объекты, имеющие хотя бы один объект Child
(через отношение has_many :children
). В простом SQL:
SELECT *
FROM parents
WHERE EXISTS (
SELECT 1
FROM children
WHERE parent_id = parents.id)
Ближайший я пришел
Parent.all.reject { |parent| parent.children.empty? }
(на основе другого ответа), но он действительно неэффективен, потому что он выполняет отдельный запрос для каждого Parent
.
Parent.joins(:children).uniq.all
Я только что изменил это решение для ваших нужд.
Parent.joins("left join childrens on childrends.parent_id = parents.id").where("childrents.parent_id is not null")
попробуйте включить детей с #includes()
Parent.includes(:children).all.reject { |parent| parent.children.empty? }
Это сделает 2 запроса:
SELECT * FROM parents;
SELECT * FROM children WHERE parent_id IN (5, 6, 8, ...);
[ОБНОВЛЕНИЕ]
Вышеприведенное решение полезно, когда вам нужно загрузить объекты Child.
Но children.empty?
также может использовать кеш-счетчик 1, 2 чтобы определить количество детей.
Для этого вам нужно добавить новый столбец в таблицу parents
:
# a new migration
def up
change_table :parents do |t|
t.integer :children_count, :default => 0
end
Parent.reset_column_information
Parent.all.each do |p|
Parent.update_counters p.id, :children_count => p.children.length
end
end
def down
change_table :parents do |t|
t.remove :children_count
end
end
Теперь измените модель Child
:
class Child
belongs_to :parent, :counter_cache => true
end
В этот момент вы можете использовать size
и empty?
, не касаясь таблицы children
:
Parent.all.reject { |parent| parent.children.empty? }
Обратите внимание, что length
не использует кеш счетчика, тогда как size
и empty?
do.
Вы просто хотите, чтобы внутреннее соединение с отдельным классификатором
SELECT DISTINCT(*)
FROM parents
JOIN children
ON children.parent_id = parents.id
Это можно сделать в стандартной активной записи как
Parent.joins(:children).uniq
Однако, если вам нужен более сложный результат поиска всех родителей без детей вам нужно внешнее соединение
Parent.joins("LEFT OUTER JOIN children on children.parent_id = parent.id").
where(:children => { :id => nil })
что является решением, которое sux по многим причинам. Я рекомендую библиотеку Ernie Millers squeel, которая позволит вам делать
Parent.joins{children.outer}.where{children.id == nil}
Принятый ответ (Parent.joins(:children).uniq
) генерирует SQL, используя DISTINCT, но это может быть медленный запрос. Для лучшей производительности вы должны написать SQL с помощью EXISTS:
Parent.where<<-SQL
EXISTS (SELECT * FROM children c WHERE c.parent_id = parents.id)
SQL
EXISTS намного быстрее, чем DISTINCT. Например, вот модель публикации, которая имеет комментарии и отзывы:
class Post < ApplicationRecord
has_many :comments
has_many :likes
end
class Comment < ApplicationRecord
belongs_to :post
end
class Like < ApplicationRecord
belongs_to :post
end
В базе данных есть 100 сообщений, и у каждого сообщения есть 50 комментариев и 50 понравлений. Только у одного сообщения нет комментариев и комментариев:
# Create posts with comments and likes
100.times do |i|
post = Post.create!(title: "Post #{i}")
50.times do |j|
post.comments.create!(content: "Comment #{j} for #{post.title}")
post.likes.create!(user_name: "User #{j} for #{post.title}")
end
end
# Create a post without comment and like
Post.create!(title: 'Hidden post')
Если вы хотите получать сообщения, имеющие хотя бы один комментарий и т.д., вы можете написать вот так:
# NOTE: uniq method will be removed in Rails 5.1
Post.joins(:comments, :likes).distinct
Запрос выше генерирует SQL следующим образом:
SELECT DISTINCT "posts".*
FROM "posts"
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
INNER JOIN "likes" ON "likes"."post_id" = "posts"."id"
Но этот SQL генерирует 250000 строк (100 сообщений * 50 комментариев * 50 нравится), а затем отфильтровывает дублированные строки, поэтому он может быть медленным.
В этом случае вы должны написать вот так:
Post.where <<-SQL
EXISTS (SELECT * FROM comments c WHERE c.post_id = posts.id)
AND
EXISTS (SELECT * FROM likes l WHERE l.post_id = posts.id)
SQL
Этот запрос генерирует SQL следующим образом:
SELECT "posts".*
FROM "posts"
WHERE (
EXISTS (SELECT * FROM comments c WHERE c.post_id = posts.id)
AND
EXISTS (SELECT * FROM likes l WHERE l.post_id = posts.id)
)
Этот запрос не создает бесполезные дублированные строки, поэтому он может быть быстрее.
Вот эталон:
user system total real
Uniq: 0.010000 0.000000 0.010000 ( 0.074396)
Exists: 0.000000 0.000000 0.000000 ( 0.003711)
Он показывает, что EXISTS на 20.047661 раз быстрее DISTINCT.
Я нажал образец приложения в GitHub, так что вы можете подтвердить разницу самостоятельно:
От Rails 5.1, uniq
устарел и distinct
следует использовать вместо этого.
Parent.joins(:children).distinct
Это ответ на ответ Криса Бейли. .all
также удаляется из исходного ответа, поскольку он ничего не добавляет.