Relacja wielu do wielu z tym samym modelem w rails?

Jak mogę stworzyć relację wiele do wielu z tym samym modelem w rails?

Na przykład każdy post jest połączony z wieloma postami.

Author: Victor, 2010-01-30

6 answers

Istnieje kilka rodzajów relacji wielu 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ć w domyśle dwukierunkowe? (Jeśli post A jest połączony z postem B, to post B jest również połączony z postem A.)

To pozostawia cztery różne możliwości. Przejdę się po nich.

Dla odniesienie: dokumentacja Rails na ten temat . Jest sekcja o nazwie "wiele do wielu" i oczywiście dokumentacja samych metod klasowych.

Najprostszy scenariusz, jednokierunkowy, bez dodatkowych pól

To jest najbardziej zwarty w kodzie.

Zacznę od tego podstawowego schematu dla Twoich postów:

create_table "posts", :force => true do |t|
  t.string  "name", :null => false
end

Dla każdej relacji wielu do wielu, potrzebujesz tabeli join. Oto schemat tego:

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, Rails będzie nazywać tę tabelę kombinacją nazw dwóch tabel, do których dołączamy. Ale okazało się, że w tej sytuacji, więc postanowiłem wziąć post_connections.

Bardzo ważne jest :id => false, aby pominąć domyślną kolumnę id. Rails chce tę kolumnę wszędzie z wyjątkiem na tablicach join dla has_and_belongs_to_many. Będzie głośno narzekać.

Na koniec zauważ, że nazwy kolumn również są niestandardowe (Nie post_id), aby zapobiec konfliktowi.

Teraz w twoim modelu, po prostu musisz powiedzieć Rails o tych 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 przebiegająca przez 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 asocjacji posts spowoduje utworzenie rekordów w tabeli post_connections odpowiednio.

Kilka rzeczy do zapamiętania:

  • widać na powyższej sesji irb, że Asocjacja jest jednokierunkowa, ponieważ po a.posts = [b, c], Wyjście b.posts nie zawiera pierwszego posta.
  • inną rzeczą, którą mogłeś zauważyć, jest to, że nie ma modelu PostConnection. Zwykle nie używa się modeli do asocjacji has_and_belongs_to_many. Z tego powodu nie będzie można uzyskać dostępu do żadnych dodatkowych pól.

Jednokierunkowe, z dodatkowymi polami

W porządku... Masz regularnego użytkownika, który dziś napisał post na swojej stronie o tym, jak węgorze są pyszne. Ten zupełnie obcy przychodzi na Twoją stronę, rejestruje się i pisze skarcenie posta na nieudolność zwykłego użytkownika. W końcu węgorze są gatunkiem zagrożonym!

Chcesz więc jasno zaznaczyć w swojej bazie, że post B jest skarceniem na post A. Aby to zrobić, chcesz dodać pole category do asocjacji.

Potrzebujemy już nie has_and_belongs_to_many, ale kombinacji has_many, belongs_to, has_many ..., :through => ... i dodatkowy model dla stołu join. Ten dodatkowy model daje nam moc dodawania dodatkowych informacji do Stowarzyszenia siebie.

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

Zauważ jak w tej sytuacji, post_connections czy mA id kolumnę. (There ' s no :id => false parametr.) Jest to wymagane, ponieważ do dostępu do tabeli będzie dostępny zwykły model ActiveRecord.

Zacznę od modeluPostConnection, bo to jest ŚMIERTELNIE PROSTE:

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

Dzieje się tu tylko :class_name, co jest konieczne, ponieważ Rails nie może wywnioskować z post_a lub post_b że mamy tu do czynienia z postem. Musimy powiedzieć to wprost.

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 pierwszym has_many Stowarzyszeniem, mówimy modelowi, aby dołączył post_connections na posts.id = post_connections.post_a_id.

Z drugim asocjacją, mówimy Rails ' owi, że możemy dotrzeć do innych stanowisk, tych połączonych z tym, poprzez nasze pierwsze Asocjacje post_connections, a następnie post_b asocjację PostConnection.

Brakuje tylkojeszcze jednej rzeczy i to jest że musimy powiedzieć Rails ' owi, ż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 to połączenie niewiele by nam powiedziało, prawda? Oto jak to robimy w naszym modelu 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

Poza drobną zmianą składni, dwie prawdziwe rzeczy są tutaj różne:

  • has_many :post_connections ma dodatkowy parametr :dependent. Z wartością :destroy, mówimy Rails ' owi, że gdy ten post zniknie, może dalej niszczyć te obiekty. Na alternatywną wartością, którą możesz tu użyć, jest :delete_all, która jest szybsza, ale nie wywoła żadnych destroy hooków, jeśli ich używasz.
  • dodaliśmy has_many asocjację dla odwrotnych połączeń, również tych, które połączyły nas poprzez post_b_id. W ten sposób Rails może je równie dobrze zniszczyć. Zauważ, że musimy tutaj podać :class_name, ponieważ nazwa klasy modelu nie może być już wnioskowana z :reverse_post_connections.

Z tym na miejscu, przynoszę ci 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ć asocjację, a następnie osobno ustawiać kategorię, Możesz również utworzyć PostConnection i zrobić to:

>> 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 również manipulować asocjacjami post_connections i reverse_post_connections; będzie to starannie odzwierciedlać w asocjacji 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
=> []

Dwukierunkowe zapętlone skojarzenia

W normalnych has_and_belongs_to_many asocjacjach, Asocjacja jest zdefiniowana w obu modelach . A stowarzyszenie jest dwukierunkowa.

Ale jest tylko jeden model Post w tym przypadku. A stowarzyszenie jest określone tylko raz. Właśnie dlatego w tym konkretnym przypadku skojarzenia są jednokierunkowe.

To samo dotyczy alternatywnej metody z has_many i modelu dla tabeli join.

Jest to najlepiej widoczne, gdy po prostu uzyskasz dostęp do skojarzeń z irb i spojrzysz na SQL, który rails generuje w pliku dziennika. Znajdziesz coś takiego jak po:

SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )

Aby Asocjacja była dwukierunkowa, musielibyśmy znaleźć sposób na odwrócenie powyższych warunków za pomocą post_a_id i post_b_id, aby wyglądało to w obu kierunkach.

Niestety, jedyny sposób, aby to zrobić, o którym wiem, jest raczej trudny. Będziesz musiał ręcznie określić swój SQL za pomocą opcji has_and_belongs_to_many, takich jak :finder_sql, :delete_sql, itd. To nie jest ładne. (Tu też jestem otwarty na sugestie. Ktokolwiek?)
 254
Author: Shtééf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-27 14:44:51

Na pytanie zadane przez Shteef:

Dwukierunkowe zapętlone skojarzenia

Relacja follower-followee pomiędzy użytkownikami jest dobrym przykładem dwukierunkowego zapętlonego związku. Użytkownik może mieć wiele:
  • Obserwujący jako followee
  • następcy w charakterze następcy.

Oto jak wygląda kod dla użytkownika.RB może wyglądać:

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 jak kod do 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 W user.rb. Aby użyć asocjacji mill (non-looped) jako przykład, zespół może mieć wiele :players przez :contracts. Nie inaczej jest w przypadku gracza, który może mieć wiele :teams przez :contracts, jak również (w trakcie kariery takiego gracza). Ale w tym przypadku, gdy istnieje tylko jeden nazwany model (tj. User ), nazywając through: relacja identyczna (np. through: :follow, lub, jak to zostało zrobione powyżej w przykładzie posts, through: :post_connections) spowodowałaby kolizję nazw dla różnych przypadków użycia (lub punktów dostępu do) tabeli join. :follower_follows i :followee_follows zostały stworzone, aby uniknąć takiej kolizji nazw. Teraz użytkownik może mieć wiele :followers przez :follower_follows i wiele :followees przez :followee_follows.

Aby określić User's :followees (po wywołaniu @user.followees do bazy danych), Rails może teraz patrzeć na każdą instancję class_name: "Follow", gdzie taki użytkownik jest the follower (tj. foreign_key: :follower_id) poprzez: such User's :followee_follows. Aby określić User's :followers (po wywołaniu @user.followers do bazy danych), Rails może teraz spojrzeć na każdą instancję class_name: "Follow", gdzie taki User jest followee (tj. foreign_key: :followee_id) poprzez: such User's :follower_follows.
 15
Author: jbmilgrom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-20 11:58:42

Jeśli ktoś przyszedł tutaj, aby dowiedzieć się, jak tworzyć relacje z przyjaciółmi w Rails, to skierowałbym go do tego, co ostatecznie zdecydowałem się użyć, czyli skopiować to, co zrobił 'Community Engine'.

Możesz odnosić 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

Więcej informacje.

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"
 6
Author: hrdwdmrbl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-01 15:32:14

W przypadku dwukierunkowego belongs_to_and_has_many, zapoznaj się z napisaną już świetną odpowiedzią, a następnie utwórz kolejne skojarzenie o innej nazwie, odwrócone klucze obce i upewnij się, że masz class_name ustawione na powrót do właściwego modelu. Zdrowie.

 1
Author: Zhenya Slabkovski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-01 15:29:44

Jeśli ktoś miał problemy z uzyskaniem doskonałej odpowiedzi do pracy, takie jak:

(Obiekt nie obsługuje # inspect)
=>

Lub

NoMethodError: undefined metoda 'split' dla: Mission: Symbol

Rozwiązaniem jest zastąpienie :PostConnection przez "PostConnection", zastępując oczywiście nazwę klasy.

 0
Author: user2303277,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-21 03:54:00

Inspired by @ Stéphan Kochen, może to działać dla dwukierunkowych skojarzeń

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 przynajmniej dla mnie.

 0
Author: Alba Hoo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-15 02:24:45