ฉันจะสร้างความสัมพันธ์แบบกลุ่มต่อกลุ่มกับโมเดลเดียวกันในรางได้อย่างไร
ตัวอย่างเช่นแต่ละโพสต์จะเชื่อมต่อกับโพสต์จำนวนมาก
ฉันจะสร้างความสัมพันธ์แบบกลุ่มต่อกลุ่มกับโมเดลเดียวกันในรางได้อย่างไร
ตัวอย่างเช่นแต่ละโพสต์จะเชื่อมต่อกับโพสต์จำนวนมาก
คำตอบ:
ความสัมพันธ์แบบกลุ่มต่อกลุ่มมีหลายประเภท คุณต้องถามตัวเองด้วยคำถามต่อไปนี้:
นั่นทำให้เกิดความเป็นไปได้ที่แตกต่างกันสี่ประการ ฉันจะเดินดูสิ่งเหล่านี้ด้านล่าง
สำหรับการอ้างอิง: รางเอกสารเกี่ยวกับเรื่องนี้ มีส่วนที่เรียกว่า“ หลายต่อหลายคน” และแน่นอนว่าเอกสารเกี่ยวกับเมธอดของคลาสเอง
นี่เป็นรหัสที่กะทัดรัดที่สุด
ฉันจะเริ่มด้วยสคีมาพื้นฐานสำหรับโพสต์ของคุณ:
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
โมเดล:
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
กับสมาคมที่สองเราจะบอกทางรถไฟที่เราสามารถเข้าถึงกระทู้อื่น ๆ ที่เชื่อมต่อกับคนนี้ผ่านสมาคมครั้งแรกของเราpost_connections
ตามความสัมพันธ์ของpost_b
PostConnection
มีอีกสิ่งหนึ่งที่ขาดหายไปและนั่นคือเราต้องบอก Rails ว่า a 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
เชื่อมโยงปกติการเชื่อมโยงถูกกำหนดในทั้งสองรุ่นที่เกี่ยวข้อง และการเชื่อมโยงเป็นแบบสองทิศทาง
แต่มีเพียงรูปแบบการโพสต์เดียวในกรณีนี้ และมีการระบุการเชื่อมโยงเพียงครั้งเดียว นั่นเป็นเหตุผลว่าทำไมในกรณีนี้การเชื่อมโยงจึงเป็นแบบทิศทางเดียว
เช่นเดียวกับวิธีอื่นที่มี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 เสนอ:
ความสัมพันธ์ระหว่างผู้ติดตามและผู้ติดตามระหว่างผู้ใช้เป็นตัวอย่างที่ดีของการเชื่อมโยงแบบสองทิศทาง ผู้ใช้สามารถมีหลาย
นี่คือลักษณะของโค้ดสำหรับ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
สิ่งนี้ไม่แตกต่างกันสำหรับผู้เล่นซึ่งอาจมีหลายคน:teams
ผ่าน:contracts
เช่นกัน (ตลอดอาชีพของผู้เล่นคนนั้น) แต่ในกรณีนี้เมื่อมีโมเดลที่มีชื่อเพียงตัวเดียว (เช่นผู้ใช้ ) การตั้งชื่อผ่าน: ความสัมพันธ์เหมือนกัน (เช่นthrough: :follow
หรือเช่นเดียวกับที่ทำไว้ข้างต้นในตัวอย่างโพสต์through: :post_connections
) จะส่งผลให้การตั้งชื่อขัดแย้งกันสำหรับกรณีการใช้งานที่แตกต่างกันของ ( หรือจุดเชื่อมต่อเข้าไปใน) ตารางเข้าร่วม :follower_follows
และ:followee_follows
ถูกสร้างขึ้นเพื่อหลีกเลี่ยงความขัดแย้งในการตั้งชื่อ ตอนนี้ผู้ใช้สามารถมีจำนวนมาก:followers
ผ่าน:follower_follows
และหลายผ่าน:followees
:followee_follows
ในการกำหนดผู้ใช้ : ผู้ติดตาม (เมื่อมีการ@user.followees
โทรไปยังฐานข้อมูล) Rails อาจดูแต่ละอินสแตนซ์ของ class_name:“ Follow” โดยที่ผู้ใช้ดังกล่าวเป็นผู้ติดตาม (เช่นforeign_key: :follower_id
) ผ่าน: ผู้ใช้ดังกล่าว: followee_follows ในการกำหนดผู้ใช้ : ผู้ติดตาม (เมื่อมีการ@user.followers
เรียกฐานข้อมูล) Rails อาจดูแต่ละอินสแตนซ์ของ class_name:“ Follow” โดยที่ผู้ใช้ดังกล่าวเป็นผู้ติดตาม (เช่นforeign_key: :followee_id
) ผ่าน: ผู้ใช้ดังกล่าว: 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; ดร
# 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"
แรงบันดาลใจจาก @ Stéphan Kochen สิ่งนี้สามารถใช้ได้กับการเชื่อมโยงแบบสองทิศทาง
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
ควรทำงานทั้งสองอย่างอย่างน้อยก็เหมาะกับฉัน
สำหรับแบบสองทิศทางbelongs_to_and_has_many
โปรดดูคำตอบที่ดีที่โพสต์ไว้แล้วจากนั้นสร้างการเชื่อมโยงอื่นด้วยชื่ออื่นคีย์ต่างประเทศจะกลับด้านและตรวจสอบให้แน่ใจว่าคุณได้class_name
ตั้งค่าให้ชี้กลับไปยังโมเดลที่ถูกต้อง ไชโย
หากใครมีปัญหาในการรับคำตอบที่ดีในการทำงานเช่น:
(วัตถุไม่รองรับ #inspect)
=>
หรือ
NoMethodError: วิธีการที่ไม่ได้กำหนด `` แยก 'สำหรับ: ภารกิจ: สัญลักษณ์
จากนั้นวิธีแก้ปัญหาคือแทนที่:PostConnection
ด้วยการ"PostConnection"
แทนที่ชื่อคลาสของคุณแน่นอน
:foreign_key
บนhas_many :through
ไม่จำเป็นและฉันได้เพิ่มคำอธิบายเกี่ยวกับวิธีใช้:dependent
พารามิเตอร์ที่มีประโยชน์สำหรับhas_many
.