Відповіді:
Існує кілька видів відносин багато-до-багатьох; Ви повинні задати собі такі питання:
Це залишає чотири різні можливості. Я пройдуся нижче.
Для довідки: документація по рейках з цього питання . Є розділ під назвою "Багато-багато-багато", і, звичайно, документація про самі методи класу.
Це найбільш компактний за кодом.
Почну з цієї основної схеми для ваших публікацій:
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
. Це нічим не відрізняється для гравця , який може мати багато :teams
Through , :contracts
а також (протягом такого гравця кар'єри «s). Але в цьому випадку, коли існує лише одна названа модель (наприклад, Користувач ), іменування відношення через: однаково (наприклад through: :follow
, або, як це було зроблено вище в прикладі повідомлень through: :post_connections
), призведе до зіткнення імен для різних випадків використання ( або точки доступу до таблиці приєднання. :follower_follows
і:followee_follows
були створені для уникнення такого зіткнення імен. Тепер, користувач може мати багато :followers
наскрізних :follower_follows
і багато :followees
Through :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_key
onhas_many :through
on не є необхідним, і я додав пояснення щодо використання дуже зручного:dependent
параметра дляhas_many
.