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

100

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

Есть ли способ сделать поиск и сохранить порядок?

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

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

Леонид Шевцов
источник

Ответы:

71

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

Обновление: это будет удалено в исходном коде Rails 6.1 Rails.

ковырин
источник
Вы случайно не знаете эквивалент FIELDS()в Postgres?
Trung Lê
3
Я написал функцию plpgsql для этого в postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Омар Куреши
25
Это больше не работает. Для более свежих Rails:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff
2
Это лучшее решение, чем у Gunchars, потому что оно не нарушает разбиение на страницы.
pguardiario
.ids отлично работает для меня, и это довольно быстрая документация по
activerecord
79

Как ни странно, никто не предлагал ничего подобного:

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

Gunchars
источник
Итак, ваше редактирование, похоже, работает, но меня это беспокоит, порядок клавиш в хэше не гарантируется, не так ли? поэтому, когда вы вызываете slice и получаете "переупорядоченный" хэш, это действительно зависит от возвращаемых хешем значений в том порядке, в котором были добавлены его ключи. Похоже, это зависит от детали реализации, которая может измениться.
Джон
2
@Jon, порядок гарантирован в Ruby 1.9 и любой другой реализации, которая пытается ему следовать. Для версии 1.8 Rails (ActiveSupport) исправляет класс Hash, чтобы заставить его вести себя таким же образом, поэтому, если вы используете Rails, все должно быть в порядке.
Gunchars
спасибо за разъяснения, только что нашел это в документации.
Джон
13
Проблема в том, что он возвращает массив, а не отношение.
Велизар Христов
3
Отлично, однако, однострочный шрифт у меня не работает (Rails 4.1)
Беси
44

Как заявил Майк Вудхаус в своем ответе , это происходит потому, что под капотом Rails использует SQL-запрос с a 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)
Аджеди32
источник
3
второе решение (с index_by), похоже, не работает для меня, давая все нулевые результаты.
Бен Уиллер,
22

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

имбирная известь
источник
Ты мой герой недели. Спасибо!
ntdb
4
Обратите внимание, что это работает только в тривиальных случаях, вы в конечном итоге столкнетесь с ситуацией, когда ваш идентификатор содержится в других идентификаторах в списке (например, он найдет 1 из 11). Один из способов обойти это - добавить запятые в проверку позиции, а затем добавить последнюю запятую к объединению, например: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy
Хороший вопрос, @IrishDubGuy! Я обновлю свой ответ на основе вашего предложения. Спасибо!
имбирный лайм
для меня цепочка не работает. Здесь имя таблиц должно быть добавлено перед идентификатором: текст вроде этого: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] полная версия, которая у меня сработала: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } спасибо @gingerlime @IrishDubGuy
user1136228
Я полагаю, вам нужно добавить имя таблицы на случай, если вы сделаете какие-то соединения ... Это довольно часто бывает с областями ActiveRecord, когда вы присоединяетесь.
gingerlime
19

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

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

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

Джейкоб Эвелин
источник
1
Чувак, ты такой классный. Спасибо!
swrobel
5

Невозможно в 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}}
Омар Куреши
источник
Хотя это не очень эффективно, я согласен, что этот обходной путь не зависит от БД и приемлем, если у вас небольшое количество строк.
Trung Lê
Не используйте для этого инъекцию, это карта:sorted = arr.map { |val| Model.find(val) }
tokland
первый медленный. Я согласен со вторым с такой картой:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon 03
2

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

Предполагая Model.pluck(:id)возврат, [1,2,3,4]и вам нужен порядок[2,4,1,3]

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

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и т.д.)

Кристиан Фаццини
источник
2

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

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

Например:

Something.find_with_order(array_of_ids)

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

Something.where_with_order(:id, array_of_ids)
Хиав Реой
источник
1

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

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

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

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

Майк Вудхаус
источник
Обратите внимание, что хэш параметров устарел. (второй аргумент в этом примере :order => id)
ocodo
1

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

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

XML Slayer
источник
0

Что касается ответа здесь

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

Сэм Ках Чиин
источник
-4

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

текст ссылки

Ашиш Джайн
источник