ActiveRecord.find (array_of_ids), зберігаючи порядок


98

Коли ви робите 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 6.1


Чи трапляється вам знати еквівалент FIELDS()у Postgres?
Trung Lê

3
Я написав функцію plpgsql, щоб це зробити у postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Омар Куреші

25
Це більше не працює. Для останніх Object.where(id: ids).order("field(id, #{ids.join ','})")
Рейлів

2
Це краще рішення, ніж Gunchars ', оскільки воно не порушить сторінки.
pguardiario

.ids працює для мене чудово, і це досить швидка активна запис документації
TheJKFever

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


Так що, здається, твоя редакція працює, але це змушує мене нервувати порядок ключів у хеші, чи не так? тому, коли ви зателефонуєте на фрагмент і повертаєте хеш "переупорядкованим", це дійсно залежить від значень, що повертають хеш, у тому порядку, в який додані його ключі. Це відчувається як залежно від деталі реалізації, яка може змінитися.
Джон

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
Зауважте, що це працює лише в тривіальних випадках, ви зрештою зіткнетеся з ситуацією, коли ваш Id міститься в інших ідентифікаторах у списку (наприклад, він знайде 1 з 11). Один із способів цього - додати коси до перевірки позиції, а потім додати заключну кому до з'єднання, як це: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'в?) ", ids.join (', ') +', '])
IrishDubGuy

Добрий момент, @IrishDubGuy! Я оновлю свою відповідь на основі вашої пропозиції. Дякую!
пряник

для мене прикування не працює. Тут ім’я таблиць слід додати перед id: текст на кшталт цього: ["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
користувач1136228

Я думаю, вам потрібно додати ім’я таблиці у випадку, якщо ви приєднаєтесь… Це досить часто з областями ActiveRecord, коли ви приєднуєтесь.
пряник

19

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

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

Наскільки мені вдалося протестувати, він працює у всіх RDBMS, і він повертає відношення ActiveRecord, яке може бути пов'язане ланцюгом.


1
Чувак, ти такий дивовижний. Дякую!
swrobel

5

Неможливо в 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}}

Хоча я не дуже ефективний, я погоджуюся, що ця обробка є DB-агностичною і прийнятною, якщо у вас є невелика кількість рядків.
Trung Lê

Не використовуйте для цього sorted = arr.map { |val| Model.find(val) }
ін’єкцію

Перший - повільний. Я погоджуюся з другою картою на зразок такої:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
кубун

2

Відповідь @Gunchars чудова, але вона не виходить з поля Rails 2.3, оскільки клас Hash не замовлений. Простий спосіб вирішити - розширити клас Enumerable ' index_byдля використання класу OrdersHash:

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 WHENпункту SQL. Наприклад:

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масив ідентифікаторів генеруватиме SELECTaWHERE id IN... , яка має бути ефективнішою, ніж прокручування ідентифікаторів.

Таким чином, запит задовольняється за одну поїздку до бази даних, але SELECTs без 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.