Pergunta

Eu quero obter uma matriz de ActiveRecord objetos dada uma matriz de IDs.

Eu assumi que

Object.find([5,2,3])

Será que retornar um array com objeto 5, objeto 2, em seguida, objeto 3 nessa ordem, mas em vez disso, obter uma matriz ordenada como objeto 2, objeto 3 e, em seguida, objeto 5.

O ActiveRecord base encontrar o método API menciona que você não deve espera-lo na ordem fornecido (outra documentação não dá este aviso).

Uma solução potencial foi dado em Procurar por matriz de IDs no mesmo ordem? , mas a opção de ordem não parece ser válido para SQLite.

Eu posso escrever algum código Ruby para classificar a mim mesmo objetos (seja um tanto simples e escala mal ou melhor dimensionamento e mais complexo), mas existe uma maneira melhor?

Foi útil?

Solução

Não é que o MySQL e outras coisas DBs de classificação por conta própria, é que eles não fazem tipo-los. Quando você chama Model.find([5, 2, 3]), o SQL gerado é algo como:

SELECT * FROM models WHERE models.id IN (5, 2, 3)

Esta não especifica uma ordem, apenas o conjunto de registros que você deseja retornar. Acontece que, geralmente, o MySQL irá retornar as linhas de base de dados, a fim 'id', mas não há nenhuma garantia disso.

A única maneira de obter o banco de dados para retornar registros em uma ordem garantida é adicionar uma cláusula de ordem. Se seus registros serão sempre retornados em uma ordem particular, então você pode adicionar uma coluna de classificação para a db e fazer Model.find([5, 2, 3], :order => 'sort_column'). Se este não for o caso, você terá que fazer a triagem em código:

ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}} 

Outras dicas

Com base no meu comentário anterior para Jeroen van Dijk você pode fazer isso de forma mais eficiente e em duas linhas usando each_with_object

result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}

Para referência aqui é a referência que eu usei

ids = [5,3,1,4,11,13,10]
results = Model.find(ids)

Benchmark.measure do 
  100000.times do 
    result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
    ids.map {|id| result_hash[id]}
  end
end.real
#=>  4.45757484436035 seconds

Agora o outro

ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do 
  100000.times do 
    ids.collect {|id| results.detect {|result| result.id == id}}
  end
end.real
# => 6.10875988006592

Atualizar

Você pode fazer isso na maioria usando ordem e casos de declarações, aqui é um método de classe que você poderia usar.

def self.order_by_ids(ids)
  order_by = ["case"]
  ids.each_with_index.map do |id, index|
    order_by << "WHEN id='#{id}' THEN #{index}"
  end
  order_by << "end"
  order(order_by.join(" "))
end

#   User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id) 
#   #=> [3,2,1]

Aparentemente mySQL e outras coisas ordenação sistema de gerenciamento de DB por conta própria. Eu acho que você pode ignorar que fazer:

ids = [5,2,3]
@things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )

A solução portátil seria usar uma instrução SQL CASE no seu ORDER BY. Você pode usar praticamente qualquer expressão em uma ORDER BY e um caso pode ser usado como uma tabela de pesquisa inline. Por exemplo, o SQL que você está depois ficaria assim:

select ...
order by
    case id
    when 5 then 0
    when 2 then 1
    when 3 then 2
    end

Isso é muito fácil de gerar com um pouco de Ruby:

ids = [5, 2, 3]
order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'

O acima assume que você está trabalhando com números ou alguns outros valores seguros em ids; se isso não for o caso, então você iria querer usar connection.quote ou um dos métodos ActiveRecord SQL desinfetante para adequadamente citar o seu ids.

Em seguida, use a corda order como sua condição de pedidos:

Object.find(ids, :order => order)

ou no mundo moderno:

Object.where(:id => ids).order(order)

Este é um pouco detalhado, mas ele deve funcionar o mesmo com qualquer banco de dados SQL e não é tão difícil de esconder a feiúra.

Como eu respondi aqui , eu só divulgou um gem ( order_as_specified ) que permite que você faça ordenação SQL nativa como esta:

Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])

Apenas testado e funciona em SQLite.

Justin Weiss escreveu um artigo no blog sobre este problema há apenas dois dias.

Parece ser uma abordagem boa para dizer ao banco de dados sobre a ordem preferencial e carregar todos os registros classificados nessa ordem diretamente do banco de dados. Exemplo de sua artigo de blog :

# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
  extend ActiveSupport::Concern
  module ClassMethods
    def find_ordered(ids)
      order_clause = "CASE id "
      ids.each_with_index do |id, index|
        order_clause << "WHEN #{id} THEN #{index} "
      end
      order_clause << "ELSE #{ids.length} END"
      where(id: ids).order(order_clause)
    end
  end
end

ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)

Isso permite que você escreva:

Object.find_ordered([2, 1, 3]) # => [2, 1, 3]

Aqui está uma performance (hash de pesquisa, não O (n) busca matriz como em detectar!) One-liner, como um método:

def find_ordered(model, ids)
  model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end

# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id)          == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids

Outra maneira (provavelmente mais eficiente) a fazê-lo em Ruby:

ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record| 
  result[record.id] = record
  result
end
sorted_records = ids.map {|id| records_by_id[id] }

Aqui está a coisa mais simples que eu poderia vir acima com:

ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
@things = [5,2,3].map{|id| Object.find(id)}

Esta é provavelmente a maneira mais fácil, supondo que você não tem muitos objetos para encontrar, uma vez que exige uma viagem para o banco de dados para cada id.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top