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.
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ścieb.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 asocjacjihas_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 poprzezpost_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.
has_and_belongs_to_many
, takich jak :finder_sql
, :delete_sql
, itd. To nie jest ładne. (Tu też jestem otwarty na sugestie. Ktokolwiek?)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
.
@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.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"
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.
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.
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.
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