ActiveRecord.find (array_of_ids) รักษาการสั่งซื้อ


101

เมื่อคุณทำSomething.find(array_of_ids)ใน Rails, array_of_idsสั่งซื้อสินค้าของอาร์เรย์ที่เกิดขึ้นไม่ได้ขึ้นอยู่กับคำสั่งของ

มีวิธีใดในการค้นหาและรักษาคำสั่งซื้อหรือไม่?

ATM ฉันจัดเรียงระเบียนด้วยตนเองตามลำดับของ ID แต่นั่นเป็นเรื่องง่อย

UPD: ถ้าเป็นไปได้ที่จะระบุคำสั่งโดยใช้:orderพารามิเตอร์และส่วนคำสั่ง SQL บางประเภทแล้วจะทำอย่างไร?


คำตอบ:


71

คำตอบสำหรับ mysql เท่านั้น

มีฟังก์ชันใน mysql เรียกว่าFIELD ()

นี่คือวิธีที่คุณสามารถใช้ใน. find ():

>> ids = [100, 1, 6]
=> [100, 1, 6]

>> WordDocument.find(ids).collect(&:id)
=> [1, 6, 100]

>> WordDocument.find(ids, :order => "field(id, #{ids.join(',')})")
=> [100, 1, 6]

For new Version
>> WordDocument.where(id: ids).order("field(id, #{ids.join ','})")

อัปเดต: สิ่งนี้จะถูกลบออกในซอร์สโค้ด Rails 6.1 Rails


คุณรู้หรือไม่ว่าเทียบเท่าFIELDS()ใน Postgres?
Trung Lê

3
ฉันเขียนฟังก์ชัน plpgsql เพื่อทำสิ่งนี้ใน postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi

25
สิ่งนี้ใช้ไม่ได้อีกต่อไป สำหรับ Rails ล่าสุด:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff

2
นี่เป็นทางออกที่ดีกว่าของ Gunchars เพราะจะไม่แบ่งเลขหน้า
pguardiario

.ids ใช้งานได้ดีสำหรับฉันและเป็นเอกสารบันทึกที่ใช้งาน
TheJKFever

80

แปลกไม่มีใครแนะนำอะไรแบบนี้:

index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }

มีประสิทธิภาพเท่าที่จะได้รับนอกเหนือจากการให้แบ็กเอนด์ SQL ทำ

แก้ไข:เพื่อปรับปรุงคำตอบของฉันเองคุณสามารถทำได้เช่นนี้:

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

#index_byและ#sliceเป็นส่วนเพิ่มเติมที่มีประโยชน์ในActiveSupportสำหรับอาร์เรย์และแฮชตามลำดับ


ดังนั้นการแก้ไขของคุณดูเหมือนจะใช้งานได้ แต่มันทำให้ฉันกังวลเกี่ยวกับลำดับคีย์ในแฮชไม่รับประกันใช่ไหม ดังนั้นเมื่อคุณเรียก slice และรับแฮชกลับ "สั่งซื้อใหม่" จริงๆแล้วมันขึ้นอยู่กับค่าที่ส่งคืนแฮชตามลำดับที่มีการเพิ่มคีย์ สิ่งนี้ให้ความรู้สึกเหมือนขึ้นอยู่กับรายละเอียดการใช้งานที่อาจเปลี่ยนแปลงได้
จอน

2
@ จอนรับประกันคำสั่งซื้อใน Ruby 1.9 และทุกการใช้งานอื่น ๆ ที่พยายามทำตาม สำหรับ 1.8 Rails (ActiveSupport) จะแพตช์คลาส Hash เพื่อให้ทำงานในลักษณะเดียวกันดังนั้นหากคุณใช้ Rails คุณควรจะโอเค
มือปืน

ขอบคุณสำหรับคำชี้แจงพบว่าในเอกสาร
จอน

14
ปัญหานี้คือมันส่งคืนอาร์เรย์แทนที่จะเป็นความสัมพันธ์
Velizar Hristov

3
เยี่ยมมาก แต่ซับเดียวไม่ได้ผลสำหรับฉัน (Rails 4.1)
Besi

44

ดังที่Mike Woodhouseระบุไว้ในคำตอบของเขาสิ่งนี้เกิดขึ้นเนื่องจากภายใต้ประทุน Rails กำลังใช้แบบสอบถาม SQL กับ a WHERE id IN... clauseเพื่อดึงข้อมูลทั้งหมดในแบบสอบถามเดียว ซึ่งเร็วกว่าการดึงข้อมูลทีละ id แต่อย่างที่คุณสังเกตเห็นว่ามันไม่ได้รักษาลำดับของระเบียนที่คุณกำลังเรียกค้น

ในการแก้ไขปัญหานี้คุณสามารถจัดเรียงระเบียนในระดับแอปพลิเคชันตามรายการ ID ดั้งเดิมที่คุณใช้เมื่อค้นหาระเบียน

จากคำตอบที่ยอดเยี่ยมมากมายในการจัดเรียงอาร์เรย์ตามองค์ประกอบของอาร์เรย์อื่นฉันขอแนะนำวิธีแก้ปัญหาต่อไปนี้:

Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id}

หรือถ้าคุณต้องการอะไรที่เร็วกว่านี้ (แต่อ่านได้ค่อนข้างน้อย) คุณสามารถทำได้:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)

3
วิธีที่สอง (พร้อม index_by) ดูเหมือนจะล้มเหลวสำหรับฉันทำให้ผลลัพธ์เป็นศูนย์ทั้งหมด
Ben Wheeler

23

ดูเหมือนว่าจะใช้ได้กับpostgresql ( แหล่งที่มา ) - และส่งคืนความสัมพันธ์ ActiveRecord

class Something < ActiveRecrd::Base

  scope :for_ids_with_order, ->(ids) {
    order = sanitize_sql_array(
      ["position((',' || id::text || ',') in ?)", ids.join(',') + ',']
    )
    where(:id => ids).order(order)
  }    
end

# usage:
Something.for_ids_with_order([1, 3, 2])

สามารถขยายสำหรับคอลัมน์อื่น ๆ ได้เช่นกันเช่นสำหรับnameคอลัมน์ใช้position(name::text in ?)...


คุณคือฮีโร่ประจำสัปดาห์ของฉัน ขอบคุณ!
ntdb

4
โปรดทราบว่าสิ่งนี้ใช้ได้เฉพาะในกรณีที่ไม่สำคัญในที่สุดคุณจะต้องเผชิญกับสถานการณ์ที่ Id ของคุณอยู่ใน ID อื่น ๆ ในรายการ (เช่นจะพบ 1 ใน 11) วิธีหนึ่งในการแก้ปัญหานี้คือการเพิ่มเครื่องหมายจุลภาคลงในการตรวจสอบตำแหน่งจากนั้นเพิ่มเครื่องหมายจุลภาคสุดท้ายในการเข้าร่วมดังนี้: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy

จุดดี @IrishDubGuy! ฉันจะอัปเดตคำตอบตามคำแนะนำของคุณ ขอบคุณ!
Gingerlime

สำหรับฉันการผูกมัดไม่ได้ผล ควรเพิ่มชื่อตารางที่นี่ก่อน id: ข้อความเช่นนี้: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] เวอร์ชันเต็มที่ใช้ได้กับฉัน: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } ขอบคุณ @gingerlime @IrishDubGuy
user1136228

ฉันเดาว่าคุณต้องเพิ่มชื่อตารางในกรณีที่คุณทำการรวมบางอย่าง ... ซึ่งเป็นเรื่องธรรมดาสำหรับขอบเขต ActiveRecord เมื่อคุณเข้าร่วม
Gingerlime

19

ดังที่ฉันตอบที่นี่ฉันเพิ่งเปิดตัว gem ( order_as_specified ) ที่ให้คุณทำการสั่ง SQL แบบเนทีฟดังนี้:

Something.find(array_of_ids).order_as_specified(id: array_of_ids)

เท่าที่ฉันสามารถทดสอบได้มันทำงานโดยกำเนิดใน RDBMS ทั้งหมดและส่งคืนความสัมพันธ์ ActiveRecord ที่สามารถผูกมัดได้


1
เพื่อนคุณสุดยอดมาก ขอบคุณ!
swrobel

5

เป็นไปไม่ได้ใน SQL ที่จะใช้งานได้ในทุกกรณีคุณอาจต้องเขียนการค้นหาเดี่ยวสำหรับแต่ละเร็กคอร์ดหรือคำสั่งเป็นทับทิมแม้ว่าอาจมีวิธีทำให้ใช้งานได้โดยใช้เทคนิคที่เป็นกรรมสิทธิ์:

ตัวอย่างแรก:

sorted = arr.inject([]){|res, val| res << Model.find(val)}

ไม่มีประสิทธิภาพมาก

ตัวอย่างที่สอง:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}

แม้ว่าจะไม่ได้มีประสิทธิภาพมากนัก แต่ฉันยอมรับว่าการแก้ปัญหานี้เป็นแบบไม่เชื่อเรื่องพระเจ้าและยอมรับได้หากคุณมีแถวจำนวนน้อย
Trung Lê

อย่าใช้การฉีดสำหรับสิ่งนี้มันเป็นแผนที่:sorted = arr.map { |val| Model.find(val) }
tokland

อันแรกช้า ฉันเห็นด้วยกับแผนที่ที่สองดังนี้sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon

3

มีfind_with_order gem ซึ่งช่วยให้คุณทำมันได้อย่างมีประสิทธิภาพโดยใช้แบบสอบถาม SQL ดั้งเดิม

และรองรับทั้งMysqlและPostgreSQL.

ตัวอย่างเช่น:

Something.find_with_order(array_of_ids)

หากคุณต้องการความสัมพันธ์:

Something.where_with_order(:id, array_of_ids)

2

คำตอบของ @Gunchars นั้นยอดเยี่ยม แต่มันไม่ได้ผลใน Rails 2.3 เนื่องจากคลาส Hash ไม่ได้รับคำสั่ง วิธีแก้ปัญหาง่ายๆคือการขยายคลาส Enumerable ' index_byเพื่อใช้คลาส OrderHash:

module Enumerable
  def index_by_with_ordered_hash
    inject(ActiveSupport::OrderedHash.new) do |accum, elem|
      accum[yield(elem)] = elem
      accum
    end
  end
  alias_method_chain :index_by, :ordered_hash
end

ตอนนี้แนวทางของ @Gunchars จะได้ผล

Something.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values

โบนัส

module ActiveRecord
  class Base
    def self.find_with_relevance(array_of_ids)
      array_of_ids = Array(array_of_ids) unless array_of_ids.is_a?(Array)
      self.find(array_of_ids).index_by(&:id).slice(*array_of_ids).values
    end
  end
end

แล้ว

Something.find_with_relevance(array_of_ids)

2

สมมติว่าModel.pluck(:id)ส่งคืน[1,2,3,4]และคุณต้องการลำดับของ[2,4,1,3]

แนวคิดคือการใช้ส่วนORDER BY CASE WHENคำสั่ง SQL ตัวอย่างเช่น:

SELECT * FROM colors
  ORDER BY
  CASE
    WHEN code='blue' THEN 1
    WHEN code='yellow' THEN 2
    WHEN code='green' THEN 3
    WHEN code='red' THEN 4
    ELSE 5
  END, name;

ใน Rails คุณสามารถทำได้โดยใช้วิธีการสาธารณะในโมเดลของคุณเพื่อสร้างโครงสร้างที่คล้ายกัน:

def self.order_by_ids(ids)
  if ids.present?
    order_by = ["CASE"]
    ids.each_with_index do |id, index|
      order_by << "WHEN id='#{id}' THEN #{index}"
    end
    order_by << "END"
    order(order_by.join(" "))
  end
else
  all # If no ids, just return all
end

จากนั้นทำ:

ordered_by_ids = [2,4,1,3]

results = Model.where(id: ordered_by_ids).order_by_ids(ordered_by_ids)

results.class # Model::ActiveRecord_Relation < ActiveRecord::Relation

สิ่งที่ดีเกี่ยวกับเรื่องนี้ ผลการค้นหาจะกลับมาเป็น ActiveRecord สัมพันธ์ (ช่วยให้คุณใช้วิธีการเช่นlast, count, where, pluckฯลฯ )


1

ภายใต้ประทุนfindด้วยอาร์เรย์ของรหัสจะสร้าง a ที่SELECTมีWHERE id IN...อนุประโยคซึ่งน่าจะมีประสิทธิภาพมากกว่าการวนซ้ำรหัส

ดังนั้นคำขอจึงเป็นที่พอใจในการเดินทางไปยังฐานข้อมูลครั้งเดียว แต่SELECTs ที่ไม่มีORDER BYอนุประโยคจะไม่ถูกเรียงลำดับ ActiveRecord เข้าใจสิ่งนี้ดังนั้นเราจึงขยายความfindดังนี้:

Something.find(array_of_ids, :order => 'id')

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


โปรดทราบว่าแฮชตัวเลือกถูกเลิกใช้งานแล้ว (อาร์กิวเมนต์ที่สองในตัวอย่างนี้:order => id)
ocodo

1

แม้ว่าฉันจะไม่เห็นมันกล่าวถึงที่ใดใน CHANGELOG แต่ดูเหมือนว่าฟังก์ชันนี้จะเปลี่ยนไปเมื่อมีการเปิดตัวเวอร์ชัน5.2.0กับการเปิดตัวของรุ่น

นี่กระทำการปรับปรุงเอกสารติดแท็กด้วย5.2.0แต่ดูเหมือนว่ามันจะยังได้รับการbackported5.0เข้าสู่รุ่น



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