Чистый способ найти объекты ActiveRecord по идентификатору в указанном порядке
Я хочу получить массив объектов ActiveRecord, заданных массивом идентификаторов.
Я предположил, что
Object.find([5,2,3])
Вернул бы массив с объектом 5, объектом 2, затем объектом 3 в этом порядке, но вместо этого я получаю массив, упорядоченный как объект 2, объект 3, а затем объект 5.
База ActiveRecord Base find method API упоминает, что вы не должны ожидать ее в указанном порядке (другая документация не дает этого предупреждения).
Одно потенциальное решение было дано в Найти по массиву идентификаторов в том же порядке?, но опция порядка не кажется действительной для SQLite.
Я могу написать код ruby для сортировки самих объектов (несколько простых и плохо масштабируемых или лучшего масштабирования и более сложных), но есть ли лучший способ?
Ответы
Ответ 1
Это не то, что MySQL и другие БД сами сортируют вещи, они не сортируют их. Когда вы вызываете 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}}
Ответ 2
Основываясь на моем предыдущем комментарии к 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
Обновить
В большинстве случаев вы можете использовать инструкции порядка и case, вот метод класса, который вы могли бы использовать.
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]
Ответ 3
По-видимому, mySQL и другие системы управления БД сами сортируют вещи. Я думаю, что вы можете обойти это:
ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
Ответ 4
Переносимым решением было бы использовать оператор SQL CASE в вашем ORDER BY. Вы можете использовать почти любое выражение в ORDER BY, а CASE можно использовать как встроенную таблицу поиска. Например, SQL, после которого будет выглядеть так:
select ...
order by
case id
when 5 then 0
when 2 then 1
when 3 then 2
end
Это довольно легко создать с помощью Ruby:
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, и это не так сложно скрыть уродство.
Ответ 5
Как я ответил здесь, я только что выпустили драгоценный камень ( order_as_specifiedа > ), который позволяет выполнять собственный SQL-заказ следующим образом:
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
Просто протестирован и работает в SQLite.
Ответ 6
Джастин Вайс написал статью статью в блоге об этой проблеме всего два дня назад.
Кажется, это хороший подход, чтобы сообщить базе данных о предпочтительном порядке и загрузить все записи, отсортированные в этом порядке непосредственно из базы данных. Пример из его статьи блога:
# 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]
Ответ 7
Другой (возможно, более эффективный) способ сделать это в Ruby:
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] }
Ответ 8
Вот простейшая вещь, которую я мог бы придумать:
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
Ответ 9
Здесь выполняется поиск (хэш-поиск, а не 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
Ответ 10
@things = [5,2,3].map{|id| Object.find(id)}
Это, пожалуй, самый простой способ, предполагая, что у вас слишком много объектов для поиска, поскольку для каждого идентификатора требуется поездка в базу данных.