Undr

На память

Два способа ‘eager loading’ в ActiveRecord

1 Star2 Stars3 Stars4 Stars5 Stars (1 голосов, средний: 5.00 из 5)
Loading ... Loading ...

without comments

Начиная с Rails версии 2.1 в ActiveRecord существует два способа нетерпеливой загрузки (’eager loading’). Первый использует один запрос в котором связывает таблицы оператором JOIN. Второй использует по одному запросу на каждую модель. В Rails версии до 2.1 использовался только первый способ, в версиях начиная с 2.1 используются оба, но второй используется по умолчанию.

Первый способ, обладает некоторыми проблеммами. Он считается медленным, из-за необходимости обрабатывать большое количество избыточных данных (пример 1), особенно при большой вложенности ассоциаций для загрузки, и используется только если в запросе необходимо использовать поля ассоциаций (например в параметре :conditions)

Пример 1. Избыточные данные первого способа

К примеру у нас есть модели:

class Level < ActiveRecord::Base
  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
Article.find(:all, :include => :category)

Этот код до Rails версии 2.1 сгенерировал бы, примерно, вот такой запрос:

SELECT
    `articles`.*,
    `categories`.*
FROM
    `articles`  
    LEFT OUTER JOIN `categories` ON `categories`.id = `articles`.category_id

Учитывая что у статей могут быть одинаковые категории, то получаются в каждой найденной строке избыточные данные. При большей вложенности :include, например

User.find(:all, :include => {:level => [:groups], :articles => [:category, :user]})

мы получаем запросы типа этого:

SELECT
    `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 выбирает способ загрузки

Как было сказано выше, если в запросе используются поля из ассоциаций, то используется первый вариант, иначе – второй.

Вызов

Article.find(:all, :include => :category)

использует второй вариант

SELECT * FROM `articles`
SELECT * FROM `categories` WHERE (`categories`.`id` IN (1,2))

Вызов

Article.find(:all, :conditions => ["user_id = >", 1], :include => :category)

использует второй вариант

SELECT * FROM `articles` WHERE (user_id = 1)
SELECT * FROM `categories` WHERE (`categories`.`id` IN (1,2))

Но уже вызов

Article.find(:all, :conditions => ["categories.group_id = ?", 1], :include => :category)

использует первый вариант, потому что запрос зависит от таблицы categories

SELECT
    `articles`.*, `categories`.*
FROM
    `articles`
    LEFT OUTER JOIN `categories` ON `categories`.id = `articles`.category_id
WHERE
    (categories.group_id = 1)

Физически этот выбор совершается в методе find_every:

def find_every(options)
  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 используются поля из ассоциаций.

Написал undr ()

26 декабря 2009 в 19:00

Размещено в Программирование

Метки: , , , ,

Оставьте комментарий