ActiveRecord.find (array_of_ids), bảo toàn thứ tự


98

Khi bạn làm Something.find(array_of_ids)trong Rails, thứ tự của mảng kết quả không phụ thuộc vào thứ tự của array_of_ids.

Có cách nào để thực hiện việc tìm và bảo toàn thứ tự không?

ATM Tôi sắp xếp hồ sơ theo cách thủ công dựa trên thứ tự của các ID, nhưng điều đó thật là khập khiễng.

UPD: nếu có thể chỉ định thứ tự bằng cách sử dụng :orderparam và một số loại mệnh đề SQL, thì làm thế nào?


Câu trả lời:


71

Câu trả lời chỉ dành cho mysql

Có một hàm trong mysql được gọi là FIELD ()

Đây là cách bạn có thể sử dụng nó trong .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 ','})")

Cập nhật: Điều này sẽ bị xóa trong mã nguồn Rails 6.1 Rails


Bạn có tình cờ biết tương đương với FIELDS()trong Postgres không?
Trung Lê

3
Tôi đã viết một chức năng plpgsql để làm điều này trong postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi

25
Điều này không còn hoạt động. Để biết thêm các Rails gần đây:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff

2
Đây là một giải pháp tốt hơn Gunchars 'vì nó sẽ không phá vỡ phân trang.
pguardiario

.ids hoạt động tốt đối với tôi, và là khá nhanh tài liệu activerecord
TheJKFever

79

Thật kỳ lạ, không ai đề xuất điều gì đó như thế này:

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

Hiệu quả như nó có được ngoài việc cho phép SQL backend làm điều đó.

Chỉnh sửa: Để cải thiện câu trả lời của riêng tôi, bạn cũng có thể làm như sau:

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

#index_by#slicelà những bổ sung khá tiện dụng trong ActiveSupport cho các mảng và hàm băm tương ứng.


Vì vậy, chỉnh sửa của bạn có vẻ hoạt động nhưng nó khiến tôi lo lắng về thứ tự khóa trong một hàm băm không được đảm bảo phải không? vì vậy, khi bạn gọi slice và lấy lại hàm băm "được sắp xếp lại", nó thực sự phụ thuộc vào các giá trị trả về hàm băm theo thứ tự các khóa của nó được thêm vào. Điều này giống như tùy thuộc vào một chi tiết triển khai có thể thay đổi.
Jon

2
@Jon, thứ tự được đảm bảo trong Ruby 1.9 và mọi quá trình triển khai khác cố gắng tuân theo nó. Đối với 1.8, Rails (ActiveSupport) vá lớp Hash để làm cho nó hoạt động theo cách tương tự, vì vậy nếu bạn đang sử dụng Rails, bạn sẽ ổn.
Gunchars

cảm ơn vì đã làm rõ, chỉ cần tìm thấy điều đó trong tài liệu.
Jon

13
Vấn đề với điều này là nó trả về một mảng, thay vì một quan hệ.
Velizar Hristov

3
Tuyệt vời, tuy nhiên, một lớp lót không phù hợp với tôi (Rails 4.1)
Besi

44

Như Mike Woodhouse đã nêu trong câu trả lời của anh ấy , điều này xảy ra ở phần dưới, Rails đang sử dụng một truy vấn SQL với một WHERE id IN... clauseđể truy xuất tất cả các bản ghi trong một truy vấn. Điều này nhanh hơn so với việc truy xuất từng id riêng lẻ, nhưng như bạn nhận thấy, nó không bảo toàn thứ tự của các bản ghi bạn đang truy xuất.

Để khắc phục điều này, bạn có thể sắp xếp hồ sơ ở cấp ứng dụng theo danh sách ID ban đầu mà bạn đã sử dụng khi tra cứu hồ sơ.

Dựa trên nhiều câu trả lời xuất sắc cho Sắp xếp một mảng theo các phần tử của một mảng khác , tôi đề xuất giải pháp sau:

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

Hoặc nếu bạn cần thứ gì đó nhanh hơn một chút (nhưng được cho là hơi khó đọc hơn), bạn có thể làm điều này:

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

3
giải pháp thứ hai (với index_by) dường như không thành công đối với tôi, tạo ra tất cả các kết quả không.
Ben Wheeler

22

Điều này dường như hoạt động đối với postgresql ( nguồn ) - và trả về một quan hệ 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])

cũng có thể được mở rộng cho các cột khác, ví dụ: đối với namecột, sử dụng position(name::text in ?)...


Bạn là người hùng của tôi trong tuần. Cảm ơn bạn!
ntdb 29/02/16

4
Lưu ý rằng điều này chỉ hoạt động trong những trường hợp nhỏ, cuối cùng bạn sẽ gặp phải tình huống Id của bạn nằm trong các ID khác trong danh sách (ví dụ: nó sẽ tìm thấy 1 trong 11). Một cách để giải quyết vấn đề này là thêm dấu phẩy vào kiểm tra vị trí, rồi thêm dấu phẩy cuối cùng vào phép nối, như sau: order = sanitize_sql_array (["position (',' || client.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy

Điểm tốt, @IrishDubGuy! Tôi sẽ cập nhật câu trả lời của mình dựa trên đề xuất của bạn. Cảm ơn!
Gingerlime

đối với tôi chuỗi không hoạt động. Ở đây tên bảng nên được thêm trước id: văn bản như thế này: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] phiên bản đầy đủ phù hợp với tôi: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } cảm ơn @gingerlime @IrishDubGuy
user1136228

Tôi đoán bạn cần thêm tên bảng trong trường hợp bạn thực hiện một số lần tham gia ... Điều đó khá phổ biến với phạm vi ActiveRecord khi bạn tham gia.
Gingerlime

19

Như tôi đã trả lời ở đây , tôi vừa phát hành một gem ( order_as_specified ) cho phép bạn thực hiện việc sắp xếp SQL gốc như sau:

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

Theo như tôi đã có thể kiểm tra, nó hoạt động nguyên bản trong tất cả các RDBMS và nó trả về một quan hệ ActiveRecord có thể được xâu chuỗi.


1
Anh bạn, bạn thật tuyệt vời. Cảm ơn bạn!
swrobel

5

Thật không may trong SQL có thể hoạt động trong mọi trường hợp, bạn sẽ cần phải viết các tìm thấy đơn lẻ cho mỗi bản ghi hoặc thứ tự bằng ruby, mặc dù có thể có một cách để làm cho nó hoạt động bằng cách sử dụng các kỹ thuật độc quyền:

Ví dụ đầu tiên:

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

RẤT HIỆU QUẢ

Ví dụ thứ hai:

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

Mặc dù không hiệu quả lắm, tôi đồng ý rằng giải pháp này là DB-bất khả tri và có thể chấp nhận được nếu bạn có số lượng hàng nhỏ.
Trung Lê

Đừng 'sử dụng tiêm cho điều này, đó là một bản đồ:sorted = arr.map { |val| Model.find(val) }
tokland

đầu tiên là chậm. Tôi đồng ý với điều thứ hai có bản đồ như thế này:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon

2

Câu trả lời của @Gunchars là tuyệt vời, nhưng nó không hoạt động tốt trong Rails 2.3 vì lớp Hash không được sắp xếp. Một giải pháp đơn giản là mở rộng lớp Enumerable ' index_byđể sử dụng lớp OrderedHash:

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

Bây giờ cách tiếp cận của @Gunchars sẽ hoạt động

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

Tặng kem

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

Sau đó

Something.find_with_relevance(array_of_ids)

2

Giả định Model.pluck(:id)lợi nhuận[1,2,3,4] và bạn muốn thứ tự[2,4,1,3]

Khái niệm là sử dụng ORDER BY CASE WHENmệnh đề SQL. Ví dụ:

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;

Trong Rails, bạn có thể đạt được điều này bằng cách có một phương thức public trong mô hình của bạn để xây dựng một cấu trúc tương tự:

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

Sau đó làm:

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

Điều tốt về điều này. Kết quả được trả về như Quan hệ ActiveRecord (cho phép bạn sử dụng các phương pháp như last, count, where, pluck, vv)


2

Có một viên ngọc find_with_order cho phép bạn làm điều đó một cách hiệu quả bằng cách sử dụng truy vấn SQL gốc.

Và nó hỗ trợ cả MysqlPostgreSQL.

Ví dụ:

Something.find_with_order(array_of_ids)

Nếu bạn muốn quan hệ:

Something.where_with_order(:id, array_of_ids)

1

Dưới mui xe, findvới một mảng id sẽ tạo ra SELECTmột WHERE id IN...mệnh đề với, điều này sẽ hiệu quả hơn việc lặp qua các id.

Vì vậy, yêu cầu được thỏa mãn trong một lần chuyển đến cơ sở dữ liệu, nhưng SELECTcác ORDER BYmệnh đề không có s không được sắp xếp. ActiveRecord hiểu điều này, vì vậy chúng tôi mở rộng findnhư sau:

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

Nếu thứ tự id trong mảng của bạn là tùy ý và quan trọng (tức là bạn muốn thứ tự các hàng được trả về khớp với mảng của mình bất kể chuỗi id có trong đó) thì tôi nghĩ bạn nên là người phục vụ tốt nhất bằng cách xử lý hậu mã - bạn có thể xây dựng một :ordermệnh đề nhưng nó sẽ rất phức tạp và hoàn toàn không tiết lộ ý định.


Lưu ý rằng băm tùy chọn đã không được dùng nữa. (số thứ hai, trong ví dụ này :order => id)
ocodo

1

Mặc dù tôi không thấy nó được đề cập ở bất kỳ đâu trong CHANGELOG, nhưng có vẻ như chức năng này đã được thay đổi khi phát hành phiên bản5.2.0 .

Tại đây cam kết cập nhật các tài liệu được gắn thẻ 5.2.0Tuy nhiên, nó dường như cũng đã được báo cáo lại thành phiên bản 5.0.


0

Cùng tham khảo câu trả lời tại đây

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") làm việc cho Postgresql.


-4

Có một mệnh đề thứ tự trong find (: order => '...') thực hiện điều này khi tìm nạp các bản ghi. Bạn cũng có thể nhận được trợ giúp từ đây.

văn bản liên kết

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.