Архив рубрики: AR

Rails preload, eager_load, includes и joins.

http://habrahabr.ru/post/191762/

http://sqlfiddle.com/#!2/659f69/1

Rails предоставляют нам 4 различных способа загрузки ассоциаций: preload, eager_load, includes и joins. Рассмотрим каждый из них:

Preload


Этот метод загружает ассоциации в отдельном запросе:

User.preload(:posts).to_a

# =>
SELECT "users".* FROM "users"
SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" IN (1)


Т.к. preload всегда создает два отдельных запроса, то мы не можем использовать таблицу posts в условии выборки:

User.preload(:posts).where("posts.desc='ruby is awesome'")

# =>
SQLite3::SQLException: no such column: posts.desc:
SELECT "users".* FROM "users"  WHERE (posts.desc='ruby is awesome')


А таблицу users – можем:

User.preload(:posts).where("users.name='Neeraj'")

# =>
SELECT "users".* FROM "users"  WHERE (users.name='Neeraj')
SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" IN (3)




Includes


По умолчанию includes действует точно так же, как и preload, но в случае наличия условия по ассоциированной таблице переключается на создание единственного запроса с LEFT OUTER JOIN.

User.includes(:posts).where('posts.desc = "ruby is awesome"').to_a

# =>
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "posts"."id" AS t1_r0,
       "posts"."title" AS t1_r1,
       "posts"."user_id" AS t1_r2, "posts"."desc" AS t1_r3
FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
WHERE (posts.desc = "ruby is awesome")


Если по каким-то причинам необходимо форсировать применение такого подхода, то можно использовать метод references:

User.includes(:posts).references(:posts).to_a

# =>
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "posts"."id" AS t1_r0,
       "posts"."title" AS t1_r1,
       "posts"."user_id" AS t1_r2, "posts"."desc" AS t1_r3
FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"



Eager_load


Этот метод загружает ассоциации в одном запросе с использованием Left Outer Join, точно так же, как действует includes в сочетании с references.

User.eager_load(:posts).to_a

# =>
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "posts"."id" AS t1_r0,
       "posts"."title" AS t1_r1, "posts"."user_id" AS t1_r2, "posts"."desc" AS t1_r3
FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"



Joins


Создает запрос с использованием INNER JOIN. 

User.joins(:posts)

# =>
SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"


При этом, загружаются данные только из таблицы users. Кроме того, этот запрос может возвратить дублирующие друг друга записи:

def self.setup
  User.delete_all
  Post.delete_all

  u = User.create name: 'Neeraj'
  u.posts.create! title: 'ruby', desc: 'ruby is awesome'
  u.posts.create! title: 'rails', desc: 'rails is awesome'
  u.posts.create! title: 'JavaScript', desc: 'JavaScript is awesome'

  u = User.create name: 'Neil'
  u.posts.create! title: 'JavaScript', desc: 'Javascript is awesome'

  u = User.create name: 'Trisha'
end



Результат выполнения User.joins(:posts) в БД с такими данными:

#<User id: 9, name: "Neeraj">
#<User id: 9, name: "Neeraj">
#<User id: 9, name: "Neeraj">
#<User id: 10, name: "Neil">


Избежать повторений мы можем с использованием distinct:

User.joins(:posts).select('distinct users.*').to_a
# в комментариях подсказывают, что можно сделать проще:
User.joins(:posts).uniq


Если же мы хотим дополнительно получить какие-либо данные из таблицы posts, мы должны внести их в предложение select:

records = User.joins(:posts).select('distinct users.*, posts.title as posts_title').to_a
records.each do |user|
  puts user.name
  puts user.posts_title
end


Стоит заметить, что после выполнения метода joins вызов user.posts приведет в созданию еще одного запроса.

Методы eager нужны, чтобы избежать появления так называемых запросов N+1, и не стоит их использовать для подключения ассоциаций в where и order, для этого есть joins. Рекомендую гем bullet (https://github.com/flyerhzm/bullet), чтобы найти запросы в которых нужна eager-загрузка.

Плохо, когда появляются похожие штуки: ‘posts’.’user_id’ AS t1_r2, ‘posts’.’desc’ AS t1_r3, этим грешит includes особенно при сортировке через ассоциацию, даже если она подключена через joins (вроде в 4 исправили). И во многих случаях to_a не актуально.

Алсо, гем squeel (https://github.com/ernie/squeel) очень упрощает написание сложных запросов.