Відповіді:
Існує кілька видів відносин багато-до-багатьох; Ви повинні задати собі такі питання:
Це залишає чотири різні можливості. Я пройдуся нижче.
Для довідки: документація по рейках з цього питання . Є розділ під назвою "Багато-багато-багато", і, звичайно, документація про самі методи класу.
Це найбільш компактний за кодом.
Почну з цієї основної схеми для ваших публікацій:
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таблиці, як це доречно.
Деякі речі, які слід зазначити:
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і т.д. Це не красиво. (Я також відкритий для пропозицій тут. Хтось?)
Щоб відповісти на запитання Штефа:
Відносини між послідовником та послідовником серед Користувачів є хорошим прикладом двонаправленої петельної асоціації. Користувач може мати багато:
Ось як може виглядати код для 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.
Якщо хтось приїхав сюди, щоб спробувати дізнатися, як створити стосунки з друзями в 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"
Натхненний @ 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повинні обидва працювати, принаймні працювали для мене.
Для двонаправлених даних belongs_to_and_has_manyзверніться до вже опублікованої великої відповіді, а потім створіть іншу асоціацію з іншим іменем, зовнішні клавіші перевернуті і переконайтеся, що ви class_nameвстановили, щоб повернути правильну модель. Ура.
Якщо у когось виникли проблеми з отриманням відмінної відповіді на роботу, наприклад:
(Об'єкт не підтримує #inspect)
=>
або
NoMethodError: невизначений метод `split 'для: Mission: Symbol
Тоді рішення замінити :PostConnectionз "PostConnection", підставляючи своє ім'я класу, звичайно.
:foreign_keyonhas_many :throughon не є необхідним, і я додав пояснення щодо використання дуже зручного:dependentпараметра дляhas_many.