ActiveRecord.find (array_of_ids), zachowując kolejność

98

Kiedy robisz to Something.find(array_of_ids)w Railsach, kolejność wynikowej tablicy nie zależy od kolejności array_of_ids.

Czy jest jakiś sposób, aby znaleźć i zachować zamówienie?

Bankomat I ręcznie sortuję rekordy według kolejności identyfikatorów, ale to trochę kiepskie.

UPD: jeśli możliwe jest określenie kolejności za pomocą :orderparametru i jakiejś klauzuli SQL, to w jaki sposób?

Leonid Shevtsov
źródło

Odpowiedzi:

71

Odpowiedź dotyczy tylko mysql

W mysql znajduje się funkcja o nazwie FIELD ()

Oto, jak możesz go użyć w .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 ','})")

Aktualizacja: To zostanie usunięte z kodu źródłowego Rails 6.1 Rails

kovyrin
źródło
Czy znasz odpowiednik FIELDS()Postgres?
Trung Lê
3
Napisałem funkcję plpgsql, aby to zrobić w postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi
25
To już nie działa. Nowsze Railsy:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff
2
To lepsze rozwiązanie niż Gunchars, ponieważ nie zepsuje paginacji.
pguardiario
.ids działa dobrze dla mnie i jest dość szybką dokumentacją
activerecord
79

Co dziwne, nikt nie zasugerował czegoś takiego:

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

Tak wydajny, jak to tylko możliwe, oprócz tego, że pozwala na to backendowi SQL.

Edycja: aby poprawić własną odpowiedź, możesz to również zrobić w ten sposób:

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

#index_byi #slicesą całkiem przydatnymi dodatkami w ActiveSupport odpowiednio dla tablic i skrótów .

Gunchars
źródło
Wydaje się, że twoja edycja działa, ale denerwuje mnie kolejność kluczy w skrócie, czy nie jest to gwarantowane? więc kiedy wywołujesz wycinek i otrzymujesz skrót z powrotem „ponownie uporządkowany”, to naprawdę zależy to od wartości zwracanej przez hash w kolejności, w jakiej zostały dodane klucze. Wydaje się, że zależy to od szczegółów implementacji, które mogą ulec zmianie.
Jon,
2
@Jon, kolejność jest gwarantowana w Rubim 1.9 i każdej innej implementacji, która stara się go przestrzegać. Dla 1.8, Rails (ActiveSupport) łata klasę Hash, aby zachowywała się w ten sam sposób, więc jeśli używasz Railsów, powinno być dobrze.
Gunchars,
dzięki za wyjaśnienie, właśnie znalazłem to w dokumentacji.
Jon
13
Problem polega na tym, że zwraca tablicę, a nie relację.
Velizar Hristov
3
Świetnie, jednak jednoliniowiec nie działa u mnie (Rails 4.1)
Besi
44

Jak stwierdził Mike Woodhouse w swojej odpowiedzi , dzieje się tak, ponieważ pod maską Railsy używają zapytania SQL z a, WHERE id IN... clauseaby pobrać wszystkie rekordy w jednym zapytaniu. Jest to szybsze niż pobieranie każdego identyfikatora z osobna, ale jak zauważyłeś, nie zachowuje kolejności pobieranych rekordów.

Aby to naprawić, możesz posortować rekordy na poziomie aplikacji zgodnie z oryginalną listą identyfikatorów użytych podczas wyszukiwania rekordu.

Bazując na wielu doskonałych odpowiedziach na sortowanie tablicy według elementów innej tablicy , polecam następujące rozwiązanie:

Something.find(array_of_ids).sort_by{|thing| array_of_ids.index thing.id}

Lub jeśli potrzebujesz czegoś nieco szybszego (ale prawdopodobnie nieco mniej czytelnego), możesz zrobić to:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Ajedi32
źródło
3
Wydaje się, że drugie rozwiązanie (z index_by) zawodzi, dając wszystkie wyniki zerowe.
Ben Wheeler,
22

Wydaje się, że to działa dla postgresql ( źródło ) - i zwraca relację 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])

można rozszerzyć również o inne kolumny, np. o namekolumnę, użyj position(name::text in ?)...

imbir
źródło
Jesteś moim bohaterem tygodnia. Dziękuję Ci!
ntdb
4
Zauważ, że działa to tylko w trywialnych przypadkach, w końcu napotkasz sytuację, w której twój identyfikator jest zawarty w innych identyfikatorach na liście (np. Znajdzie 1 na 11). Jednym ze sposobów obejścia tego jest dodanie przecinków do sprawdzenia pozycji, a następnie dodanie ostatniego przecinka do złączenia, na przykład: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy
Słuszna uwaga, @IrishDubGuy! Zaktualizuję moją odpowiedź na podstawie Twojej sugestii. Dzięki!
gingerlime
dla mnie łańcuchy nie działają. Tutaj należy dodać nazwę tabel przed id: tekst w ten sposób: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] pełna wersja, która działała dla mnie: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } dzięki @gingerlime @IrishDubGuy
user1136228
Wydaje mi się, że musisz dodać nazwę tabeli na wypadek, gdybyś wykonał jakieś łączenia ... Jest to dość powszechne w przypadku zakresów ActiveRecord podczas łączenia.
gingerlime
19

Jak odpowiedziałem tutaj , właśnie opublikowałem gem ( order_as_specified ), który pozwala na natywne porządkowanie SQL w następujący sposób:

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

O ile mogłem to przetestować, działa natywnie we wszystkich RDBMS i zwraca relację ActiveRecord, którą można połączyć.

JacobEvelyn
źródło
1
Koleś, jesteś niesamowity. Dziękuję Ci!
swrobel
5

Nie jest to możliwe w języku SQL, który działałby niestety we wszystkich przypadkach, musiałbyś albo napisać pojedyncze znaleziska dla każdego rekordu lub zamówienia w języku Ruby, chociaż prawdopodobnie istnieje sposób, aby działał przy użyciu zastrzeżonych technik:

Pierwszy przykład:

sorted = arr.inject([]){|res, val| res << Model.find(val)}

BARDZO NIEWYDAJNE

Drugi przykład:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
Omar Qureshi
źródło
Chociaż niezbyt wydajne, zgadzam się, że to obejście jest niezależne od DB i akceptowalne, jeśli masz małą liczbę wierszy.
Trung Lê
Nie używaj do tego inject, to mapa:sorted = arr.map { |val| Model.find(val) }
tokland
pierwszy jest powolny. Zgadzam się z drugim z mapą taką:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon
2

Odpowiedź @Gunchars jest świetna, ale nie działa po wyjęciu z pudełka w Railsach 2.3, ponieważ klasa Hash nie jest uporządkowana. Prostym obejściem jest rozszerzenie klasy Enumerable 'w index_bycelu użycia klasy 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

Teraz podejście @Gunchars będzie działać

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

Premia

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

Następnie

Something.find_with_relevance(array_of_ids)
Chris Bloom
źródło
2

Zakładając Model.pluck(:id)zwroty [1,2,3,4]i chcesz zamówić[2,4,1,3]

Koncepcja polega na wykorzystaniu ORDER BY CASE WHENklauzuli SQL. Na przykład:

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;

W Railsach możesz to osiągnąć, mając w modelu publiczną metodę konstruowania podobnej struktury:

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

Następnie wykonaj:

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

Dobra rzecz w tym. Wyniki są zwracane jako ActiveRecord Relations (co pozwala na stosowanie metod, takich jak last, count, where, pluck, etc)

Christian Fazzini
źródło
2

Istnieje gem find_with_order, który pozwala zrobić to wydajnie za pomocą natywnego zapytania SQL.

Obsługuje zarówno Mysqli PostgreSQL.

Na przykład:

Something.find_with_order(array_of_ids)

Jeśli chcesz relacji:

Something.where_with_order(:id, array_of_ids)
khiav reoy
źródło
1

Pod maską findtablica identyfikatorów wygeneruje klauzulę SELECTz WHERE id IN...klauzulą, która powinna być bardziej wydajna niż przechodzenie przez pętle identyfikatorów.

Tak więc żądanie jest spełnione w jednej podróży do bazy danych, ale wiadomości SELECTbez ORDER BYklauzul są nieposortowane. ActiveRecord to rozumie, więc rozszerzamy nasze findw następujący sposób:

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

Jeśli kolejność identyfikatorów w Twojej tablicy jest dowolna i znacząca (tj. Chcesz, aby kolejność zwracanych wierszy pasowała do Twojej tablicy niezależnie od sekwencji zawartych w niej identyfikatorów), to myślę, że najlepszym serwerem będzie przetwarzanie końcowe wyników w kod - możesz zbudować :orderklauzulę, ale byłoby to piekielnie skomplikowane i wcale nie ujawniające intencji.

Mike Woodhouse
źródło
Zwróć uwagę, że skrót opcji został wycofany. (drugi argument w tym przykładzie :order => id)
ocodo
1

Chociaż nie widzę tego w żadnym miejscu w dzienniku zmian, wygląda na to, że ta funkcjonalność została zmieniona wraz z wydaniem wersji 5.2.0.

Tutaj zatwierdź aktualizację dokumentów oznaczonych tagiem 5.2.0Jednak wydaje się, że również zostały przeniesione do wersji 5.0.

XML Slayer
źródło
0

W nawiązaniu do odpowiedzi tutaj

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") działa dla Postgresql.

Sam Kah Chiin
źródło
-4

W find (: order => '...') znajduje się klauzula order, która robi to podczas pobierania rekordów. Tutaj również możesz uzyskać pomoc.

tekst linku

Ashish Jain
źródło