activerecord has_many: через find с одним вызовом sql - PullRequest
4 голосов
/ 09 ноября 2009

У меня есть эти 3 модели:

class User < ActiveRecord::Base
  has_many :permissions, :dependent => :destroy
  has_many :roles, :through => :permissions
end

class Permission < ActiveRecord::Base
  belongs_to :role
  belongs_to :user
end
class Role < ActiveRecord::Base
  has_many :permissions, :dependent => :destroy
  has_many :users, :through => :permissions
end

Я хочу найти пользователя и его роли в одном SQL-выражении, но я не могу этого достичь:

Следующее утверждение:

user = User.find_by_id(x, :include => :roles)

Дает мне следующие запросы:

  User Load (1.2ms)   SELECT * FROM `users` WHERE (`users`.`id` = 1) LIMIT 1
  Permission Load (0.8ms)   SELECT `permissions`.* FROM `permissions` WHERE (`permissions`.user_id = 1) 
  Role Load (0.8ms)   SELECT * FROM `roles` WHERE (`roles`.`id` IN (2,1)) 

Не совсем идеально. Как мне сделать это так, чтобы он выполнял один SQL-запрос с соединениями и загружал роли пользователя в память, говоря:

user.roles

не выдает новый SQL-запрос

Ответы [ 3 ]

5 голосов
/ 09 ноября 2009

Как отметил Дэмиен, если вы действительно хотите один запрос каждый раз, вы должны использовать join.

Но вы, возможно, не захотите ни одного вызова SQL. Вот почему (с здесь ):

Оптимизированная готовая загрузка


Давайте посмотрим на это:

Post.find(:all, :include => [:comments])

До Rails 2.0 мы видели что-то вроде следующего SQL-запроса в журнале:

SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `comments`.`id` AS t1_r0, `comments`.`body` AS t1_r1 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id 

Но теперь в Rails 2.1 одна и та же команда доставит разные SQL-запросы. На самом деле как минимум 2, а не 1. «И как это может быть улучшением?» Давайте посмотрим на сгенерированные запросы SQL:

SELECT `posts`.`id`, `posts`.`title`, `posts`.`body` FROM `posts` 

SELECT `comments`.`id`, `comments`.`body` FROM `comments` WHERE (`comments`.post_id IN (130049073,226779025,269986261,921194568,972244995))

Ключевое слово :include для Eager Loading было реализовано для решения страшной проблемы 1 + N. Эта проблема возникает, когда у вас есть ассоциации, затем вы загружаете родительский объект и начинаете загружать одну ассоциацию за раз, таким образом, проблема 1 + N. Если ваш родительский объект имеет 100 дочерних объектов, вы выполняете 101 запрос, что не очень хорошо. Один из способов оптимизировать это - объединить все, используя предложение OUTER JOIN в SQL, чтобы родительский и дочерний объекты загружались одновременно в одном запросе.

Казалось, хорошая идея и на самом деле все еще. Но для некоторых ситуаций внешнее соединение монстров становится медленнее, чем многие меньшие запросы. Шло много дискуссий, и вы можете посмотреть детали на билеты 9640, 9497, 9560, L109.

Суть: : как правило, лучше разделить соединение монстров на более мелкие, как вы видели в приведенном выше примере. Это позволяет избежать проблемы перегрузки декартовой системы. Для непосвященных давайте запустим версию запроса с внешним соединением:

mysql> SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `comments`.`id` AS t1_r0, `comments`.`body` AS t1_r1 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id ;

+-----------+-----------------+--------+-----------+---------+
| t0_r0     | t0_r1           | t0_r2  | t1_r0     | t1_r1   |
+-----------+-----------------+--------+-----------+---------+
| 130049073 | Hello RailsConf | MyText |      NULL | NULL    | 
| 226779025 | Hello Brazil    | MyText | 816076421 | MyText5 | 
| 269986261 | Hello World     | MyText |  61594165 | MyText3 | 
| 269986261 | Hello World     | MyText | 734198955 | MyText1 | 
| 269986261 | Hello World     | MyText | 765025994 | MyText4 | 
| 269986261 | Hello World     | MyText | 777406191 | MyText2 | 
| 921194568 | Rails 2.1       | NULL   |      NULL | NULL    | 
| 972244995 | AkitaOnRails    | NULL   |      NULL | NULL    | 
+-----------+-----------------+--------+-----------+---------+
8 rows in set (0.00 sec)

Обратите внимание на это: вы видите много дупликаций в первых 3 столбцах (от t0_r0 до t0_r2)? Это столбцы модели Post, остальные - столбцы комментариев каждого сообщения. Обратите внимание, что сообщение «Hello World» было повторено 4 раза. Вот что делает соединение: родительские строки повторяются для каждого потомка. Этот конкретный пост имеет 4 комментария, поэтому он был повторен 4 раза.

Проблема в том, что это сильно бьет по Rails, потому что ему придется иметь дело с несколькими маленькими и недолговечными объектами. Боль ощущается на стороне Rails, не так сильно на стороне MySQL. Теперь сравните это с меньшими запросами:

mysql> SELECT `posts`.`id`, `posts`.`title`, `posts`.`body` FROM `posts` ;
+-----------+-----------------+--------+
| id        | title           | body   |
+-----------+-----------------+--------+
| 130049073 | Hello RailsConf | MyText | 
| 226779025 | Hello Brazil    | MyText | 
| 269986261 | Hello World     | MyText | 
| 921194568 | Rails 2.1       | NULL   | 
| 972244995 | AkitaOnRails    | NULL   | 
+-----------+-----------------+--------+
5 rows in set (0.00 sec)

mysql> SELECT `comments`.`id`, `comments`.`body` FROM `comments` WHERE (`comments`.post_id IN (130049073,226779025,269986261,921194568,972244995));
+-----------+---------+
| id        | body    |
+-----------+---------+
|  61594165 | MyText3 | 
| 734198955 | MyText1 | 
| 765025994 | MyText4 | 
| 777406191 | MyText2 | 
| 816076421 | MyText5 | 
+-----------+---------+
5 rows in set (0.00 sec)

На самом деле я немного обманываю, я вручную удалил поля create_at и updated_at из всех вышеупомянутых запросов, чтобы вы поняли это немного яснее. Итак, вот оно: набор результатов сообщений, разделенных и не дублированных, и набор результатов комментариев того же размера, что и раньше. Чем длиннее и сложнее результирующий набор, тем больше это имеет значение, поскольку с большим количеством объектов придется столкнуться Rails. Выделение и освобождение нескольких сотен или тысяч маленьких дублированных объектов никогда не бывает выгодным.

Но эта новая функция умна. Допустим, вы хотите что-то вроде этого:

>> Post.find(:all, :include => [:comments], :conditions => ["comments.created_at > ?", 1.week.ago.to_s(:db)])

В Rails 2.1 он поймет, что для таблицы 'comments' существует условие фильтрации, поэтому он не будет разбивать его на небольшие запросы, но вместо этого он сгенерирует старую версию внешнего соединения, например:

SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `posts`.`created_at` AS t0_r3, `posts`.`updated_at` AS t0_r4, `comments`.`id` AS t1_r0, `comments`.`post_id` AS t1_r1, `comments`.`body` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id WHERE (comments.created_at > '2008-05-18 18:06:34') 

Итак, вложенные объединения, условия и т. Д. В таблицах соединений все равно должны работать нормально. В целом это должно ускорить ваши запросы. Некоторые сообщили, что из-за большего количества отдельных запросов MySQL, похоже, получает более сильный удар по процессору. Вы работаете дома и проводите стресс-тесты и тесты, чтобы увидеть, что происходит.

5 голосов
/ 09 ноября 2009

Загрузка ролей в отдельном запросе SQL на самом деле является оптимизацией под названием «Оптимизированная загрузка с нетерпением».

Role Load (0.8ms)   SELECT * FROM `roles` WHERE (`roles`.`id` IN (2,1))

(Это делается вместо загрузки каждой роли отдельно, проблема N + 1.)

Команда Rails обнаружила, что обычно быстрее использовать запрос IN с ранее найденными ассоциациями вместо большого объединения.

Объединение будет происходить только в этом запросе, если вы добавите условия в одну из других таблиц. Rails обнаружит это и сделает соединение.

Например:

User.all(:include => :roles, :conditions => "roles.name = 'Admin'")

См. оригинальный билет , этот предыдущий вопрос о переполнении стека и сообщение в блоге Фабио Акиты о Оптимизированной загрузке .

0 голосов
/ 09 ноября 2009

Включая модель загружает данные. Но делает второй запрос.
Для того, что вы хотите сделать, вы должны использовать параметр :joins.

user = User.find_by_id(x, :joins => :roles)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...