ความสัมพันธ์แบบกลุ่มต่อกลุ่มกับรุ่นเดียวกันในราง?


107

ฉันจะสร้างความสัมพันธ์แบบกลุ่มต่อกลุ่มกับโมเดลเดียวกันในรางได้อย่างไร

ตัวอย่างเช่นแต่ละโพสต์จะเชื่อมต่อกับโพสต์จำนวนมาก

คำตอบ:


276

ความสัมพันธ์แบบกลุ่มต่อกลุ่มมีหลายประเภท คุณต้องถามตัวเองด้วยคำถามต่อไปนี้:

  • ฉันต้องการจัดเก็บข้อมูลเพิ่มเติมกับสมาคมหรือไม่? (ช่องเพิ่มเติมในตารางเข้าร่วม)
  • การเชื่อมโยงจำเป็นต้องเป็นแบบสองทิศทางโดยปริยายหรือไม่? (หากโพสต์ A เชื่อมต่อกับโพสต์ B โพสต์ B จะเชื่อมต่อกับโพสต์ A ด้วย)

นั่นทำให้เกิดความเป็นไปได้ที่แตกต่างกันสี่ประการ ฉันจะเดินดูสิ่งเหล่านี้ด้านล่าง

สำหรับการอ้างอิง: รางเอกสารเกี่ยวกับเรื่องนี้ มีส่วนที่เรียกว่า“ หลายต่อหลายคน” และแน่นอนว่าเอกสารเกี่ยวกับเมธอดของคลาสเอง

สถานการณ์ที่ง่ายที่สุดทิศทางเดียวไม่มีฟิลด์เพิ่มเติม

นี่เป็นรหัสที่กะทัดรัดที่สุด

ฉันจะเริ่มด้วยสคีมาพื้นฐานสำหรับโพสต์ของคุณ:

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ตารางตามความเหมาะสม

สิ่งที่ควรทราบ:

  • คุณสามารถเห็นในเซสชัน irb ด้านบนว่าการเชื่อมโยงเป็นแบบทิศทางเดียวเพราะหลังจากa.posts = [b, c]นั้นผลลัพธ์ของb.postsจะไม่รวมโพสต์แรก
  • PostConnectionสิ่งที่คุณอาจจะสังเกตเห็นก็คือว่ามีเป็นรูปแบบไม่มี โดยปกติคุณไม่ได้ใช้โมเดลสำหรับการhas_and_belongs_to_manyเชื่อมโยง ด้วยเหตุนี้คุณจะไม่สามารถเข้าถึงฟิลด์เพิ่มเติมใด ๆ

Uni-directional พร้อมฟิลด์เพิ่มเติม

ตอนนี้ ... คุณมีผู้ใช้ประจำวันนี้ที่โพสต์บนเว็บไซต์ของคุณว่าปลาไหลอร่อยแค่ไหน คนแปลกหน้าทั้งหมดนี้เข้ามาในไซต์ของคุณสมัครและเขียนโพสต์ด่าว่าผู้ใช้ทั่วไป ท้ายที่สุดแล้วปลาไหลเป็นสัตว์ใกล้สูญพันธุ์!

ดังนั้นคุณต้องการระบุให้ชัดเจนในฐานข้อมูลของคุณว่าโพสต์ 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_connectionsposts.id = post_connections.post_a_id

กับสมาคมที่สองเราจะบอกทางรถไฟที่เราสามารถเข้าถึงกระทู้อื่น ๆ ที่เชื่อมต่อกับคนนี้ผ่านสมาคมครั้งแรกของเราpost_connectionsตามความสัมพันธ์ของpost_bPostConnection

มีอีกสิ่งหนึ่งที่ขาดหายไปและนั่นคือเราต้องบอก 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ฯลฯ มันไม่สวย (ฉันเปิดให้คำแนะนำที่นี่ด้วยทุกคน?)


ขอบคุณสำหรับความคิดเห็นที่ดี! :) ฉันได้ทำการแก้ไขเพิ่มเติมแล้ว โดยเฉพาะอย่างยิ่ง:foreign_keyบนhas_many :throughไม่จำเป็นและฉันได้เพิ่มคำอธิบายเกี่ยวกับวิธีใช้:dependentพารามิเตอร์ที่มีประโยชน์สำหรับhas_many.
Stéphan Kochen

@ Shtééfแม้กระทั่งการมอบหมายงานจำนวนมาก (update_attributes) จะใช้ไม่ได้ในกรณีของการเชื่อมโยงแบบสองทิศทางเช่น postA.update_attributes ({: post_b_ids => [2,3,4]}) มีแนวคิดหรือวิธีแก้ปัญหาใด ๆ
Lohith MV

เพื่อนตอบดีมาก 5.times {ใส่ "+1"}
ราหุล

@ Shtééfฉันได้เรียนรู้มากมายจากคำตอบนี้ขอบคุณ! ฉันพยายามถามและตอบคำถามการเชื่อมโยงสองทิศทางของคุณที่นี่: stackoverflow.com/questions/25493368/…
jbmilgrom

17

ในการตอบคำถามที่ 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


1
สิ่งที่ฉันต้องการ! ขอบคุณ! (ฉันขอแนะนำให้แสดงรายการการย้ายฐานข้อมูลด้วยฉันต้องรวบรวมข้อมูลนั้นจากคำตอบที่ยอมรับ)
Adam Denoon

6

หากใครมาที่นี่เพื่อค้นหาวิธีสร้างความสัมพันธ์แบบเพื่อนใน 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"

2

แรงบันดาลใจจาก @ 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ควรทำงานทั้งสองอย่างอย่างน้อยก็เหมาะกับฉัน


1

สำหรับแบบสองทิศทางbelongs_to_and_has_manyโปรดดูคำตอบที่ดีที่โพสต์ไว้แล้วจากนั้นสร้างการเชื่อมโยงอื่นด้วยชื่ออื่นคีย์ต่างประเทศจะกลับด้านและตรวจสอบให้แน่ใจว่าคุณได้class_nameตั้งค่าให้ชี้กลับไปยังโมเดลที่ถูกต้อง ไชโย


2
คุณช่วยแสดงตัวอย่างในโพสต์ของคุณได้ไหม ฉันได้ลองหลายวิธีตามที่คุณแนะนำ แต่ดูเหมือนจะตอกไม่ได้
achabacha322

0

หากใครมีปัญหาในการรับคำตอบที่ดีในการทำงานเช่น:

(วัตถุไม่รองรับ #inspect)
=>

หรือ

NoMethodError: วิธีการที่ไม่ได้กำหนด `` แยก 'สำหรับ: ภารกิจ: สัญลักษณ์

จากนั้นวิธีแก้ปัญหาคือแทนที่:PostConnectionด้วยการ"PostConnection"แทนที่ชื่อคลาสของคุณแน่นอน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.