Рейки: включати vs.: приєднується


345

Це скоріше питання "чому все працює таким чином", а не питання "я не знаю, як це зробити" ...

Тож євангелію про витягнення пов’язаних записів, які ви знаєте, що збираєтесь використовувати, це використовувати, :includeтому що ви отримаєте приєднання та уникнете цілого ряду зайвих запитів:

Post.all(:include => :comments)

Однак, дивлячись на журнали, жодного приєднання не відбувається:

Post Load (3.7ms)   SELECT * FROM "posts"
Comment Load (0.2ms)   SELECT "comments.*" FROM "comments" 
                       WHERE ("comments".post_id IN (1,2,3,4)) 
                       ORDER BY created_at asc) 

Він бере ярлик, оскільки він одразу отримує всі коментарі, але це все ще не є об'єднанням (про що, як видається, йдеться у всій документації). Єдиний спосіб я можу отримати приєднання - це використовувати :joinsзамість :include:

Post.all(:joins => :comments)

І журнали показують:

Post Load (6.0ms)  SELECT "posts".* FROM "posts" 
                   INNER JOIN "comments" ON "posts".id = "comments".post_id

Я щось пропускаю? У мене є додаток з півдесятка асоціацій, і на одному екрані я показую дані з усіх них. Здається, було б краще мати один запит, що приєднався, замість 6 осіб. Я знаю, що на користь продуктивності не завжди краще робити приєднання, а не окремі запити (адже якщо ви збираєтесь витрачений час, то схоже, що два окремі запити вище швидше, ніж з'єднання), але зрештою, документи Я читав, я здивований, бачу, що :includeне працює так, як рекламується.

Може бути , Rails є усвідомлюють проблеми продуктивності і не приєднується , за винятком деяких випадків?


3
якщо ви використовували старішу версію Rails, будь ласка, вкажіть це за допомогою тегів або в своєму запитальному органі. В іншому випадку, якщо ви використовуєте Rails 4 ЗАРАЗ, це includes(для тих, хто це читає)
onebree

Також в даний час: преднагрузки і: eager_load blog.bigbinary.com/2013/07/01 / ...
CJW

Відповіді:


179

Здається, що :includeфункціональність була змінена за допомогою Rails 2.1. Рейки використовувались у всіх випадках, але з міркувань продуктивності було змінено на використання декількох запитів за певних обставин. У цій публікації в блозі Фабіо Акіти є хороша інформація про зміни (див. Розділ «Оптимізоване швидке завантаження»).



Це дуже корисно, дякую. Хоча хотілося б, щоб був спосіб змусити Рейлів зробити приєднання навіть без того, де цього вимагає. У деяких випадках ви знаєте, що приєднання буде більш ефективним і не несе ризику дублювання.
Джонатан Суорц

1

@JonathanSwartz Схоже, що нова версія Rails підтримує це за допомогою нетерпіння . Дякую за посилання NathanLong
rubyprince

92

.joinsпросто приєднається до таблиць і принесе вибрані поля натомість. якщо ви викликаєте асоціації за результатами запиту приєднання, він знову запустить запити бази даних

:includesнетерпляче завантажить включені асоціації та додасть їх у пам'ять. :includesзавантажує всі включені атрибути таблиць. Якщо ви викликаєте асоціації за результатом включення запиту, це не запустить жодних запитів


71

Різниця між приєднанням і включенням полягає в тому, що використання оператора include генерує набагато більшу кількість запитів SQL, завантажуючи в пам'ять всі атрибути з інших таблиць (ів).

Наприклад, якщо у вас таблиця з коментарями, і ви використовуєте: приєднується => користувачі, щоб отримати всю інформацію про користувачів для цілей сортування тощо, це буде добре працювати та займе менше часу, ніж: включити, але сказати, що ви хочете показати коментар разом із іменем користувачів, електронною поштою тощо. Щоб отримати інформацію за допомогою: приєднується, доведеться робити окремі запити SQL для кожного користувача, який він отримує, тоді як якщо ви використовували: включіть цю інформацію, вона готова до використання.

Чудовий приклад:

http://railscasts.com/episodes/181-include-vs-joins


55

Нещодавно я читав більше про різницю між рейками :joinsта :includesв них. Ось пояснення того, що я зрозумів (із прикладами :))

Розглянемо цей сценарій:

  • Користувач has_many коментарі та коментар належить користувачеві.

  • Модель користувача має такі атрибути: ім'я (рядок), вік (ціле число). Модель коментаря має такі атрибути: Content, user_id. Для коментаря user_id може бути недійсним.

Приєднується:

: join виконує внутрішнє з'єднання між двома таблицями. Таким чином

Comment.joins(:user)

#=> <ActiveRecord::Relation [#<Comment id: 1, content: "Hi I am Aaditi.This is my first   comment!", user_id: 1, created_at: "2014-11-12 18:29:24", updated_at: "2014-11-12 18:29:24">, 
     #<Comment id: 2, content: "Hi I am Ankita.This is my first comment!", user_id: 2, created_at: "2014-11-12 18:29:29", updated_at: "2014-11-12 18:29:29">,    
     #<Comment id: 3, content: "Hi I am John.This is my first comment!", user_id: 3, created_at: "2014-11-12 18:30:25", updated_at: "2014-11-12 18:30:25">]>

отримає всі записи, де user_id (таблиця коментарів) дорівнює user.id (таблиця користувачів). Таким чином, якщо ви робите

Comment.joins(:user).where("comments.user_id is null")

#=> <ActiveRecord::Relation []>

Ви отримаєте порожній масив, як показано.

Більше того, приєднання не завантажує об'єднану таблицю в пам'ять. Таким чином, якщо ви робите

comment_1 = Comment.joins(:user).first

comment_1.user.age
#=>←[1m←[36mUser Load (0.0ms)←[0m  ←[1mSELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT 1←[0m  [["id", 1]]
#=> 24

Як бачите, comment_1.user.ageзапит на базу даних буде знову запущено у фоновому режимі, щоб отримати результати

Включає:

: включає виконує ліве зовнішнє з'єднання між двома таблицями. Таким чином

Comment.includes(:user)

#=><ActiveRecord::Relation [#<Comment id: 1, content: "Hi I am Aaditi.This is my first comment!", user_id: 1, created_at: "2014-11-12 18:29:24", updated_at: "2014-11-12 18:29:24">,
   #<Comment id: 2, content: "Hi I am Ankita.This is my first comment!", user_id: 2, created_at: "2014-11-12 18:29:29", updated_at: "2014-11-12 18:29:29">,
   #<Comment id: 3, content: "Hi I am John.This is my first comment!", user_id: 3, created_at: "2014-11-12 18:30:25", updated_at: "2014-11-12 18:30:25">,    
   #<Comment id: 4, content: "Hi This is an anonymous comment!", user_id: nil, created_at: "2014-11-12 18:31:02", updated_at: "2014-11-12 18:31:02">]>

призведе до об'єднаної таблиці з усіма записами з таблиці коментарів. Таким чином, якщо ви робите

Comment.includes(:user).where("comment.user_id is null")
#=> #<ActiveRecord::Relation [#<Comment id: 4, content: "Hi This is an anonymous comment!", user_id: nil, created_at: "2014-11-12 18:31:02", updated_at: "2014-11-12 18:31:02">]>

він буде отримувати записи, де comments.user_id дорівнює нулю, як показано.

Крім того, включає завантаження обох таблиць у пам'ять. Таким чином, якщо ви робите

comment_1 = Comment.includes(:user).first

comment_1.user.age
#=> 24

Як ви можете помітити comment_1.user.age просто завантажує результат з пам'яті, не запускаючи запит бази даних у фоновому режимі.


Це для Rails 4?
onebree

@HunterStevens: Так, це
Аадіті Джайн

54

Крім міркувань щодо продуктивності, є і функціональна різниця. Коли ви приєднуєтесь до коментарів, ви запитуєте публікації, які мають коментарі - за замовчуванням внутрішнє приєднання. Коли ви додаєте коментарі, ви просите про всі публікації - зовнішнє приєднання.


10

тл; д-р

Я протиставляю їх двома способами:

приєднується - для умовного вибору записів.

включає - При використанні асоціації для кожного члена набору результатів.

Більш довга версія

Приєднання призначене для фільтрації набору результатів, що надходить із бази даних. Ви використовуєте його для встановлення операцій на вашому столі. Розгляньте це як пункт де, який виконує теорію множин.

Post.joins(:comments)

те саме, що

Post.where('id in (select post_id from comments)')

За винятком того, що якщо буде більше одного коментаря, ви отримаєте повторювані повідомлення з приєднанням. Але кожен пост буде публікацією, в якій є коментарі. Ви можете виправити це виразно:

Post.joins(:comments).count
=> 10
Post.joins(:comments).distinct.count
=> 2

У контракті includesметод просто переконається у відсутності додаткових запитів до бази даних при посиланні на відношення (щоб ми не робили n + 1 запитів)

Post.includes(:comments).count
=> 4 # includes posts without comments so the count might be higher.

Мораль полягає в тому, щоб використовувати, joinsколи ви хочете робити умовні задані операції, і використовувати, includesколи ви збираєтесь використовувати відношення до кожного члена колекції.


Це distinctотримує мене щоразу. Дякую!
Бен Халл

4

.joins працює як приєднання до бази даних, і вона приєднується до двох або більше таблиць і отримує вибрані дані з бекенда (бази даних).

.включає роботу як ліве з'єднання бази даних. Він завантажив усі записи лівої сторони, не має актуальності моделі правого боку. Він використовується для прагнення до завантаження, оскільки він завантажує всі пов'язані об'єкти в пам'ять. Якщо ми викликаємо асоціації за включенням результату запиту, то він не запускає запит на базу даних, він просто повертає дані з пам'яті, оскільки він вже завантажив дані в пам'ять.


0

'приєднується', щойно використовується для приєднання до таблиць, і коли ви викликаєте асоціації при приєднанні, він знову запустить запит (це означає, що багато запитів запуститься)

lets suppose you have tow model, User and Organisation
User has_many organisations
suppose you have 10 organisation for a user 
@records= User.joins(:organisations).where("organisations.user_id = 1")
QUERY will be 
 select * from users INNER JOIN organisations ON organisations.user_id = users.id where organisations.user_id = 1

it will return all records of organisation related to user
and @records.map{|u|u.organisation.name}
it run QUERY like 
select * from organisations where organisations.id = x then time(hwo many organisation you have)

загальна кількість SQL в цьому випадку становить 11

Але з "включено" буде охоче завантажувати включені асоціації та додавати їх у пам'ять (завантажувати всі асоціації при першому завантаженні) і не запускати запит знову

коли ви отримуєте записи з такими елементами, як @ records = User.includes (: organization) .where ("organisations.user_id = 1"), запит буде

select * from users INNER JOIN organisations ON organisations.user_id = users.id where organisations.user_id = 1
and 


 select * from organisations where organisations.id IN(IDS of organisation(1, to 10)) if 10 organisation
and when you run this 

@ records.map {| u | u.organisation.name} запит не запускається

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.