Отношения многие ко многим с одной и той же моделью в рельсах? - PullRequest
101 голосов
/ 30 января 2010

Как я могу установить отношения «многие ко многим» с одной и той же моделью в рельсах?

Например, каждый пост связан со многими постами.

Ответы [ 6 ]

264 голосов
/ 30 января 2010

Существует несколько видов отношений «многие ко многим»; Вы должны задать себе следующие вопросы:

  • Хочу ли я хранить дополнительную информацию в ассоциации? (Дополнительные поля в соединительной таблице.)
  • Должны ли ассоциации быть неявно двунаправленными? (Если сообщение 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 является ругательством на пост 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, что 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 и т. Д. Это не красиво. (Я тоже открыт для предложений. Кто-нибудь?)

15 голосов
/ 19 августа 2014

Чтобы ответить на вопрос, поставленный Штифом:

Двунаправленные петельные ассоциации

Отношение последователь-подписчик среди пользователей является хорошим примером двунаправленной петлевой ассоциации. A Пользователь может иметь много:

  • последователи в своем качестве последователя
  • последователи в своем качестве последователя.

Вот как может выглядеть код для 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. Чтобы использовать в качестве примера связку мельницы (без зацикливания), Team может иметь много: players - :contracts. Это ничем не отличается для игрока , у которого также может быть много от :teams до :contracts (в течение карьеры такого игрока ). Но в этом случае, когда существует только одна именованная модель (то есть Пользователь ), идентифицирующая отношение сквозной связи идентично (например, through: :follow, или, как было сделано выше в примере сообщений, through: :post_connections) приведет к коллизии имен для разных случаев использования (или точек доступа в) таблицы соединений. :follower_follows и :followee_follows были созданы, чтобы избежать такого конфликта имен. Теперь Пользователь может иметь от :followers до :follower_follows и от :followees до :followee_follows.

Чтобы определить User : followees (после @user.followees обращения к базе данных), Rails теперь может просматривать каждый экземпляр class_name: «Follow», где такой пользователь является последователем ( то есть foreign_key: :follower_id) через: таких пользователей 's: followee_follows. Чтобы определить пользователя : последователей (после @user.followers вызова базы данных), Rails теперь может просматривать каждый экземпляр class_name: «Follow», где пользователь является подписчик (т. е. foreign_key: :followee_id) через: таких Пользователь 's: follower_follows.

6 голосов
/ 29 октября 2012

Если бы кто-нибудь пришел сюда, чтобы попытаться выяснить, как создать дружеские отношения в 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"
1 голос
/ 15 сентября 2017

Вдохновленный @ 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 должны оба работать, по крайней мере для меня.

1 голос
/ 09 января 2011

Для двунаправленного сообщения belongs_to_and_has_many обратитесь к уже опубликованному большому ответу, а затем создайте другую ассоциацию с другим именем, внешние ключи поменялись местами и убедитесь, что у вас установлен class_name для указания на правильную модель. Приветствия.

0 голосов
/ 21 апреля 2013

Если у кого-то возникли проблемы с получением отличного ответа на работу, например:

(Объект не поддерживает #inspect)
=>

или

NoMethodError: неопределенный метод `split 'для: Mission: Symbol

Тогда решение состоит в том, чтобы заменить :PostConnection на "PostConnection", заменяя ваше имя класса, конечно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...