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

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

Например, каждое сообщение связано со многими сообщениями.

Ответ 1

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

  • Должен ли я хранить дополнительную информацию в ассоциации? (Дополнительные поля в таблице соединений.)
  • Должны ли ассоциации быть неявно двунаправленными? (Если пост 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 здесь. Мы должны сказать это явно.

Теперь модель 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 и т.д. Это не очень. (Я тоже открыт для предложений. Кто-нибудь?)

Ответ 2

Чтобы ответить на вопрос, заданный Shteef:

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

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

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

или

NoMethodError: undefined метод `split 'для: Mission: Symbol

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

Ответ 6

Вдохновленный @Стефан Кохен, это может работать для двунаправленных ассоциаций

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