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

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

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

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"Подставляя ваше имя класса, конечно.

Другие вопросы по тегам