Два способа ‘eager loading’ в ActiveRecord
Начиная с Rails версии 2.1 в ActiveRecord существует два способа нетерпеливой загрузки (’eager loading’). Первый использует один запрос в котором связывает таблицы оператором JOIN. Второй использует по одному запросу на каждую модель. В Rails версии до 2.1 использовался только первый способ, в версиях начиная с 2.1 используются оба, но второй используется по умолчанию.
Первый способ, обладает некоторыми проблеммами. Он считается медленным, из-за необходимости обрабатывать большое количество избыточных данных (пример 1), особенно при большой вложенности ассоциаций для загрузки, и используется только если в запросе необходимо использовать поля ассоциаций (например в параметре :conditions)
Пример 1. Избыточные данные первого способа
К примеру у нас есть модели:
has_many :groups
has_many :users
end
class Group < ActiveRecord::Base
belongs_to :level
has_many :users
has_many :categories
end
class Category < ActiveRecord::Base
belongs_to :group
has_many :articles
end
class User < ActiveRecord::Base
belongs_to :group
belongs_to :level
has_many :articles
end
class Article < ActiveRecord::Base
belongs_to :category
belongs_to :user
end
Этот код до Rails версии 2.1 сгенерировал бы, примерно, вот такой запрос:
`articles`.*,
`categories`.*
FROM
`articles`
LEFT OUTER JOIN `categories` ON `categories`.id = `articles`.category_id
Учитывая что у статей могут быть одинаковые категории, то получаются в каждой найденной строке избыточные данные. При большей вложенности :include, например
мы получаем запросы типа этого:
`users`.*,
`articles`.*,
`categories`.*,
`users_articles`.*,
`levels`.*,
`groups`.*
FROM
`users`
LEFT OUTER JOIN `articles` ON articles.user_id = users.id
LEFT OUTER JOIN `categories` ON `categories`.id = `articles`.category_id
LEFT OUTER JOIN `users` users_articles ON `users_articles`.id = `articles`.user_id
LEFT OUTER JOIN `levels` ON `levels`.id = `users`.level_id
LEFT OUTER JOIN `groups` ON groups.level_id = levels.id
Тут избыточные данные содержат все поля таблиц кроме articles. Соответственно обработка этих данных отжирает процессорное время и память. В таком случае быстрее должен работать второй вариант нетерпеливой загрузки.
Пример 2. Как Rails выбирает способ загрузки
Как было сказано выше, если в запросе используются поля из ассоциаций, то используется первый вариант, иначе – второй.
Вызов
использует второй вариант
SELECT * FROM `categories` WHERE (`categories`.`id` IN (1,2))
Вызов
использует второй вариант
SELECT * FROM `categories` WHERE (`categories`.`id` IN (1,2))
Но уже вызов
использует первый вариант, потому что запрос зависит от таблицы categories
`articles`.*, `categories`.*
FROM
`articles`
LEFT OUTER JOIN `categories` ON `categories`.id = `articles`.category_id
WHERE
(categories.group_id = 1)
Физически этот выбор совершается в методе find_every:
include_associations = merge_includes(scope(:find, :include), options[:include])
if include_associations.any? && references_eager_loaded_tables?(options)
records = find_with_associations(options)
else
records = find_by_sql(construct_finder_sql(options))
if include_associations.any?
preload_associations(records, include_associations)
end
end
records.each { |record| record.readonly! } if options[:readonly]
records
end
Вызов метода references_eager_loaded_tables?(options) возвращает true, если в :conditions, :order или :select используются поля из ассоциаций.
