ActiveRecord.find (array_of_ids), preservando a ordem

98

Quando você faz Something.find(array_of_ids)no Rails, a ordem do array resultante não depende da ordem de array_of_ids.

Existe alguma maneira de fazer a localização e preservar a ordem?

ATM Eu classifico manualmente os registros com base na ordem dos IDs, mas isso é meio chato.

UPD: se for possível especificar a ordem usando o :orderparâmetro e algum tipo de cláusula SQL, como?

Leonid Shevtsov
fonte

Respostas:

71

A resposta é apenas para mysql

Existe uma função no mysql chamada FIELD ()

Aqui está como você pode usá-lo em .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 ','})")

Atualização: Isto será removido no código fonte do Rails 6.1 Rails

kovyrin
fonte
Você conhece o equivalente FIELDS()em Postgres?
Trung Lê
3
Eu escrevi uma função plpgsql para fazer isso no postgres - omarqureshi.net/articles/2010-6-10-find-in-set-for-postgresql
Omar Qureshi
25
Isso não funciona mais. Para Rails mais recentes:Object.where(id: ids).order("field(id, #{ids.join ','})")
mahemoff
2
Esta é uma solução melhor que a de Gunchars porque não interrompe a paginação.
pguardiario
.ids funciona bem para mim e é uma documentação de
registro de ativação
79

Estranhamente, ninguém sugeriu algo assim:

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

Tão eficiente quanto possível, além de permitir que o backend SQL faça isso.

Edit: Para melhorar minha própria resposta, você também pode fazer assim:

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

#index_bye #slicesão adições muito úteis no ActiveSupport para matrizes e hashes, respectivamente.

Gunchars
fonte
Sua edição parece funcionar, mas me deixa nervoso a ordem das teclas em um hash não é garantida, certo? então, quando você chama o slice e recebe o hash de volta "reordenado", na verdade, depende dos valores de retorno do hash na ordem em que suas chaves foram adicionadas. Parece que depende de um detalhe de implementação que pode mudar.
Jon
2
@Jon, a ordem é garantida no Ruby 1.9 e em todas as outras implementações que tentarem segui-la. Para 1.8, Rails (ActiveSupport) corrige a classe Hash para fazê-la se comportar da mesma maneira, então se você estiver usando Rails, não terá problemas.
Gunchars
obrigado pelo esclarecimento, só descobri que na documentação.
Jon
13
O problema com isso é que ele retorna uma matriz, em vez de uma relação.
Velizar Hristov
3
Ótimo, no entanto, o one-liner não funciona para mim (Rails 4.1)
Besi
44

Como Mike Woodhouse afirmou em sua resposta , isso ocorre porque, nos bastidores, o Rails está usando uma consulta SQL com um WHERE id IN... clausepara recuperar todos os registros em uma consulta. Isso é mais rápido do que recuperar cada id individualmente, mas como você percebeu, não preserva a ordem dos registros que está recuperando.

Para corrigir isso, você pode classificar os registros no nível do aplicativo de acordo com a lista original de IDs que você usou ao pesquisar o registro.

Com base nas muitas respostas excelentes para classificar uma matriz de acordo com os elementos de outra matriz , recomendo a seguinte solução:

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

Ou se você precisar de algo um pouco mais rápido (mas indiscutivelmente um pouco menos legível), você pode fazer isso:

Something.find(array_of_ids).index_by(&:id).values_at(*array_of_ids)
Ajedi32
fonte
3
a segunda solução (com index_by) parece falhar para mim, produzindo todos os resultados nulos.
Ben Wheeler
22

Isso parece funcionar para postgresql ( fonte ) - e retorna uma relação 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])

pode ser estendido para outras colunas também, por exemplo, para a namecoluna, use position(name::text in ?)...

gengibre
fonte
Você é meu herói da semana. Obrigado!
ntdb
4
Observe que isso só funciona em casos triviais, você acabará se deparando com uma situação em que seu Id está contido em outros IDs na lista (por exemplo, ele encontrará 1 em 11). Uma maneira de contornar isso é adicionar as vírgulas na verificação de posição e, em seguida, adicionar uma vírgula final à junção, como esta: order = sanitize_sql_array (["position (',' || clients.id :: text || ', 'in?) ", ids.join (', ') +', '])
IrishDubGuy
Boa observação, @IrishDubGuy! Vou atualizar minha resposta com base em sua sugestão. Obrigado!
gengibre
para mim o encadeamento não funciona. Aqui, o nome das tabelas deve ser adicionado antes do id: texto como este: ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] versão completa que funcionou para mim: scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( ["position((',' || somethings.id::text || ',') in ?)", ids.join(',') + ','] ) where(:id => ids).order(order) } obrigado @gingerlime @IrishDubGuy
user1136228
Eu acho que você precisa adicionar o nome da tabela no caso de fazer algumas junções ... Isso é bastante comum com escopos ActiveRecord quando você participa.
gingerlime
19

Como respondi aqui , acabei de lançar uma gema ( order_as_specified ) que permite que você faça uma ordenação SQL nativa como esta:

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

Pelo que pude testar, ele funciona nativamente em todos os RDBMSs e retorna uma relação ActiveRecord que pode ser encadeada.

JacobEvelyn
fonte
1
Cara, você é tão incrível. Obrigado!
swrobel de
5

Não é possível em SQL que funcione em todos os casos, infelizmente, você precisaria escrever achados únicos para cada registro ou pedido em ruby, embora provavelmente haja uma maneira de fazê-lo funcionar usando técnicas proprietárias:

Primeiro exemplo:

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

MUITO INEFICIENTE

Segundo exemplo:

unsorted = Model.find(arr)
sorted = arr.inject([]){|res, val| res << unsorted.detect {|u| u.id == val}}
Omar Qureshi
fonte
Embora não seja muito eficiente, concordo que essa solução alternativa é independente do banco de dados e é aceitável se você tiver poucas linhas.
Trung Lê
Não use injetar para isso, é um mapa:sorted = arr.map { |val| Model.find(val) }
tokland
o primeiro é lento. Eu concordo com o segundo mapa como este:sorted = arr.map{|id| unsorted.detect{|u|u.id==id}}
kuboon
2

A resposta de @Gunchars é ótima, mas não funciona fora da caixa no Rails 2.3 porque a classe Hash não é ordenada. Uma solução simples é estender a classe Enumerable ' index_bypara usar a 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

Agora a abordagem de @Gunchars funcionará

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

Bônus

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

Então

Something.find_with_relevance(array_of_ids)
Chris Bloom
fonte
2

Assumindo Model.pluck(:id)retornos[1,2,3,4] e você deseja o pedido de[2,4,1,3]

O conceito é utilizar a ORDER BY CASE WHENcláusula SQL. Por exemplo:

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;

No Rails, você pode conseguir isso tendo um método público em seu modelo para construir uma estrutura semelhante:

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

Então faça:

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

A coisa boa sobre isso. Os resultados são retornados como Relações ActiveRecord (permitindo que você use métodos como last, count, where, pluck, etc)

Christian Fazzini
fonte
2

Há uma joia find_with_order que permite que você faça isso de forma eficiente usando uma consulta SQL nativa.

E suporta ambos Mysqle PostgreSQL.

Por exemplo:

Something.find_with_order(array_of_ids)

Se você quer relação:

Something.where_with_order(:id, array_of_ids)
khiav reoy
fonte
1

Sob o capô, findcom uma matriz de ids irá gerar um SELECTcom umWHERE id IN... cláusula, que deve ser mais eficiente do que repetir os ids.

Portanto, a solicitação é atendida em uma viagem ao banco de dados, mas SELECTs sem ORDER BYcláusulas não são classificados. ActiveRecord entende isso, então expandimos nosso da findseguinte forma:

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

Se a ordem dos ids em sua matriz for arbitrária e significativa (ou seja, você deseja que a ordem das linhas retornadas corresponda à sua matriz, independentemente da sequência de ids contida nela), então acho que você seria o melhor servidor pós-processamento dos resultados em código - você poderia construir uma :ordercláusula, mas seria terrivelmente complicado e não revelaria nenhuma intenção.

Mike Woodhouse
fonte
Observe que o hash de opções se tornou obsoleto. (segundo argumento, neste exemplo :order => id)
ocodo
1

Embora eu não veja isso mencionado em nenhum CHANGELOG, parece que essa funcionalidade foi alterada com o lançamento da versão 5.2.0.

Aqui, confirme a atualização dos documentos marcados com 5.2.0No entanto, parece que também foi feito o backport para a versão 5.0.

XML Slayer
fonte
0

Com referência à resposta aqui

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

Sam Kah Chiin
fonte
-4

Há uma cláusula de pedido em find (: pedido => '...') que faz isso ao buscar registros. Você também pode obter ajuda aqui.

Texto do link

Ashish Jain
fonte