Как мне заставить 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

Надеюсь, поможет!

Другие вопросы по тегам