Отношения Many-many с той же моделью в направляющих?

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

Например, каждое сообщение подключено ко многим сообщениям.

105
задан Victor 30 January 2010 в 15:55
поделиться

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 будет называть эту таблицу комбинацией имен двух таблиц, к которым мы присоединяемся. Но это обернулось бы как посты _ посты в этой ситуации, поэтому я решил взять пост _ соединения вместо.

Здесь очень важно : id = > false , чтобы опустить столбец id по умолчанию. Rails хочет, чтобы столбец везде , за исключением в таблицах соединения для , имел _ и _ принадлежит _ ко _ многим . Это будет громко жаловаться.

Наконец, обратите внимание, что имена столбцов также являются нестандартными (не 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, выполняемого через скрипт/консоль :

>> 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!">]

При назначении связи post будут созданы записи в таблице post _ connections .

Некоторые вещи, которые следует отметить:

  • В вышеприведенной сессии irb можно увидеть, что ассоциация является однонаправленной, потому что после a.posts = [b, c] вывод b.posts не включает первую запись.
  • Еще одна вещь, которую вы могли заметить, это отсутствие модели PostConnection . Обычно модели не используются для связи имеет _ и _ принадлежит _ ко _ многим . По этой причине доступ к дополнительным полям невозможен.

Однонаправленный, с дополнительными полями

Прямо сейчас... У вас есть постоянный пользователь, который сегодня сделал сообщение на вашем сайте о том, как угри вкусны. Этот полный незнакомец приходит на ваш сайт, подписывается и пишет ругательный пост о неумении обычного пользователя. Ведь угри - вымирающий вид!

Таким образом, вы хотите уточнить в своей базе данных, что публикация B является ругательным раритетом на публикации A. Для этого вы хотите добавить поле категории в ассоциацию.

То, что нам больше не нужно, это имеет _ и _ принадлежит _ ко _ многим , но комбинация имеет _ много , принадлежит _ , имеет _ много...,: через = >... и дополнительную модель для таблицы соединений. Эта дополнительная модель дает нам возможность добавлять дополнительную информацию в саму ассоциацию.

Вот еще одна схема, очень похожая на приведенную выше:

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 . (Параметр no : id = > false отсутствует.) Это необходимо, поскольку для доступа к таблице будет использоваться обычная модель ActiveRecord.

Я начну с модели PostConnection , потому что это просто:

class PostConnection < ActiveRecord::Base
  belongs_to :post_a, :class_name => :Post
  belongs_to :post_b, :class_name => :Post
end

Здесь происходит только : класс _ имя , что необходимо, потому что 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

С первой имеет _ связь , мы говорим модели присоединиться post _ connections на posts.id = post_connections.post_a_id.

Со второй ассоциацией мы говорим Rails, что мы можем достичь других постов, которые связаны с этой, через нашу первую ассоциацию post _ connections , а затем post _ b association 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

Помимо незначительного изменения синтаксиса, здесь различаются две реальные вещи:

  • имеет _ много: post _ connections имеет дополнительный параметр : dependent . Со значением : уничтожить , мы говорим Rails, что, как только этот пост исчезнет, он может пойти вперед и уничтожить эти объекты. Альтернативное значение, которое вы можете использовать здесь, это : удалить _ все , что быстрее, но не вызовет никаких крючков уничтожения, если вы используете их.
  • Мы также добавили ассоциацию has _ many для обратных соединений, которые связали нас через post _ b _ id . Таким образом, Рейлз может уничтожить и их. Обратите внимание, что здесь необходимо указать : class _ name , поскольку имя класса модели больше нельзя вывести из : reverse _ post _ connections .

Установив это, я приведу еще один сеанс irb через скрипт/консоль :

>> 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 ; он будет точно отражать в ассоциации постов :

>> 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
=> []

двунаправленные закольцованные ассоциации

В обычной имеет ассоциации _ и _ принадлежит _ к _ многим , ассоциация определяется в обеих задействованных моделях . И ассоциация является двунаправленной.

Но в данном случае существует только одна модель Post. И ассоциация указывается только один раз. Именно поэтому в данном конкретном случае ассоциации являются однонаправленными.

То же самое справедливо для альтернативного метода с имеет _ много и модель для таблицы соединения.

Это лучше всего видно, если просто обратиться к ассоциациям из 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, используя параметры имеет _ и _ принадлежит _ к _ многим , например : finder _ sql , : delete _ sql и т.д. Это не красиво. (Я тоже открыт для предложений. Кто угодно?)

269
ответ дан 24 November 2019 в 03:55
поделиться
Другие вопросы по тегам:

Похожие вопросы: