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


Итак, ваше редактирование, похоже, работает, но меня это беспокоит, порядок клавиш в хэше не гарантируется, не так ли? поэтому, когда вы вызываете 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)

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



Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.