Как я могу создать отношение "многие ко многим" с той же моделью в рельсах?
Например, каждый пост связан со многими постами.
источник
Как я могу создать отношение "многие ко многим" с той же моделью в рельсах?
Например, каждый пост связан со многими постами.
Есть несколько видов отношений «многие ко многим»; вы должны задать себе следующие вопросы:
Это оставляет четыре различных возможности. Я пройдусь по ним ниже.
Для справки: документация 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
и т. Д. Это некрасиво. (Я тоже открыт для предложений. Кто угодно?)
:foreign_key
наhas_many :through
это не нужно, и я добавил объяснение о том , как использовать очень удобный:dependent
параметр дляhas_many
.Чтобы ответить на вопрос, заданный Штифом:
Двунаправленные петлевые ассоциации
Отношения последователь-подписчик между пользователями - хороший пример двунаправленной зацикленной ассоциации. У пользователя может быть много:
Вот как может выглядеть код для user.rb :
Вот как выглядит код для follow.rb :
Наиболее важные моменты, на которые следует обратить внимание, это, вероятно, термины
:follower_follows
и:followee_follows
в user.rb. Чтобы использовать обычную ассоциацию (без зацикливания) в качестве примера, у команды может быть много:players
through: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
..
источник
Вдохновленный @ Stéphan Kochen, это может сработать для двунаправленных ассоциаций.
тогда
post.posts
&&post.reversed_posts
должен работать, по крайней мере, работал у меня.источник
Для двунаправленного действия
belongs_to_and_has_many
обратитесь к уже опубликованному отличному ответу, а затем создайте еще одну ассоциацию с другим именем, внешние ключи поменяны местами и убедитесь, что выclass_name
установили указатель обратно на правильную модель. Ура.источник
Если у кого-то были проблемы с получением отличного ответа на работу, например:
или
Тогда решение - заменить
:PostConnection
на"PostConnection"
, конечно, подставив свое имя класса.источник