Подтвердить что ты не робот

Rails: Как получить объекты с хотя бы одним ребенком?

После поиска в Интернете, просмотра 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.

4b9b3361

Ответ 1

Parent.joins(:children).uniq.all

Ответ 2

Я только что изменил это решение для ваших нужд.

Parent.joins("left join childrens on childrends.parent_id = parents.id").where("childrents.parent_id is not null")

Ответ 3

попробуйте включить детей с #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.

Ответ 4

Вы просто хотите, чтобы внутреннее соединение с отдельным классификатором

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}

Ответ 5

Принятый ответ (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, так что вы можете подтвердить разницу самостоятельно:

https://github.com/JunichiIto/exists-query-sandbox

Ответ 6

От Rails 5.1, uniq устарел и distinct следует использовать вместо этого.

Parent.joins(:children).distinct

Это ответ на ответ Криса Бейли. .all также удаляется из исходного ответа, поскольку он ничего не добавляет.