Rails: как получить объекты хотя бы с одним потомком? - PullRequest
20 голосов
/ 29 марта 2012

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

Ответы [ 6 ]

52 голосов
/ 29 марта 2012
Parent.joins(:children).uniq.all
3 голосов
/ 29 марта 2012

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

Parent.joins("left join childrens on childrends.parent_id = parents.id").where("childrents.parent_id is not null")
2 голосов
/ 06 марта 2017

Начиная с Rails 5.1 , uniq устарело и вместо него следует использовать distinct.

Parent.joins(:children).distinct

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

2 голосов
/ 22 октября 2016

Принятый ответ (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

2 голосов
/ 29 марта 2012

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

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

, решение которого подходит по многим причинам.Я рекомендую библиотеку Эрни Миллера squeel , которая позволит вам сделать

Parent.joins{children.outer}.where{children.id == nil}
2 голосов
/ 29 марта 2012

попробуйте включить детей с #includes()

Parent.includes(:children).all.reject { |parent| parent.children.empty? }

Это сделает 2 запроса:

SELECT * FROM parents;
SELECT * FROM children WHERE parent_id IN (5, 6, 8, ...);

[ОБНОВЛЕНИЕ]

Приведенное выше решение полезно, когда вам нужно загрузить дочерние объекты.Но 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? делают.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...