Odpowiedzi:
Istnieje kilka rodzajów relacji „wiele do wielu”; musisz zadać sobie następujące pytania:
To pozostawia cztery różne możliwości. Przejdę przez to poniżej.
Dla odniesienia: dokumentacja Rails na ten temat . Jest sekcja o nazwie „Wiele do wielu” i oczywiście dokumentacja dotycząca samych metod klasowych.
Jest to najbardziej zwarty kod.
Zacznę od tego podstawowego schematu dla Twoich postów:
create_table "posts", :force => true do |t|
t.string "name", :null => false
end
W przypadku każdej relacji wiele do wielu potrzebna jest tabela łączenia. Oto schemat:
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
Domyślnie Railsy będą nazywać tę tabelę kombinacją nazw dwóch tabel, do których się przyłączamy. Ale to by się okazało jak posts_posts
w tej sytuacji, więc zdecydowałem się post_connections
zamiast tego wziąć .
Bardzo ważne jest :id => false
, aby pominąć domyślną id
kolumnę. Railsy chcą, aby ta kolumna była wszędzie z wyjątkiem tabel łączenia dla has_and_belongs_to_many
. Będzie głośno narzekać.
Na koniec zwróć uwagę, że nazwy kolumn również są niestandardowe (nie post_id
), aby zapobiec konfliktom.
Teraz w swoim modelu musisz po prostu powiedzieć Railsom o kilku niestandardowych rzeczach. Będzie wyglądać następująco:
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
I to powinno po prostu zadziałać! Oto przykładowa sesja 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!">]
Przekonasz się, że przypisanie do posts
asocjacji spowoduje utworzenie odpowiednich rekordów w post_connections
tabeli.
Kilka uwag:
a.posts = [b, c]
wyjściu b.posts
nie obejmuje pierwszego wpisu.PostConnection
. Zwykle nie używasz modeli do has_and_belongs_to_many
asocjacji. Z tego powodu nie będziesz mieć dostępu do żadnych dodatkowych pól.No właśnie, teraz ... Masz zwykłego użytkownika, który opublikował dziś w Twojej witrynie post o tym, jak pyszne są węgorze. Ten zupełnie nieznajomy przychodzi do Twojej witryny, rejestruje się i pisze z karą za nieudolność zwykłego użytkownika. W końcu węgorze to gatunek zagrożony wyginięciem!
Więc chciałbyś jasno określić w swojej bazie danych, że post B jest karcącym tyłem na post A. Aby to zrobić, chcesz dodać category
pole do skojarzenia.
Co musimy już nie ma has_and_belongs_to_many
, ale kombinacja has_many
, belongs_to
, has_many ..., :through => ...
a dodatkowy model łączenia tabeli. Ten dodatkowy model daje nam moc dodawania dodatkowych informacji do samego stowarzyszenia.
Oto kolejny schemat, bardzo podobny do powyższego:
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
Wskazówki, jak w tej sytuacji post_connections
nie mają id
kolumnę. (Nie ma żadnego :id => false
parametru). Jest to konieczne, ponieważ nie będzie regularny wzór ActiveRecord dostępu do tabeli.
Zacznę od PostConnection
modelu, bo to jest banalnie proste:
class PostConnection < ActiveRecord::Base
belongs_to :post_a, :class_name => :Post
belongs_to :post_b, :class_name => :Post
end
Jedyne, co się tutaj dzieje, to to :class_name
, co jest konieczne, ponieważ Railsy nie mogą wywnioskować z Postu post_a
lub post_b
że mamy tu do czynienia z Postem. Musimy to wyraźnie powiedzieć.
Teraz Post
model:
class Post < ActiveRecord::Base
has_many :post_connections, :foreign_key => :post_a_id
has_many :posts, :through => :post_connections, :source => :post_b
end
Z pierwszego has_many
związku, mówimy modelu dołączyć post_connections
na posts.id = post_connections.post_a_id
.
Za pomocą drugiego skojarzenia mówimy Railsom, że możemy dotrzeć do innych postów, tych połączonych z tym, poprzez nasze pierwsze skojarzenie post_connections
, po którym następuje post_b
skojarzenie PostConnection
.
Brakuje tylko jednej rzeczy , a mianowicie tego, że musimy powiedzieć Railsom, że a PostConnection
jest zależne od postów, do których należy. Gdyby jedno lub oba z post_a_id
i post_b_id
były NULL
, to połączenie niewiele by nam powiedziało, prawda? Oto jak to robimy w naszym Post
modelu:
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
Oprócz niewielkiej zmiany składni, dwie rzeczywiste rzeczy są tutaj różne:
has_many :post_connections
Ma dodatkowy :dependent
parametr. Za pomocą tej wartości :destroy
mówimy Railsom, że gdy ten post zniknie, może przejść dalej i zniszczyć te obiekty. Alternatywną wartością, której możesz tutaj użyć, jest to :delete_all
, że jest szybsza, ale nie wywoła żadnych haków zniszczenia, jeśli ich używasz.has_many
skojarzenie dla połączeń zwrotnych , tych, które nas połączyły post_b_id
. W ten sposób Railsy mogą porządnie je zniszczyć. Zauważ, że musimy :class_name
tutaj określić , ponieważ nazwa klasy modelu nie może być już wywnioskowana z :reverse_post_connections
.Mając to na miejscu, przedstawiam kolejną sesję IRB przez 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
Zamiast tworzyć powiązanie, a następnie oddzielnie ustawiać kategorię, możesz po prostu utworzyć PostConnection i skończyć z tym:
>> 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!">]
Możemy także manipulować skojarzeniami post_connections
i reverse_post_connections
; dobrze odzwierciedli w posts
stowarzyszeniu:
>> 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
=> []
W normalnych has_and_belongs_to_many
asocjacjach powiązanie jest zdefiniowane w obu modelach. Skojarzenie jest dwukierunkowe.
Ale w tym przypadku jest tylko jeden model Post. Powiązanie jest określone tylko raz. Właśnie dlatego w tym konkretnym przypadku skojarzenia są jednokierunkowe.
To samo dotyczy alternatywnej metody zi has_many
modelu tabeli łączenia.
Najlepiej widać to po prostu uzyskując dostęp do asocjacji z irb i patrząc na kod SQL generowany przez Rails w pliku dziennika. Znajdziesz coś takiego:
SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )
Aby związek dwukierunkowy, że musimy znaleźć sposób, aby Szyny OR
powyższe warunki z post_a_id
i post_b_id
odwrócone, tak to będzie wyglądać w obu kierunkach.
Niestety jedyny znany mi sposób na zrobienie tego jest dość hakerski. Będziesz musiał ręcznie określić swoją SQL przy użyciu opcji has_and_belongs_to_many
, takich jak :finder_sql
, :delete_sql
itd To nie jest ładna. (Tutaj też jestem otwarty na sugestie. Czy ktoś?)
Aby odpowiedzieć na pytanie postawione przez Shteef:
Relacja naśladowca-podążający między użytkownikami jest dobrym przykładem dwukierunkowego zapętlonego skojarzenia. Użytkownik może mieć wiele:
Oto jak może wyglądać kod 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
Oto kod dla 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
Najważniejsze rzeczy, na które należy zwrócić uwagę, to prawdopodobnie terminy :follower_follows
i :followee_follows
plik user.rb. Aby użyć jako przykładu asocjacji typu run of the mill (niezapętlonego), zespół może mieć wiele: players
do :contracts
. Nie inaczej jest w przypadku Gracza , który może mieć również wiele :teams
przejść :contracts
(w trakcie kariery takiego Gracza ). Ale w tym przypadku, gdy istnieje tylko jeden nazwany model (tj. Użytkownik ), nazwanie relacji przez: identycznie (np. through: :follow
Lub tak jak zostało to zrobione powyżej w przykładzie postów through: :post_connections
) spowodowałoby kolizję nazw dla różnych przypadków użycia ( lub punkty dostępu do) tabeli łączenia. :follower_follows
i:followee_follows
zostały stworzone, aby uniknąć takiej kolizji nazw. Teraz użytkownik może mieć wiele :followers
przejść :follower_follows
i wiele :followees
przejść :followee_follows
.
Aby określić użytkownika : followees (po @user.followees
wywołaniu bazy danych), Railsy mogą teraz przeglądać każdą instancję class_name: „Follow”, gdzie taki użytkownik jest obserwatorem (tj. foreign_key: :follower_id
) Poprzez: takiego użytkownika : followee_follows. Aby określić Użytkownika „s Obserwujący (upon a @user.followers
połączenia do bazy danych), Szyny mogą teraz spojrzeć na każdej instancji CLASS_NAME:«Obserwuj»gdzie takie obsługi Czy THE followee (tj foreign_key: :followee_id
) poprzez: tak User „s follower_follows.
Gdyby ktoś przyszedł tutaj, aby spróbować dowiedzieć się, jak tworzyć relacje przyjacielskie w Railsach, to odesłałbym go do tego, co ostatecznie zdecydowałem się użyć, czyli skopiowania tego, co zrobił „Community Engine”.
Możesz odnieść się do:
https://github.com/bborn/communityengine/blob/master/app/models/friendship.rb
i
https://github.com/bborn/communityengine/blob/master/app/models/user.rb
po więcej informacji.
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"
Zainspirowany @ Stéphan Kochen, może to działać w przypadku stowarzyszeń dwukierunkowych
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
wtedy post.posts
&& post.reversed_posts
powinny działać, przynajmniej działały dla mnie.
W przypadku komunikacji dwukierunkowej belongs_to_and_has_many
zapoznaj się ze świetną już opublikowaną odpowiedzią, a następnie utwórz kolejne skojarzenie z inną nazwą, odwróć klucze obce i upewnij się, że class_name
ustawiłeś wskazanie z powrotem na właściwy model. Twoje zdrowie.
Gdyby ktoś miał problemy z uzyskaniem doskonałej odpowiedzi do pracy, takie jak:
(Obiekt nie obsługuje #inspect)
=>
lub
NoMethodError: undefined method `split 'for: Mission: Symbol
Wtedy rozwiązaniem jest zastąpienie :PostConnection
z "PostConnection"
zastępując swoją classname oczywiście.
:foreign_key
onhas_many :through
nie jest konieczna i dodałem wyjaśnienie, jak używać bardzo przydatnego:dependent
parametru forhas_many
.