Как мне заставить Rails рассчитывать на загрузку?
Это связано с вопросом год назад и изменением назад.
Я поставил пример вопроса, который должен работать из коробки, при условии, что у вас есть sqlite3: https://github.com/cairo140/rails-eager-loading-counts-demo
Инструкция по установке (для основной ветки)
git clone git://github.com/cairo140/rails-eager-loading-counts-demo.git
cd rails-eager-loading-counts-demo
rails s
У меня есть более полная запись в хранилище, но мой общий вопрос заключается в следующем.
Как я могу заставить Rails активно рассчитывать нагрузку таким образом, чтобы минимизировать количество запросов в базу данных?
n+1
проблема возникает всякий раз, когда вы используете #count
на ассоциации, несмотря на то, что включение этой ассоциации через #includes(:associated)
в ActiveRelation. Обходной путь должен использовать #length
, но это работает хорошо только тогда, когда объект, к которому он вызывается, уже загружен, не говоря уже о том, что я подозреваю, что он дублирует что-то, что внутренности Rails уже сделали. Также проблема с использованием #length
в том, что это приводит к неудачной перегрузке, когда ассоциация не была загружена с самого начала, и это все, что вам нужно.
Из readme:
Мы можем избежать этой проблемы, запустив #length в массиве posts (см. Приложение), который уже загружен, но было бы хорошо, если бы count также был легко доступен. Мало того, что это более последовательно; он обеспечивает путь доступа, который не обязательно требует загрузки сообщений. Например, если у вас есть частичное, которое отображает счет независимо от того, что, но половину времени, частичное вызывается с загруженными сообщениями, а половину - без, вы сталкиваетесь со следующим сценарием:
- С помощью
#count
- N
COUNT
запросы стиля, когда сообщения уже загружены- N
COUNT
запросы стиля, когда сообщения еще не загружены- С помощью
#length
- Ноль дополнительных запросов, когда сообщения уже загружены
- N
*
запросы стиля, когда сообщения еще не загруженыМежду этими двумя вариантами нет доминирующего варианта. Но было бы неплохо изменить #count, чтобы отложить до #length или получить доступ к длине, которая каким-то другим образом хранится за кулисами, чтобы у нас был следующий сценарий:
- Используя пересмотренный
#count
- Ноль дополнительных запросов, когда сообщения уже загружены
- N
COUNT
запросы стиля, когда сообщения еще не загружены
Так каков правильный подход здесь? Есть что-то, что я упустил (очень, очень вероятно)?
4 ответа
Похоже, что наилучшим способом реализации такого рода средств может быть создание представлений SQL (см.: здесь и здесь) для отдельных объектов модели и дочернего счета, которые вы хотите; и связанные с ними модели ActiveRecord.
Вы можете быть очень умным и использовать подклассы в исходной модели в сочетании с set_table_name :sql_view_name
сохранить все оригинальные методы на объектах и, возможно, даже некоторые из их ассоциаций.
Например, скажем, мы должны были добавить "Post.has_many: comments" к вашему примеру, как в ответе @Zubin выше; тогда можно было бы сделать:
class CreatePostsWithCommentsCountsView < ActiveRecord::Migration
def self.up
#Create SQL View called posts_with_comments_counts which maps over
# select posts.*, count(comments.id) as comments_count from posts
# left outer join comments on comments.post_id = posts.id
# group by posts.id
# (As zubin pointed out above.)
#*Except* this is in SQL so perhaps we'll be able to do further
# reducing queries against it *as though it were any other table.*
end
end
class PostWithCommentsCount < Post #Here there be cleverness.
#The class definition sets up PWCC
# with all the regular methods of
# Post (pointing to the posts table
# due to Rails' STI facility.)
set_table_name :posts_with_comment_counts #But then we point it to the
# SQL view instead.
#If you don't really care about
# the methods of Post being in PWCC
# then you could just make it a
# normal subclass of AR::Base.
end
PostWithCommentsCount.all(:include => :user) #Obviously, this sort of "upward
# looking" include is best used in big lists like "latest posts" rather than
# "These posts for this user." But hopefully it illustrates the improved
# activerecordiness of this style of solution.
PostWithCommentsCount.all(:include => :comments) #And I'm pretty sure you
# should be able to do this without issue as well. And it _should_ only be
# the two queries.
Как подсказывает @apneadiving, counter_cache работает хорошо, потому что столбец counter автоматически обновляется при добавлении или удалении записей. Поэтому, когда вы загружаете родительский объект, счетчик включается в объект без необходимости доступа к другой таблице.
Однако, если по какой-либо причине вам не нравится такой подход, вы можете сделать это:
Post.find(:all,
:select => "posts.*, count(comments.id) `comments_count`",
:joins => "left join comments on comments.post_id = posts.id")
Альтернативный подход к тому из Зубина:
Post.select('posts.*, count(comments.id) `comments_count`').joins(:comments).group('posts.id')
Я создал маленький драгоценный камень, который добавляет includes_count
метод ActiveRecord, который использует SELECT COUNT для извлечения количества записей в ассоциации, не прибегая к JOIN, что может быть дорогостоящим (в зависимости от случая).
Смотрите https://github.com/manastech/includes-count
Надеюсь, поможет!