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