Зв'язок між багатьма з однією і тією ж моделлю в рейках?


107

Як я можу скласти відносини "багато до багатьох" з тією ж моделлю в рейках?

Наприклад, кожна публікація підключена до багатьох публікацій.

Відповіді:


276

Існує кілька видів відносин багато-до-багатьох; Ви повинні задати собі такі питання:

  • Чи хочу я зберігати додаткову інформацію в асоціації? (Додаткові поля в таблиці приєднання.)
  • Чи повинні асоціації бути неявними двонаправленими? (Якщо до пункту B підключено пост А, до пункту А. також підключено пост Б.)

Це залишає чотири різні можливості. Я пройдуся нижче.

Для довідки: документація по рейках з цього питання . Є розділ під назвою "Багато-багато-багато", і, звичайно, документація про самі методи класу.

Найпростіший сценарій, однонаправлений, без додаткових полів

Це найбільш компактний за кодом.

Почну з цієї основної схеми для ваших публікацій:

create_table "posts", :force => true do |t|
  t.string  "name", :null => false
end

Для будь-яких стосунків "багато до багатьох" вам потрібна таблиця приєднання. Ось схема для цього:

create_table "post_connections", :force => true, :id => false do |t|
  t.integer "post_a_id", :null => false
  t.integer "post_b_id", :null => false
end

За замовчуванням Rails буде називати цю таблицю комбінацією імен двох таблиць, до яких ми приєднуємось. Але це виявиться як posts_postsу цій ситуації, тому я вирішив взяти post_connectionsзамість цього.

Тут дуже важливо :id => falseпропустити idстовпець за замовчуванням . Rails хоче, щоб цей стовпець був скрізь, крім таблиць приєднання для has_and_belongs_to_many. Він буде голосно скаржитися.

Нарешті, зауважте, що імена стовпців також є нестандартними post_id, щоб запобігти конфлікту.

Тепер у вашій моделі вам просто потрібно розповісти Rails про ці пару нестандартних речей. Це виглядатиме так:

class Post < ActiveRecord::Base
  has_and_belongs_to_many(:posts,
    :join_table => "post_connections",
    :foreign_key => "post_a_id",
    :association_foreign_key => "post_b_id")
end

І це має просто працювати! Ось приклад irb сесії, що проходить через script/console:

>> a = Post.create :name => 'First post!'
=> #<Post id: 1, name: "First post!">
>> b = Post.create :name => 'Second post?'
=> #<Post id: 2, name: "Second post?">
>> c = Post.create :name => 'Definitely the third post.'
=> #<Post id: 3, name: "Definitely the third post.">
>> a.posts = [b, c]
=> [#<Post id: 2, name: "Second post?">, #<Post id: 3, name: "Definitely the third post.">]
>> b.posts
=> []
>> b.posts = [a]
=> [#<Post id: 1, name: "First post!">]

Ви побачите, що postsприєднання до асоціації створить записи в post_connectionsтаблиці, як це доречно.

Деякі речі, які слід зазначити:

  • На вищезгаданому сеансі irb можна побачити, що асоціація є однонаправленою, оскільки після цього a.posts = [b, c], результат b.postsне включає перше повідомлення.
  • Ще одна річ, яку ви, можливо, помітили, - це те, що моделі немає PostConnection. Зазвичай ви не використовуєте моделі для has_and_belongs_to_manyасоціації. З цієї причини ви не зможете отримати доступ до будь-яких додаткових полів.

Однонаправлений, з додатковими полями

Зараз ... У вас є постійний користувач, який сьогодні написав на вашому сайті публікацію про те, як вугрі смачні. Цей повний незнайомець заходить на ваш сайт, підписується та пише недоречний пост про незручність звичайного користувача. Адже вугрі - це зникаючий вид!

Отже, ви хочете, щоб у вашій базі було зрозуміло, що пост B - це лаянка на пошті А. Для цього потрібно додати categoryполе до асоціації.

Однак те , що нам потрібно , ніж довше немає has_and_belongs_to_many, але поєднання has_many, belongs_to, has_many ..., :through => ...і додаткові моделі для об'єднання таблиці. Ця додаткова модель - це те, що дає нам можливість додавати додаткову інформацію до самої асоціації.

Ось ще одна схема, дуже схожа на вищезгадану:

create_table "posts", :force => true do |t|
  t.string  "name", :null => false
end

create_table "post_connections", :force => true do |t|
  t.integer "post_a_id", :null => false
  t.integer "post_b_id", :null => false
  t.string  "category"
end

Зверніть увагу , як в цій ситуації, post_connections дійсно є idстовпець. (Там немає ні :id => false параметра.) Це необхідно, тому що там буде регулярна модель ActiveRecord для доступу до таблиці.

Почну з PostConnectionмоделі, тому що вона просто мертва:

class PostConnection < ActiveRecord::Base
  belongs_to :post_a, :class_name => :Post
  belongs_to :post_b, :class_name => :Post
end

Єдине, що тут відбувається :class_name, це те , що необхідно, тому що Rails не може зробити висновок post_aабо post_bщо ми маємо справу з повідомленням тут. Треба сказати це прямо.

Тепер Postмодель:

class Post < ActiveRecord::Base
  has_many :post_connections, :foreign_key => :post_a_id
  has_many :posts, :through => :post_connections, :source => :post_b
end

З першої has_manyасоціацією, ми говоримо моделі приєднатися post_connectionsна posts.id = post_connections.post_a_id.

З другою асоціацією ми повідомляємо Рейлам, що ми можемо дістатись до інших постів, пов'язаних з цією, через нашу першу асоціацію post_connections, а потім post_bасоціацію PostConnection.

Не вистачає ще однієї речі , і це те, що нам потрібно сказати Rails, що a PostConnectionзалежить від посади, якій він належить. Якщо один або обидва post_a_idі post_b_idбули NULL, то , що з'єднання не сказав би нам багато, чи не так? Ось як ми це робимо в нашій Postмоделі:

class Post < ActiveRecord::Base
  has_many(:post_connections, :foreign_key => :post_a_id, :dependent => :destroy)
  has_many(:reverse_post_connections, :class_name => :PostConnection,
      :foreign_key => :post_b_id, :dependent => :destroy)

  has_many :posts, :through => :post_connections, :source => :post_b
end

Окрім незначної зміни синтаксису, тут відрізняються дві реальні речі:

  • Параметр has_many :post_connectionsмає додатковий :dependentпараметр. Зі значенням :destroyми повідомляємо Рейлам, що, як тільки ця публікація зникне, вона може продовжувати і знищувати ці об’єкти. Альтернативне значення, яке ви можете використовувати тут, - :delete_allце швидше, але не зажадає жодних рушників руйнування, якщо ви їх використовуєте.
  • Ми також додали has_manyасоціацію для зворотних з’єднань, ті, що пов'язали нас post_b_id. Таким чином, Рейки можуть акуратно знищити і їх. Зауважте, що ми маємо вказати :class_nameтут, оскільки назву класу моделі вже не можна зробити з :reverse_post_connections.

З цим на місці, я пропоную вам ще один сеанс irb через script/console:

>> a = Post.create :name => 'Eels are delicious!'
=> #<Post id: 16, name: "Eels are delicious!">
>> b = Post.create :name => 'You insensitive cloth!'
=> #<Post id: 17, name: "You insensitive cloth!">
>> b.posts = [a]
=> [#<Post id: 16, name: "Eels are delicious!">]
>> b.post_connections
=> [#<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>]
>> connection = b.post_connections[0]
=> #<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>
>> connection.category = "scolding"
=> "scolding"
>> connection.save!
=> true

Замість того, щоб створювати асоціацію, а потім встановлювати категорію окремо, ви також можете просто створити PostConnection і зробити це з ним:

>> b.posts = []
=> []
>> PostConnection.create(
?>   :post_a => b, :post_b => a,
?>   :category => "scolding"
>> )
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> b.posts(true)  # 'true' means force a reload
=> [#<Post id: 16, name: "Eels are delicious!">]

І ми також можемо маніпулювати post_connectionsі reverse_post_connectionsасоціаціями; це буде чітко відображено в postsасоціації:

>> a.reverse_post_connections
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> a.reverse_post_connections = []
=> []
>> b.posts(true)  # 'true' means force a reload
=> []

Двонаправлені петельні асоціації

У звичайних has_and_belongs_to_manyасоціаціях асоціація визначається в обох задіяних моделях. А асоціація двостороння.

Але в цій справі є лише одна модель Посту. А асоціація вказується лише один раз. Саме тому в цьому конкретному випадку асоціації є однонаправленими.

Те саме стосується альтернативного методу з has_manyта моделлю таблиці приєднання.

Це найкраще видно при простому зверненні до асоціацій з irb та перегляді SQL, який Rails створює у файлі журналу. Ви знайдете щось таке:

SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )

Щоб зробити асоціацію двонаправленою, нам доведеться знайти спосіб зробити Rails ORвищезазначеними умовами post_a_idі post_b_idповернути їх назад, так що це буде виглядати в обох напрямках.

На жаль, єдиний спосіб зробити це, про що я знаю, - досить хакі. Вам доведеться вручну вказати SQL з допомогою опції , has_and_belongs_to_manyтакі як :finder_sql, :delete_sqlі т.д. Це не красиво. (Я також відкритий для пропозицій тут. Хтось?)


Дякуємо за приємні коментарі! :) Я вніс ще кілька змін. Зокрема, значення :foreign_keyon has_many :throughon не є необхідним, і я додав пояснення щодо використання дуже зручного :dependentпараметра для has_many.
Stéphan Kochen

@ Shtééf навіть масове призначення (update_attributes) не спрацює у випадку двонаправлених асоціацій, наприклад: postA.update_attributes ({: post_b_ids => [2,3,4]}) будь-яка ідея чи обхідні шляхи?
Lohith MV

Дуже приємна відповідь, товариш 5 разів {ставить "+1"}
Рахул

@ Shteéf Я багато чого навчився з цієї відповіді, дякую! Я спробував запитати і відповісти на ваш двонаправлений питання про асоціації тут: stackoverflow.com/questions/25493368 / ...
jbmilgrom

17

Щоб відповісти на запитання Штефа:

Двонаправлені петельні асоціації

Відносини між послідовником та послідовником серед Користувачів є хорошим прикладом двонаправленої петельної асоціації. Користувач може мати багато:

  • послідовники в якості послідовника
  • послідовники в якості послідовника.

Ось як може виглядати код для user.rb :

class User < ActiveRecord::Base
  # follower_follows "names" the Follow join table for accessing through the follower association
  has_many :follower_follows, foreign_key: :followee_id, class_name: "Follow" 
  # source: :follower matches with the belong_to :follower identification in the Follow model 
  has_many :followers, through: :follower_follows, source: :follower

  # followee_follows "names" the Follow join table for accessing through the followee association
  has_many :followee_follows, foreign_key: :follower_id, class_name: "Follow"    
  # source: :followee matches with the belong_to :followee identification in the Follow model   
  has_many :followees, through: :followee_follows, source: :followee
end

Ось як код для follow.rb :

class Follow < ActiveRecord::Base
  belongs_to :follower, foreign_key: "follower_id", class_name: "User"
  belongs_to :followee, foreign_key: "followee_id", class_name: "User"
end

Найважливіші речі, які слід зазначити, - це, мабуть, терміни :follower_followsта :followee_followsв user.rb. Щоб використовувати запуск об'єднання фрези (без циклу) як приклад, у команди може бути багато: playersнаскрізний :contracts. Це нічим не відрізняється для гравця , який може мати багато :teamsThrough , :contractsа також (протягом такого гравця кар'єри «s). Але в цьому випадку, коли існує лише одна названа модель (наприклад, Користувач ), іменування відношення через: однаково (наприклад through: :follow, або, як це було зроблено вище в прикладі повідомлень through: :post_connections), призведе до зіткнення імен для різних випадків використання ( або точки доступу до таблиці приєднання. :follower_followsі:followee_followsбули створені для уникнення такого зіткнення імен. Тепер, користувач може мати багато :followersнаскрізних :follower_followsі багато :followeesThrough :followee_follows.

Щоб визначити користувача : followees (після @user.followeesвиклику в базу даних), Rails тепер може переглядати кожен екземпляр class_name: "Follow", де такий Користувач є послідовником (тобто foreign_key: :follower_id) через: такого користувача : followee_follow. Щоб визначити послідовників Користувача : (після @user.followersвиклику до бази даних), Rails тепер може переглядати кожен екземпляр class_name: "Дотримуйтесь", де таким Користувачем є підписант (тобто foreign_key: :followee_id) через: такий Користувач : follower_follow.


1
Саме те, що мені було потрібно! Дякую! (Рекомендую також перерахувати міграцію бази даних; мені довелося отримати цю інформацію з прийнятої відповіді)
Адам Денон

6

Якщо хтось приїхав сюди, щоб спробувати дізнатися, як створити стосунки з друзями в Rails, я б посилав їх на те, що я нарешті вирішив використати, а саме скопіювати те, що зробив 'Community Engine'.

Ви можете звернутися до:

https://github.com/bborn/communityengine/blob/master/app/models/friendship.rb

і

https://github.com/bborn/communityengine/blob/master/app/models/user.rb

для отримання додаткової інформації.

TL; DR

# user.rb
has_many :friendships, :foreign_key => "user_id", :dependent => :destroy
has_many :occurances_as_friend, :class_name => "Friendship", :foreign_key => "friend_id", :dependent => :destroy

..

# friendship.rb
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"

2

Натхненний @ Stéphan Kochen, це може працювати для двосторонніх асоціацій

class Post < ActiveRecord::Base
  has_and_belongs_to_many(:posts,
    :join_table => "post_connections",
    :foreign_key => "post_a_id",
    :association_foreign_key => "post_b_id")

  has_and_belongs_to_many(:reversed_posts,
    :class_name => Post,
    :join_table => "post_connections",
    :foreign_key => "post_b_id",
    :association_foreign_key => "post_a_id")
 end

то post.posts& & post.reversed_postsповинні обидва працювати, принаймні працювали для мене.


1

Для двонаправлених даних belongs_to_and_has_manyзверніться до вже опублікованої великої відповіді, а потім створіть іншу асоціацію з іншим іменем, зовнішні клавіші перевернуті і переконайтеся, що ви class_nameвстановили, щоб повернути правильну модель. Ура.


2
Чи можете ви показати свій приклад у своєму дописі? Я спробував кілька способів, як ви запропонували, але, здається, це не вдалося.
achabacha322

0

Якщо у когось виникли проблеми з отриманням відмінної відповіді на роботу, наприклад:

(Об'єкт не підтримує #inspect)
=>

або

NoMethodError: невизначений метод `split 'для: Mission: Symbol

Тоді рішення замінити :PostConnectionз "PostConnection", підставляючи своє ім'я класу, звичайно.

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