Отношения многие ко многим с одной и той же моделью в рельсах?
Как я могу установить отношения "многие ко многим" с одной и той же моделью в рельсах?
Например, каждый пост связан со многими постами.
6 ответов
Есть несколько видов отношений "многие ко многим"; Вы должны задать себе следующие вопросы:
- Хочу ли я хранить дополнительную информацию в ассоциации? (Дополнительные поля в таблице соединений.)
- Должны ли ассоциации быть неявно двунаправленными? (Если пост A связан с постом B, то пост B также связан с постом A.)
Это оставляет четыре разные возможности. Я пройдусь по этим ниже.
Для справки: документация по теме на 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
стол в зависимости от обстоятельств.
Некоторые вещи на заметку:
- В приведенном выше сеансе 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
,
Со второй ассоциацией мы сообщаем Rails, что мы можем получить доступ к другим постам, связанным с этой, через нашу первую ассоциацию. post_connections
с последующим post_b
ассоциация PostConnection
,
Там просто еще одна вещь отсутствует, и это то, что мы должны сказать Rails, что 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. Чтобы использовать в качестве примера прогон мельничной (не зацикленной) ассоциации, команда может иметь много:players
через :contracts
, Это не отличается для игрока, который может иметь много :teams
через :contracts
также (в течение карьеры такого игрока). Но в этом случае, когда существует только одна именованная модель (т. Е. Пользователь), тождественно присваивается отношение through: (например, through: :follow
или, как это было сделано выше в примере сообщений, through: :post_connections
) приведет к конфликту именования для разных случаев использования (или точек доступа в) таблицы соединений. :follower_follows
а также :followee_follows
были созданы, чтобы избежать такого именования. Теперь пользователь может иметь много :followers
через :follower_follows
и много :followees
через :followee_follows
,
Чтобы определить пользователя: подписчики (после @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"
Подставляя ваше имя класса, конечно.