Как я могу сделать отношение "многие ко многим" с той же моделью в рельсах?
Например, каждое сообщение связано со многими сообщениями.
Как я могу сделать отношение "многие ко многим" с той же моделью в рельсах?
Например, каждое сообщение связано со многими сообщениями.
Существует несколько видов отношений "многие-ко-многим"; вы должны задать себе следующие вопросы:
Это оставляет четыре разные возможности. Я рассмотрю эти ниже.
Для справки: документация 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
, если это необходимо.
Некоторые примечания:
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
и т.д. Это не очень. (Я тоже открыт для предложений. Кто-нибудь?)
Чтобы ответить на вопрос, заданный 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.
Если кто-нибудь пришел сюда, чтобы попытаться выяснить, как создавать отношения друзей в 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"
Для двунаправленного belongs_to_and_has_many
обратитесь к большому ответу, уже опубликованному, а затем создайте другую ассоциацию с другим именем, внешние ключи перевернули и убедитесь, что у вас есть class_name
, чтобы указать на правильную модель. Приветствия.
Если у кого-то возникли проблемы с получением отличного ответа на работу, например:
(Объект не поддерживает #inspect)
= >
или
NoMethodError: undefined метод `split 'для: Mission: Symbol
Тогда решение должно заменить :PostConnection
на "PostConnection"
, заменив ваше имя класса конечно.
Вдохновленный @Стефан Кохен, это может работать для двунаправленных ассоциаций
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
должен работать, по крайней мере, работал у меня.