Relacja wiele do wielu z tym samym modelem w szynach?

107

Jak mogę utworzyć relację wiele do wielu z tym samym modelem w szynach?

Na przykład każdy post jest powiązany z wieloma postami.

Zwycięzca
źródło

Odpowiedzi:

276

Istnieje kilka rodzajów relacji „wiele do wielu”; musisz zadać sobie następujące pytania:

  • Czy chcę przechowywać dodatkowe informacje w stowarzyszeniu? (Dodatkowe pola w tabeli łączenia).
  • Czy skojarzenia muszą być pośrednio dwukierunkowe? (Jeśli słupek A jest połączony ze słupkiem B, słupek B jest również połączony ze słupkiem A.)

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.

Najprostszy scenariusz, jednokierunkowy, bez dodatkowych pól

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_postsw tej sytuacji, więc zdecydowałem się post_connectionszamiast tego wziąć .

Bardzo ważne jest :id => false, aby pominąć domyślną idkolumnę. 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 postsasocjacji spowoduje utworzenie odpowiednich rekordów w post_connectionstabeli.

Kilka uwag:

  • Możesz zobaczyć w powyższej sesji irb, że skojarzenie jest jednokierunkowe, ponieważ po a.posts = [b, c]wyjściu b.postsnie obejmuje pierwszego wpisu.
  • Inną rzeczą, którą być może zauważyłeś, jest brak modelu PostConnection. Zwykle nie używasz modeli do has_and_belongs_to_manyasocjacji. Z tego powodu nie będziesz mieć dostępu do żadnych dodatkowych pól.

Jednokierunkowy, z dodatkowymi polami

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ć categorypole 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ą idkolumnę. (Nie ma żadnego :id => false parametru). Jest to konieczne, ponieważ nie będzie regularny wzór ActiveRecord dostępu do tabeli.

Zacznę od PostConnectionmodelu, 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_alub post_bże mamy tu do czynienia z Postem. Musimy to wyraźnie powiedzieć.

Teraz Postmodel:

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_manyzwiązku, mówimy modelu dołączyć post_connectionsna 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_bskojarzenie PostConnection.

Brakuje tylko jednej rzeczy , a mianowicie tego, że musimy powiedzieć Railsom, że a PostConnectionjest zależne od postów, do których należy. Gdyby jedno lub oba z post_a_idi post_b_idbyły NULL, to połączenie niewiele by nam powiedziało, prawda? Oto jak to robimy w naszym Postmodelu:

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_connectionsMa dodatkowy :dependentparametr. Za pomocą tej wartości :destroymó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.
  • Dodaliśmy również has_manyskojarzenie 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_nametutaj 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_connectionsi reverse_post_connections; dobrze odzwierciedli w postsstowarzyszeniu:

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

Dwukierunkowe skojarzenia zapętlone

W normalnych has_and_belongs_to_manyasocjacjach 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_manymodelu 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 ORpowyższe warunki z post_a_idi post_b_idodwró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_sqlitd To nie jest ładna. (Tutaj też jestem otwarty na sugestie. Czy ktoś?)

Shtééf
źródło
Dziękuję za miłe komentarze! :) Wprowadziłem kilka dalszych zmian. W szczególności opcja :foreign_keyon has_many :throughnie jest konieczna i dodałem wyjaśnienie, jak używać bardzo przydatnego :dependentparametru for has_many.
Stéphan Kochen
@ Shtééf nawet przypisanie masowe (update_attributes) nie będzie działać w przypadku skojarzeń dwukierunkowych, np .: postA.update_attributes ({: post_b_ids => [2,3,4]}) jakiś pomysł lub obejście?
Lohith MV
Bardzo miła odpowiedź kolega 5. razy {wstawia "+1"}
Rahul,
@ Shtééf Wiele się nauczyłem z tej odpowiedzi, dziękuję! Próbowałem tutaj zadać i odpowiedzieć na twoje pytanie dotyczące dwukierunkowego stowarzyszenia: stackoverflow.com/questions/25493368/ ...
jbmilgrom
17

Aby odpowiedzieć na pytanie postawione przez Shteef:

Dwukierunkowe skojarzenia zapętlone

Relacja naśladowca-podążający między użytkownikami jest dobrym przykładem dwukierunkowego zapętlonego skojarzenia. Użytkownik może mieć wiele:

  • obserwujących w charakterze obserwujących
  • followees w charakterze naśladowcy.

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_followsi :followee_followsplik user.rb. Aby użyć jako przykładu asocjacji typu run of the mill (niezapętlonego), zespół może mieć wiele: playersdo :contracts. Nie inaczej jest w przypadku Gracza , który może mieć również wiele :teamsprzejść :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: :followLub 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_followsi:followee_followszostały stworzone, aby uniknąć takiej kolizji nazw. Teraz użytkownik może mieć wiele :followersprzejść :follower_followsi wiele :followeesprzejść :followee_follows.

Aby określić użytkownika : followees (po @user.followeeswywoł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.followerspołą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.

jbmilgrom
źródło
1
Dokładnie to, czego potrzebowałem! Dzięki! (Polecam również
wymienić
6

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"
hrdwdmrbl
źródło
2

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_postspowinny działać, przynajmniej działały dla mnie.

Alba Hoo
źródło
1

W przypadku komunikacji dwukierunkowej belongs_to_and_has_manyzapoznaj się ze świetną już opublikowaną odpowiedzią, a następnie utwórz kolejne skojarzenie z inną nazwą, odwróć klucze obce i upewnij się, że class_nameustawiłeś wskazanie z powrotem na właściwy model. Twoje zdrowie.

Zhenya Slabkovski
źródło
2
Czy mógłbyś pokazać przykład w swoim poście? Próbowałem wielu sposobów, jak sugerowałeś, ale nie mogę tego naprawić.
achabacha322
0

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 :PostConnectionz "PostConnection"zastępując swoją classname oczywiście.

user2303277
źródło