ActiveRecord.find (array_of_ids), mantenendo l'ordine


98

Quando lo fai Something.find(array_of_ids) in Rails, l'ordine dell'array risultante non dipende dall'ordine di array_of_ids.

C'è un modo per trovare e preservare l'ordine?

ATM Ordino manualmente i record in base all'ordine degli ID, ma è un po 'noioso.

UPD: se è possibile specificare l'ordine utilizzando il :orderparametro e una sorta di clausola SQL, allora come?


Risposte:


71

La risposta è solo per mysql

C'è una funzione in mysql chiamata FIELD ()

Ecco come potresti usarlo in .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 ','})")

Aggiornamento: verrà rimosso nel codice sorgente di Rails 6.1 Rails


Conosci per caso l'equivalente di FIELDS()in Postgres?
Trung Lê

3
Ho scritto una funzione plpgsql per farlo in postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi,

25
Non funziona più. Per Rails più recenti:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff

2
Questa è una soluzione migliore di quella di Gunchars perché non interromperà l'impaginazione.
pguardiario

.ids funziona bene per me, ed è abbastanza veloce documentare la registrazione
TheJKFever

79

Stranamente, nessuno ha suggerito qualcosa del genere:

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

Il più efficiente possibile oltre a lasciare che il backend SQL lo faccia.

Modifica: per migliorare la mia risposta, puoi anche farlo in questo modo:

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

#index_bye #slicesono aggiunte piuttosto utili in ActiveSupport rispettivamente per array e hash.


Quindi la tua modifica sembra funzionare ma mi rende nervoso l'ordine dei tasti in un hash non è garantito, vero? così quando si chiama slice e si ottiene nuovamente l'hash "riordinato", dipende davvero dai valori di ritorno dell'hash nell'ordine in cui sono state aggiunte le chiavi. Sembra dipendere da un dettaglio di implementazione che potrebbe cambiare.
Jon

2
@ Jon, l'ordine è garantito in Ruby 1.9 e in ogni altra implementazione che cerca di seguirlo. Per 1.8, Rails (ActiveSupport) applica una patch alla classe Hash per farla comportarsi allo stesso modo, quindi se stai usando Rails, dovresti essere a posto.
Gunchars

grazie per il chiarimento, l'ho appena trovato nella documentazione.
Jon

13
Il problema con questo è che restituisce un array, piuttosto che una relazione.
Velizar Hristov

3
Fantastico, tuttavia, la battuta non funziona per me (Rails 4.1)
Besi

44

Come affermato Mike Woodhouse nella sua risposta , ciò si verifica perché, sotto il cofano, Rails sta usando una query SQL con estensioneWHERE id IN... clause per recuperare tutti i record in una query. Questo è più veloce del recupero di ogni ID individualmente, ma come hai notato non conserva l'ordine dei record che stai recuperando.

Per risolvere questo problema, puoi ordinare i record a livello di applicazione in base all'elenco originale di ID che hai utilizzato durante la ricerca del record.

Basato sulle molte risposte eccellenti per ordinare un array in base agli elementi di un altro array , consiglio la seguente soluzione:

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

O se hai bisogno di qualcosa di un po 'più veloce (ma probabilmente un po' meno leggibile) potresti farlo:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)

3
la seconda soluzione (con index_by) sembra fallire per me, producendo tutti risultati nulli.
Ben Wheeler

22

Questo sembra funzionare per postgresql ( sorgente ) e restituisce una relazione 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])

può essere esteso anche ad altre colonne, ad esempio per la namecolonna, utilizzare position(name::text in ?)...


Sei il mio eroe della settimana. Grazie!
ntdb

4
Nota che questo funziona solo in casi banali, alla fine ti imbatterai in una situazione in cui il tuo ID è contenuto all'interno di altri ID nell'elenco (ad esempio, troverà 1 su 11). Un modo per aggirare questo è aggiungere le virgole nel controllo della posizione, quindi aggiungere una virgola finale al join, in questo modo: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy

Buon punto, @IrishDubGuy! Aggiornerò la mia risposta in base al tuo suggerimento. Grazie!
gingerlime

per me il concatenamento non funziona. Qui il nome delle tabelle dovrebbe essere aggiunto prima di id: testo come questo: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] versione completa che ha funzionato per me: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } grazie @gingerlime @IrishDubGuy
user1136228

Immagino che tu debba aggiungere il nome della tabella nel caso in cui esegui alcuni join ... Questo è abbastanza comune con gli ambiti ActiveRecord quando ti unisci.
gingerlime

19

Come ho risposto qui , ho appena rilasciato una gemma ( order_as_specified ) che ti consente di eseguire l'ordinamento SQL nativo in questo modo:

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

Per quanto ho potuto testare, funziona in modo nativo in tutti gli RDBMS e restituisce una relazione ActiveRecord che può essere concatenata.


1
Amico, sei così fantastico. Grazie!
swrobel

5

Non è possibile in SQL che funzioni in tutti i casi, sfortunatamente, dovresti scrivere singoli reperti per ogni record o ordinare in ruby, anche se probabilmente c'è un modo per farlo funzionare utilizzando tecniche proprietarie:

Primo esempio:

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

MOLTO INEFFICIENTE

Secondo esempio:

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

Sebbene non sia molto efficiente, sono d'accordo che questa soluzione sia indipendente dal DB e accettabile se si dispone di una piccola quantità di righe.
Trung Lê

Non usare inject per questo, è una mappa:sorted = arr.map { |val| Model.find(val) }
tokland

il primo è lento. Sono d'accordo con il secondo con una mappa come questa:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon

2

La risposta di @Gunchars è ottima, ma non funziona immediatamente in Rails 2.3 perché la classe Hash non è ordinata. Una soluzione semplice è estendere la classe Enumerable index_byper utilizzare la classe 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

Ora l'approccio di @Gunchars funzionerà

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

Bonus

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

Poi

Something.find_with_relevance(array_of_ids)

2

Supponendo che i Model.pluck(:id)ritorni [1,2,3,4]e tu voglia l'ordine di[2,4,1,3]

Il concetto è quello di utilizzare la ORDER BY CASE WHENclausola SQL. Per esempio:

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;

In Rails, puoi ottenere questo risultato avendo un metodo pubblico nel tuo modello per costruire una struttura simile:

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

Quindi fa:

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

La cosa buona di questo. I risultati vengono restituiti come ActiveRecord Relazioni (che consente di utilizzare metodi come last, count, where, pluck, ecc)


2

C'è una gemma find_with_order che ti consente di farlo in modo efficiente utilizzando una query SQL nativa.

E supporta sia Mysqle PostgreSQL.

Per esempio:

Something.find_with_order(array_of_ids)

Se vuoi una relazione:

Something.where_with_order(:id, array_of_ids)

1

Sotto il cofano, findcon un array di ID genererà un SELECTcon una WHERE id IN...clausola, che dovrebbe essere più efficiente del ciclo tra gli ID.

Quindi la richiesta viene soddisfatta in un viaggio nel database, ma le clausole SELECTsenza ORDER BYclausole non vengono ordinate. ActiveRecord lo capisce, quindi espandiamo il nostro findcome segue:

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

Se l'ordine degli ID nel tuo array è arbitrario e significativo (ovvero vuoi che l'ordine delle righe restituite corrisponda al tuo array indipendentemente dalla sequenza degli ID in esso contenuti), allora penso che saresti il ​​miglior server post-elaborando i risultati in codice: potresti creare una :orderclausola ma sarebbe diabolicamente complicata e non rivelerebbe affatto l'intenzione.


Nota che l'hash delle opzioni è stato deprecato. (secondo argomento, in questo esempio :order => id)
ocodo

1

Sebbene non lo vedo menzionato da nessuna parte in un CHANGELOG, sembra che questa funzionalità sia stata modificata con il rilascio della versione5.2.0 .

Qui eseguire il commit dell'aggiornamento dei documenti contrassegnati con 5.2.0Tuttavia sembra che sia stato anche eseguito il backport nella versione 5.0.


0

Con riferimento alla risposta qui

Object.where(id: ids).order("position(id::text in '#{ids.join(',')}')") lavora per Postgresql.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.