it-roy-ru.com

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

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

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

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

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

91
Leonid Shevtsov

Ответ только для 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]
66
kovyrin

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

index = Something.find(array_of_ids).group_by(&:id)
array_of_ids.map { |i| index[i].first }

Столь же эффективен, как это возможно, помимо того, что позволяет SQL-серверу это делать.

Edit: Чтобы улучшить мой собственный ответ, вы также можете сделать это следующим образом:

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

#index_by и #slice - довольно удобные дополнения в ActiveSupport для массивов и хэшей соответственно.

74
Gunchars

Как сказал Майк Вудхаус в его ответе , это происходит потому, что 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)
34
Ajedi32

Кажется, это работает для 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 ?) ...

18
gingerlime

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

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

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

17
JacobEvelyn

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

Первый пример:

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}}
5
Omar Qureshi

Ответ @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

Бонус

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
Chris Bloom

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

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

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

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

1
Mike Woodhouse

Предполагая, что 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 и т.д.)

1
Christian Fazzini

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

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

1
XML Slayer

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

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

0
Sam Kah Chiin

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

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

Например:

Something.find_with_order(array_of_ids)

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

Something.where_with_order(:id, array_of_ids)
0
khiav reoy