지정된 순서대로 ID별로 activeRecord 객체를 찾는 깨끗한 방법
-
03-07-2019 - |
문제
ID 배열이 주어진 ActiveRecord 객체 배열을 얻고 싶습니다.
나는 그것을 가정했다
Object.find([5,2,3])
객체 5, 객체 2, 객체 3을 순서대로 반환하지만 대신 객체 2, 객체 3, 객체 5로 주문한 배열을 얻습니다.
ActiveRecord 기반 방법 API를 찾으십시오 제공된 순서대로 기대하지 않아야한다고 언급합니다 (다른 문서는이 경고를 제공하지 않습니다).
하나의 잠재적 인 해결책이 제공되었습니다 동일한 순서로 ID 배열로 찾으십니까?, 그러나 주문 옵션은 SQLITE에 유효하지 않은 것 같습니다.
나는 객체를 직접 분류하기 위해 루비 코드를 작성할 수 있지만 (다소 단순하고 제대로 스케일링하거나 더 나은 스케일링과 더 복잡한) 더 나은 방법이 있습니까?
해결책
MySQL과 다른 DBS가 스스로 분류하는 것이 아니라 분류하지 않는 것입니다. 전화 할 때 Model.find([5, 2, 3])
, 생성 된 SQL은 다음과 같습니다.
SELECT * FROM models WHERE models.id IN (5, 2, 3)
이것은 주문을 지정하지 않고 반환하려는 레코드 세트 만 지정합니다. 일반적으로 MySQL은 데이터베이스 행을 반환 할 것입니다. 'id'
주문, 그러나 이것에 대한 보장은 없습니다.
데이터베이스가 보장 된 주문으로 레코드를 반환하도록하는 유일한 방법은 주문 조항을 추가하는 것입니다. 레코드가 항상 특정 순서로 반환되는 경우 DB에 정렬 열을 추가하고 수행 할 수 있습니다. Model.find([5, 2, 3], :order => 'sort_column')
. 그렇지 않은 경우 코드에서 정렬을 수행해야합니다.
ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}}
다른 팁
Jeroen van Dijk에 대한 나의 이전 의견을 기반으로 더 효율적이고 두 줄로이 작업을 수행 할 수 있습니다. each_with_object
result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
참고로 여기에 내가 사용한 벤치 마크가 있습니다
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
end
end.real
#=> 4.45757484436035 seconds
이제 다른 하나
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
ids.collect {|id| results.detect {|result| result.id == id}}
end
end.real
# => 6.10875988006592
업데이트
순서 및 사례 문을 사용하여 대부분의 경우 사용할 수 있습니다. 여기에 사용할 수있는 클래스 방법이 있습니다.
def self.order_by_ids(ids)
order_by = ["case"]
ids.each_with_index.map do |id, index|
order_by << "WHEN id='#{id}' THEN #{index}"
end
order_by << "end"
order(order_by.join(" "))
end
# User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id)
# #=> [3,2,1]
분명히 MySQL 및 기타 DB 관리 시스템은 스스로 정렬됩니다. 나는 당신이 그 일을 우회 할 수 있다고 생각합니다.
ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
휴대용 솔루션은 주문시 SQL Case 문을 사용하는 것입니다. 순서대로 거의 모든 표현식을 사용할 수 있으며 케이스는 인라인 조회 테이블로 사용할 수 있습니다. 예를 들어, 당신이 후에 SQL이 다음과 같이 보일 것입니다.
select ...
order by
case id
when 5 then 0
when 2 then 1
when 3 then 2
end
그것은 약간의 루비로 생성하기가 매우 쉽습니다.
ids = [5, 2, 3]
order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'
위의 내용은 숫자 또는 다른 안전한 가치로 작업하고 있다고 가정합니다. ids
; 그렇지 않은 경우 사용하고 싶을 것입니다. connection.quote
또는 중 하나 Activerecord SQL 소독제 방법 당신을 올바르게 인용하려면 ids
.
그런 다음 사용하십시오 order
주문 조건으로 문자열 :
Object.find(ids, :order => order)
또는 현대 세계에서 :
Object.where(:id => ids).order(order)
이것은 약간의 장점이지만 SQL 데이터베이스와 동일하게 작동해야하며 추악함을 숨기는 것은 어렵지 않습니다.
내가 대답했던대로 여기, 방금 보석을 발표했습니다 (Order_as_specified) 이와 같이 기본 SQL 주문을 수행 할 수 있습니다.
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
방금 테스트하고 sqlite에서 작동합니다.
저스틴 와이즈는 썼다 이 문제에 대한 블로그 기사 이틀 전에.
데이터베이스에서 선호하는 순서에 대해 데이터베이스에 알리고 데이터베이스에서 직접 정렬 된 모든 레코드를로드하는 것은 좋은 방법 인 것 같습니다. 그의 예 블로그 기사:
# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
extend ActiveSupport::Concern
module ClassMethods
def find_ordered(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << "WHEN #{id} THEN #{index} "
end
order_clause << "ELSE #{ids.length} END"
where(id: ids).order(order_clause)
end
end
end
ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)
이를 통해 다음을 쓸 수 있습니다.
Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
다음은 방법으로 수행자 (Detect!)와 같이 O (N) 배열 검색이 아닌 해시 룩업이 있습니다.
def find_ordered(model, ids)
model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end
# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id) == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids
루비에서 할 수있는 또 다른 (아마도 더 효율적인) 방법 :
ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record|
result[record.id] = record
result
end
sorted_records = ids.map {|id| records_by_id[id] }
내가 생각해 낼 수있는 가장 간단한 것은 다음과 같습니다.
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
@things = [5,2,3].map{|id| Object.find(id)}
각 ID에 대한 데이터베이스로의 여행이 필요하기 때문에 찾을 객체가 너무 많지 않다고 가정 할 때 이것은 아마도 가장 쉬운 방법 일 것입니다.