В чём разница между joins и includes в Ruby on Rails?

joins использует связь таблиц для формирования выборки, а includes заранее загружает ассоциации. Фильтрация пользователей по постам не равна загрузке самих постов.

Короткий ответ

Что ответить на собеседовании

joins(:posts) обычно строит INNER JOIN: помогает отбирать пользователей по постам, но не заполняет кеш ассоциации posts и может дублировать пользователей. includes(:posts) предназначен для предварительной загрузки: обычно использует отдельные SELECT, а при условиях на posts может перейти к LEFT OUTER JOIN. Если нужно отобрать пользователей по части постов, но загрузить все их посты, удобно сочетать joins, distinct и preload.

Подробный разбор

Ответ с пояснениями

Предположим, модель User имеет обычную ассоциацию has_many :posts. joins и includes решают разные задачи: первый соединяет таблицы для формирования выборки, второй организует предварительную загрузку связанных объектов.

users = User.joins(:posts).distinct

joins(:posts) строит INNER JOIN, поэтому пользователи без постов не попадут в результат. Если постов несколько, строки пользователя могут повторяться; distinct устраняет повторы в этой выборке. При этом joins сам по себе не загружает объекты Post в кеш ассоциации. Последующий обход user.posts у каждого пользователя способен создать N+1 запросов. Соединение таблиц

Для чтения ассоциаций подходит другой вариант:

User.includes(:posts).each do |user|
  user.posts.each { |post| puts post.title }
end

Без условий на таблицу постов это обычно отдельный запрос пользователей и затем запрос постов для найденных пользователей, а не запрос на каждого из них. Пользователи без постов сохраняются, их коллекция будет пустой. Здесь обход читает загруженные записи; замена его на posts.count может снова породить SQL-подсчёты. Предварительная загрузка

Стратегия includes зависит от запроса. Например, includes(:posts).where(posts: { published: true }) использует LEFT OUTER JOIN, но условие в WHERE исключает пользователей без подходящих постов и ограничивает загруженные посты опубликованными. Для строкового SQL-условия требуется явно указать связанную таблицу через references(:posts). Семантика includes

Если нужны пользователи хотя бы с одним опубликованным постом, но все их посты, разделяем отбор и загрузку:

users = User.joins(:posts)
            .where(posts: { published: true })
            .distinct
            .preload(:posts)

Здесь preload отдельно загружает полную ассоциацию в рамках её собственных условий. Без дополнительных scopes это все посты выбранных пользователей. Выбор метода зависит от нужного результата и плана запросов, а не от правила «JOIN всегда быстрее двух SELECT». Метод preload

Практика в реальном времени

Подготовьтесь к следующему собеседованию

Interview Boost учитывает вакансию, резюме и технологии и помогает сформулировать ответ прямо во время интервью.

Начать подготовку