Ответы:
Есть несколько видов отношений «многие ко многим»; вы должны задать себе следующие вопросы:
Это оставляет четыре различных возможности. Я пройдусь по ним ниже.
Для справки: документация Rails по этой теме . Есть раздел под названием «Многие-ко-многим» и, конечно же, документация по самим методам класса.
Это самый компактный код.
Я начну с этой базовой схемы ваших сообщений:
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 - это брань против поста A. Для этого вы хотите добавить 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.
Со второй ассоциацией мы говорим Rails, что можем достичь других сообщений, связанных с этим, через нашу первую ассоциацию. 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 мы говорим Rails, что, как только этот пост исчезнет, он может продолжить и уничтожить эти объекты. Альтернативное значение, которое вы можете использовать здесь, - :delete_allэто быстрее, но не будет вызывать никаких обработчиков уничтожения, если вы их используете.has_manyассоциацию для обратных связей, через которые мы связались post_b_id. Таким образом, Rails может аккуратно уничтожить и их. Обратите внимание, что мы должны указать :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ассоциациях ассоциация определяется как в задействованных моделях. И ассоциация двунаправленная.
Но в данном случае есть только одна модель Post. И ассоциация указывается только один раз. Именно поэтому в данном конкретном случае ассоциации однонаправлены.
То же самое верно для альтернативного метода 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. Чтобы использовать обычную ассоциацию (без зацикливания) в качестве примера, у команды может быть много: playersthrough :contracts. Это ничем не отличается от игрока , у которого тоже может быть много :teamsрезультатов :contracts(в течение карьеры такого игрока ). Но в этом случае, когда существует только одна именованная модель (например, Пользователь ), идентичное наименование отношения through: (например through: :follow, или, как это было сделано выше в примере сообщений through: :post_connections) приведет к конфликту имен для разных вариантов использования ( или точки доступа в) таблицу соединений. были созданы, чтобы избежать такого конфликта имен. Теперь:follower_followsи:followee_followsУ пользователя может быть много :followersсквозных :follower_followsи много :followeesсквозных :followee_follows.
Для определения пользователя «s: followees (при в @user.followeesобращении к базе данных), Rails может выглядеть на каждом экземпляре class_name:„Follow“ , где такой Пользователь является последователем (то есть foreign_key: :follower_id) через: такого пользователя «ы: followee_follows. Чтобы определить количество подписчиков пользователя : (при @user.followersобращении к базе данных), Rails теперь может просматривать каждый экземпляр class_name: «Follow», где такой пользователь является последующим (т. Е. foreign_key: :followee_idЧерез: таким пользователем : follower_follows).
Если бы кто-нибудь пришел сюда, чтобы попытаться узнать, как создавать дружеские отношения в 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наhas_many :throughэто не нужно, и я добавил объяснение о том , как использовать очень удобный:dependentпараметр дляhas_many.