2009-04-29 4 views
25

Я хочу получить массив объектов ActiveRecord, заданных массивом идентификаторов.Чистый способ найти объекты ActiveRecord по id в указанном порядке

Я предположил, что

Object.find([5,2,3]) 

возвращает бы массив с объектом 5, объектом 2, то объект 3 в таком порядке, но вместо этого я получаю массив заказать в качестве объекта 2, объекта 3, а затем объект 5.

База ActiveRecord find method API упоминает, что вы не должны ожидать ее в указанном порядке (другая документация не дает этого предупреждения).

Одно потенциальное решение было дано в Find by array of ids in the same order?, но опция заказа не подходит для SQLite.

Я могу написать некоторый код ruby ​​для сортировки объектов самостоятельно (несколько простых и плохо масштабируемых или лучшего масштабирования и более сложных), но есть ли лучший способ?

+1

Откуда эти идентификаторы? Если это пользовательский интерфейс (через пользователя, который выбирает их), масштабирование не должно быть проблемой, то есть пользователь вряд ли потратит время на выбор 1000 идентификаторов). Если это база данных (например, из таблицы объединений), можете ли вы сохранить заказ в таблице соединений и вычесть находку на основе этого? – pauliephonic

ответ

22

Это не то, что 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}} 
+14

Другое решение, которое более эффективно, чем использование #detect, чтобы избежать O (N) производительности: records = Model.find (ids) .group_by (&:id); sorted_records = ids.map {| id | records [id] .first} –

6

Очевидно, mySQL и другие системы управления БД самостоятельно сортируют вещи. Я думаю, что вы можете обойти эту делания:

ids = [5,2,3] 
@things = Object.find(ids, :order => "field(id,#{ids.join(',')})") 
+0

Это был ответ, предложенный в ссылке «Найти по массивам id в том же порядке?», Но, похоже, это не работает для SQLite. –

+3

И это не работает для Postgres 9 –

+2

Это больше не работает. Для более поздних Rails: 'Object.where (id: ids) .order (" field (id, # {ids.join ','}) ")' – mahemoff

-1
@things = [5,2,3].map{|id| Object.find(id)} 

Это, вероятно, самый простой способ, если вы не слишком много объектов, чтобы найти, так как он требует поездки в базу данных для каждого идентификатора.

+1

вы не должны делать этого (N + 1 запросов) – Schneems

+0

Это N запросов, а не N + 1. Есть намного лучшие способы сделать это, но, как я сказал, это самый простой. Для производительности, посмотрите их все, а затем сбросьте их в хэш с идентификаторами в качестве ключей. – jcnnghm

1

Другой (вероятно, более эффективный) способ сделать это в 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] } 
+0

вы можете используйте each_with_object, чтобы сделать этот код двумя строками FWIW. Также сделал некоторый бенчмаркинг, и похоже, что этот код быстрее _much_, если вы выполняете итерацию по большому набору. – Schneems

+0

Спасибо за ваш комментарий. Я не знал о существовании #each_with_object (http://api.rubyonrails.org/classes/Enumerable.html#method-i-each_with_object). Существует ли существенная разница между приведенным выше подходом #inject и #each_with_object? –

+0

Нет, каждый_with_object по существу такой же, как и инъекция, кроме вам не нужно возвращать объект, который вы изменяете (в вашем случае 'result') в конце вашего блока. Он упрощает создание хэшей IMHO. – Schneems

10

на основе моего предыдущего комментарий Йерун ван Дейк вы можете сделать это более эффективно и в двух линиях с использованием 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 

Update

Вы можете сделать это в большинстве используя операторы порядка и так, вот это метод класса, который вы могли бы использовать.

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] 
1

Вот самое простое, что я мог придумать:

ids = [200, 107, 247, 189] 
results = ModelObject.find(ids).group_by(&:id) 
sorted_results = ids.map {|id| results[id].first } 
6

Портативное решение было бы использовать оператор CASE, SQL в ваш заказ. Вы можете использовать почти любое выражение в 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 sanitizer methods, чтобы правильно указать свой ids.

Затем используйте order строку в качестве условия заказа:

Object.find(ids, :order => order) 

или в современном мире:

Object.where(:id => ids).order(order) 

Это немного многословным, но он должен работать так же с любой базой данных SQL и это не так сложно скрыть уродство.

+5

Я сделал это с помощью' Object.order ('cas e id '+ ids.each_with_index.map {| id, i | ", когда # {id}, затем # {i}"} .join ('') + 'end') ' –

+0

mu's не будет работать для меня, хотя Питер сделал это. +1 –

+1

или в sql injection escape, поместите это в блок карты: 'sanitize_sql_array ([" when? Then? ", Id, i])' (работает в AR-модели) – nruth

4

Как я ответил here, я только что выпустил камень (order_as_specified), что позволяет сделать родную упорядоченность SQL, как это:

Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3]) 

Просто протестирован и работает в SQLite.

3

Justin Weiss написал blog article about this problem всего два дня назад.

Кажется, это хороший подход, чтобы рассказать базе данных о предпочтительном порядке и загрузить все записи, отсортированные в этом порядке, непосредственно из базы данных. Пример из его blog article:

# 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] 
+0

Отличное решение для вставки, но применимо только к 'id'. Было бы неплохо иметь возможность динамически определять атрибут для заказа, например, 'uid' или что-то еще. –

+0

@JoshPinter: просто добавьте параметр 'field_name' в метод и используйте это' field_name' вместо '' CASE id ''следующим образом:' 'CASE # {field_name}" ' – spickermann

+0

Да, спасибо за продолжение. Думал больше о разрешении 'find_ordered_by_uid (uids)', но он просто строил то, что вы сказали. Приветствия. –

1

Вот производительный (хэш-поиска, а не O (п) поиск массива, как в детектировать!) Однострочник, а Метод:

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