ActiveRecord.find(array_of_ids), сохраняющий порядок

Когда вы выполняете Something.find(array_of_ids) в Rails, порядок результирующего массива не зависит от порядка array_of_ids.

Есть ли способ найти и сохранить заказ?

ATM. Я вручную сортирую записи по порядку идентификаторов, но это вроде ламе.

UPD: если можно указать порядок, используя параметр :order и какое-то предложение SQL, то как?

Ответ 1

Ответ только для 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 ','})")

Ответ 2

Как ни странно, никто не предложил что-то вроде этого:

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 для массивов и хешей соответственно.

Ответ 3

Как Майк Вудхаус заявил в его ответ, это происходит потому, что под капотом Rails использует SQL-запрос с WHERE id IN... clause для извлечения всех записей в одном запросе. Это быстрее, чем поиск каждого идентификатора по отдельности, но, как вы заметили, он не сохраняет порядок записей, которые вы извлекаете.

Чтобы исправить это, вы можете сортировать записи на уровне приложения в соответствии с исходным списком идентификаторов, которые вы использовали при поиске записи.

Основываясь на многих отличных ответах на Сортировка массива в соответствии с элементами другого массива, я рекомендую следующее решение:

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)

Ответ 4

Это похоже на postgresql (источник) - и возвращает отношение 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 ?)...

Ответ 5

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

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

Насколько я смог протестировать, он работает изначально во всех СУБД и возвращает отношение ActiveRecord, которое может быть связано.

Ответ 6

Невозможно в 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}}

Ответ 7

@Ответ Gunchars велик, но он не работает из коробки в Rails 2.3, потому что класс Hash не упорядочен. Простым обходным путем является расширение класса Enumerable 'index_by для использования класса 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

Теперь подход @Gunchars будет работать

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

Bonus

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)

Ответ 8

Предполагая, что Model.pluck(:id) возвращает [1,2,3,4] и вы хотите порядок [2,4,1,3]

Идея состоит в том, чтобы использовать SQL-предложение ORDER BY CASE WHEN. Например:

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

Хорошая вещь об этом. Результаты возвращаются как отношения ActiveRecord (что позволяет использовать такие методы, как last, count, where, pluck и т.д.)

Ответ 9

Под капотом find с массивом идентификаторов будет генерироваться SELECT с предложением WHERE id IN..., которое должно быть более эффективным, чем цикл через идентификаторы.

Таким образом, запрос выполняется в одной поездке в базу данных, но SELECT без ORDER BY предложения не сортируются. ActiveRecord понимает это, поэтому мы расширяем наш find следующим образом:

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

Если порядок идентификаторов в вашем массиве является произвольным и значительным (т.е. вы хотите, чтобы порядок строк возвращался в соответствии с вашим массивом независимо от последовательности идентификаторов, содержащихся в нем), тогда я думаю, что вы бы лучший сервер постобработки результаты в коде - вы могли бы построить предложение :order, но оно было бы ужасно сложным и не имело бы никакого намерения.

Ответ 10

Хотя я нигде не упоминал об этом в CHANGELOG, похоже, что эта функциональность была изменена с выпуском версии 5.2.0.

Здесь фиксируем обновление документов, помеченных как 5.2.0 Однако, похоже, что они также были перенесены в версию 5.0.

Ответ 11

Существует гем find_with_order, который позволяет вам сделать это эффективно, используя собственный SQL-запрос.

И он поддерживает как Mysql и PostgreSQL.

Например:

Something.find_with_order(array_of_ids)

Если вы хотите отношения:

Something.where_with_order(:id, array_of_ids)

Ответ 12

Со ссылкой на ответ здесь

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") работает для Postgresql.

Ответ 13

В find (: order = > '...') есть предложение order, которое делает это при извлечении записей. Вы также можете получить помощь здесь.

текст ссылки