ActiveRecord.find (array_of_ids), 순서 유지


98

Something.find(array_of_ids)Rails에서 수행 할 때 결과 배열의 순서는array_of_ids .

주문을 찾고 보존 할 수있는 방법이 있습니까?

ATM ID 순서에 따라 수동으로 레코드를 정렬하지만 다소 절름발이입니다.

UPD : :orderparam과 어떤 종류의 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에 해당하는 것을 알고 있습니까?
트룽 Lê

3
- 나는 포스트 그레스에서이 작업을 수행 할 수있는 plpgsql 기능 썼다 omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
오마르 쿠 레시

25
더 이상 작동하지 않습니다. 더 최근의 Rails :Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff

2
이것은 페이지 매김을 깨지 않기 때문에 Gunchars보다 나은 솔루션입니다.
pguardiario 2015-06-28

.ids는 저에게 잘 작동하고 꽤 빠른
액티브 레코드

79

이상하게도 아무도 다음과 같은 제안을하지 않았습니다.

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를 호출하고 해시를 다시 "재정렬"하면 키가 추가 된 순서대로 값을 반환하는 해시에 따라 달라집니다. 이것은 변경 될 수있는 구현 세부 사항에 의존하는 것처럼 느껴집니다.
Jon

2
@Jon, 순서는 Ruby 1.9와이를 따르는 다른 모든 구현에서 보장됩니다. 1.8의 경우 Rails (ActiveSupport)는 Hash 클래스를 패치하여 동일한 방식으로 작동하도록하므로 Rails를 사용하는 경우 괜찮습니다.
Gunchars 2013 년

설명을 해주셔서 감사합니다. 방금 문서에서 찾았습니다.

13
이것의 문제는 관계가 아닌 배열을 반환한다는 것입니다.
Velizar Hristov

3
위대한, 그러나, 한 줄은 나를 위해 작업 (4.1 레일)하지 않습니다
Besi

44

마이크 우드 하우스가 에 명시된 그의 대답 이는 후드 아래, 레일이 가진 SQL 쿼리를 사용하고, becase 발생 WHERE id IN... clause한 쿼리에서 모든 레코드를 검색 할 수 있습니다. 이 방법은 각 ID를 개별적으로 검색하는 것보다 빠르지 만 검색중인 레코드의 순서를 유지하지 않습니다.

이 문제를 해결하기 위해 레코드를 조회 할 때 사용한 원래 ID 목록에 따라 애플리케이션 수준에서 레코드를 정렬 할 수 있습니다.

Sort an array of the elements of another array 에 대한 많은 훌륭한 답변을 기반으로 다음 솔루션을 권장합니다.

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

22

이것은 postgresql ( source ) 에서 작동하는 것으로 보이며 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가 포함되어있는 상황 (예 : 11 개 중 1 개를 찾음)에 직면하게됩니다. 이 문제를 해결하는 한 가지 방법은 위치 확인에 쉼표를 추가 한 다음 다음과 같이 마지막 쉼표를 조인에 추가하는 것입니다. order = sanitize_sql_array ([ "position ( ','|| clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy

@IrishDubGuy, 좋은 지적입니다! 귀하의 제안에 따라 답변을 업데이트하겠습니다. 감사!
gingerlime

나를 위해 체인이 작동하지 않습니다. 여기에 테이블 이름이 id : text 앞에 추가되어야합니다. ["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 17.16.16

19

여기 에서 대답했듯이 다음과 같이 네이티브 SQL 주문을 수행 할 수 있는 gem ( order_as_specified )을 출시했습니다 .

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}}

매우 효율적이지는 않지만이 해결 방법이 DB에 구애받지 않고 적은 양의 행이있는 경우 허용된다는 데 동의합니다.
트룽 Lê

이것에 대한 내 청춘 '사용 분사, 그것은지도의 :sorted = arr.map { |val| Model.find(val) }
tokland

첫 번째는 느립니다. 나는 다음과 같은지도와 함께 두 번째 것에 동의합니다 :sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon

2

@Gunchars 대답은 훌륭하지만 Hash 클래스가 주문되지 않았기 때문에 Rails 2.3에서 상자 밖으로 작동하지 않습니다. 간단한 해결 방법은 index_byOrderedHash 클래스를 사용하도록 Enumerable 클래스를 확장하는 것입니다 .

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 WHENSQL 절 을 활용하는 것 입니다. 예를 들면 :

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

이것에 대한 좋은 점. 결과는 액티브 관계로 반환됩니다 (이 같은 방법을 사용할 수 있도록 last, count, where, pluck, 등)


2

보석 find_with_order가 있습니다네이티브 SQL 쿼리를 사용하여 효율적으로 수행 할 수 가 있습니다.

그리고 그것은 MysqlPostgreSQL.

예를 들면 :

Something.find_with_order(array_of_ids)

관계를 원하는 경우 :

Something.where_with_order(:id, array_of_ids)

1

내부적 find으로 id 배열을 사용하면 SELECTwith WHERE id IN...절이 생성되며 , 이는 id를 반복하는 것보다 더 효율적이어야합니다.

따라서 요청은 데이터베이스에 대한 한 번의 여행에서 충족되지만 절이 SELECT없는 s ORDER BY는 정렬되지 않습니다. ActiveRecord는이를 이해하므로 find다음과 같이 확장 합니다.

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

배열의 ID 순서가 임의적이고 중요한 경우 (즉, 배열에 포함 된 ID 순서에 관계없이 배열과 일치하도록 반환 된 행의 순서를 원하는 경우) 결과를 사후 처리하여 최상의 서버가 될 것이라고 생각합니다. 코드- :order절을 작성할 수는 있지만 매우 복잡하고 의도를 드러내지 않습니다.


옵션 해시는 더 이상 사용되지 않습니다. (이 예에서 두 번째 인수 :order => id)
ocodo

1

CHANGELOG 어디에도 언급되어 있지는 않지만 버전 출시 와 함께이 기능 이 변경된 것 같습니다.5.2.0 .

여기에서 태그가 붙은 문서를 업데이트하는 것을 커밋 하십시오. 5.2.0그러나 버전 으로 백 포트 된 것으로 보입니다 5.0.


0

여기 에 대한 답변을 참조하여

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") Postgresql에서 작동합니다.


-4

find (: order => '...')에는 레코드를 가져올 때이를 수행하는 order 절이 있습니다. 여기에서도 도움을받을 수 있습니다.

링크 텍스트

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.