วิธีการแสดงระเบียนที่ไม่ซ้ำกันจาก has_many ผ่านความสัมพันธ์?


107

ฉันสงสัยว่าวิธีใดเป็นวิธีที่ดีที่สุดในการแสดงบันทึกเฉพาะจาก has_many ผ่านความสัมพันธ์ใน Rails3

ฉันมีสามรุ่น:

class User < ActiveRecord::Base
    has_many :orders
    has_many :products, :through => :orders
end

class Products < ActiveRecord::Base
    has_many :orders
    has_many :users, :through => :orders
end

class Order < ActiveRecord::Base
    belongs_to :user, :counter_cache => true 
    belongs_to :product, :counter_cache => true 
end

สมมติว่าฉันต้องการแสดงรายการผลิตภัณฑ์ทั้งหมดที่ลูกค้าสั่งซื้อในหน้าแสดงของพวกเขา

พวกเขาอาจสั่งซื้อสินค้าบางรายการหลายครั้งดังนั้นฉันจึงใช้ counter_cache เพื่อแสดงตามลำดับจากมากไปหาน้อยโดยพิจารณาจากจำนวนคำสั่งซื้อ

แต่ถ้าพวกเขาสั่งซื้อผลิตภัณฑ์หลายครั้งฉันต้องตรวจสอบให้แน่ใจว่าผลิตภัณฑ์แต่ละรายการอยู่ในรายการเพียงครั้งเดียว

@products = @user.products.ranked(:limit => 10).uniq!

ทำงานเมื่อมีบันทึกคำสั่งซื้อหลายรายการสำหรับผลิตภัณฑ์ แต่จะสร้างข้อผิดพลาดหากมีการสั่งซื้อผลิตภัณฑ์เพียงครั้งเดียว (อันดับคือฟังก์ชันการจัดเรียงแบบกำหนดเองที่กำหนดไว้ที่อื่น)

อีกทางเลือกหนึ่งคือ:

@products = @user.products.ranked(:limit => 10, :select => "DISTINCT(ID)")

ฉันไม่มั่นใจว่าฉันมาถูกทางแล้ว

มีใครจัดการเรื่องนี้อีกไหม? คุณคิดประเด็นอะไรขึ้นมา ฉันจะหาข้อมูลเพิ่มเติมเกี่ยวกับความแตกต่างระหว่าง. unique ได้ที่ไหน และ DISTINCT ()?

วิธีใดเป็นวิธีที่ดีที่สุดในการสร้างรายการระเบียนเฉพาะผ่าน has_many ผ่านความสัมพันธ์

ขอบคุณ

คำตอบ:


237

คุณได้พยายามระบุอ็อพชัน: uniq ในการเชื่อมโยง has_many:

has_many :products, :through => :orders, :uniq => true

จากเอกสาร Rails :

:uniq

หากเป็นจริงรายการที่ซ้ำกันจะถูกละเว้นจากคอลเล็กชัน มีประโยชน์ร่วมกับ: ผ่าน

อัปเดตสำหรับรถไฟ 4:

ใน Rails 4 has_many :products, :through => :orders, :uniq => trueเลิกใช้งานแล้ว ตอนนี้คุณควรเขียนhas_many :products, -> { distinct }, through: :ordersแทน ดูส่วนที่แตกต่างกันสำหรับ has_many:: ผ่านความสัมพันธ์ในเอกสารคู่มือการเชื่อมโยง ActiveRecordสำหรับข้อมูลเพิ่มเติม ขอบคุณ Kurt Mueller ที่ชี้ให้เห็นในความคิดเห็นของเขา


6
ความแตกต่างไม่มากไม่ว่าคุณจะลบรายการที่ซ้ำกันในโมเดลหรือคอนโทรลเลอร์ แต่คุณใช้อ็อพชัน: uniq ในการเชื่อมโยงของคุณ (ดังที่แสดงในคำตอบของฉัน) หรือ SQL DISTINCT stmt (เช่น has_many: products,: through =>: orders ,: select => "DISTINCT products. *) ในกรณีแรกระบบจะดึงข้อมูลทั้งหมดและรางจะลบรายการที่ซ้ำกันให้คุณในกรณีต่อมาจะมีการดึงเฉพาะระเบียนที่ไม่ซ้ำกันจากฐานข้อมูลดังนั้นจึงอาจมีประสิทธิภาพที่ดีกว่า หากคุณมีชุดผลลัพธ์ขนาดใหญ่
mbreining

68
ใน Rails 4 has_many :products, :through => :orders, :uniq => trueเลิกใช้งานแล้ว ตอนนี้คุณควรเขียนhas_many :products, -> { uniq }, through: :ordersแทน
Kurt Mueller

8
โปรดทราบว่า -> {uniq} ในความหมายนี้เป็นเพียงนามแฝงสำหรับ -> { different } apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniqซึ่งเกิดขึ้นใน SQL ไม่ใช่ทับทิม
engineerDave

6
หากคุณมีข้อขัดแย้งDISTINCTและORDER BYข้อขัดแย้งคุณสามารถใช้ได้เสมอhas_many :products, -> { unscope(:order).distinct }, through: :orders
fagiani

1
ขอบคุณ @fagiani และถ้าโมเดลของคุณมีคอลัมน์ json และคุณใช้ psql มันก็ซับซ้อนกว่านี้และคุณต้องทำอะไรอย่างhas_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :locationอื่นให้ได้rails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json
ryan2johnson9

43

โปรดทราบว่าuniq: trueได้ถูกลบออกจากตัวเลือกที่ถูกต้องสำหรับhas_manyRails 4

ใน Rails 4 คุณต้องระบุขอบเขตเพื่อกำหนดค่าลักษณะการทำงานนี้ ขอบเขตสามารถจัดหาผ่าน lambdas ได้ดังนี้:

has_many :products, -> { uniq }, :through => :orders

คู่มือรางครอบคลุมถึงสิ่งนี้และวิธีอื่น ๆ ที่คุณสามารถใช้ขอบเขตเพื่อกรองการสืบค้นของความสัมพันธ์ของคุณเลื่อนลงไปที่ส่วน 4.3.3:

http://guides.rubyonrails.org/association_basics.html#has-many-association-reference


4
ใน Rails 5.1 ฉันยังคงได้รับundefined method 'except' for #<Array...และเป็นเพราะฉันใช้.uniqแทน.distinct
stevenspiel

5

คุณสามารถใช้group_by. ตัวอย่างเช่นฉันมีตะกร้าสินค้าสำหรับแกลเลอรีรูปภาพซึ่งฉันต้องการให้รายการสั่งซื้อจัดเรียงตามรูปถ่าย (แต่ละรูปสามารถสั่งซื้อได้หลายครั้งและพิมพ์ขนาดต่างกัน) จากนั้นจะส่งคืนแฮชที่มีผลิตภัณฑ์ (ภาพถ่าย) เป็นคีย์และทุกครั้งที่สั่งซื้อสามารถแสดงรายการในบริบทของภาพถ่าย (หรือไม่) เมื่อใช้เทคนิคนี้คุณสามารถส่งออกประวัติการสั่งซื้อสำหรับผลิตภัณฑ์แต่ละรายการได้ ไม่แน่ใจว่าจะมีประโยชน์กับคุณหรือไม่ในบริบทนี้ แต่ฉันพบว่ามีประโยชน์มาก นี่คือรหัส

OrdersController#show
  @order = Order.find(params[:id])
  @order_items_by_photo = @order.order_items.group_by(&:photo)

@order_items_by_photo แล้วจะมีลักษณะดังนี้:

=> {#<Photo id: 128>=>[#<OrderItem id: 2, photo_id: 128>, #<OrderItem id: 19, photo_id: 128>]

คุณสามารถทำสิ่งต่างๆเช่น:

@orders_by_product = @user.orders.group_by(&:product)

จากนั้นเมื่อคุณได้รับสิ่งนี้ในมุมมองของคุณให้วนซ้ำสิ่งนี้:

- for product, orders in @user.orders_by_product
  - "#{product.name}: #{orders.size}"
  - for order in orders
    - output_order_details

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

มันอาจจะมากเกินไปสำหรับสิ่งที่คุณกำลังพยายามทำ แต่มันก็มีตัวเลือกดีๆให้กับคุณ (เช่นวันที่สั่ง ฯลฯ ) ที่จะทำงานร่วมด้วยนอกเหนือจากปริมาณ


ขอบคุณสำหรับคำตอบโดยละเอียด นี่อาจจะมากกว่าที่ฉันต้องการเล็กน้อย แต่ก็น่าสนใจที่จะเรียนรู้ (จริงๆแล้วฉันอาจใช้สิ่งนี้ที่อื่นในแอพ) คุณคิดอย่างไรกับประสิทธิภาพสำหรับแนวทางต่างๆที่กล่าวถึงในหน้านี้
Andy Harvey

3

บน Rails 6 ฉันทำให้สิ่งนี้ทำงานได้อย่างสมบูรณ์:

  has_many :regions, -> { order(:name).distinct }, through: :sites

ฉันไม่สามารถหาคำตอบอื่น ๆ ในการทำงานได้


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