ActiveRecord.find(array_of_ids), volgorde bewaren

Als je Something.find(array_of_ids)doet in Rails, hangt de volgorde van de resulterende array niet af van de volgorde van array_of_ids.

Is er een manier om de zoekactie uit te voeren en de bestelling te behouden?

ATM Ik sorteer de records handmatig op volgorde van ID’s, maar dat is nogal flauw.

UPD: als het mogelijk is om de volgorde te specificeren met behulp van de :orderparam en een soort SQL-clausule, hoe dan?


Antwoord 1, autoriteit 100%

Vreemd genoeg heeft niemand iets als dit voorgesteld:

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

Zo efficiënt als het wordt, behalve dat de SQL-backend het doet.

Bewerken:om mijn eigen antwoord te verbeteren, kun je het ook als volgt doen:

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

#index_byen #slicezijn behoorlijk handige toevoegingen in ActiveSupportvoor respectievelijk arrays en hashes.


Antwoord 2, autoriteit 88%

Het antwoord is alleen voor mysql

Er is een functie in mysql genaamd FIELD( )

Hier is hoe je het zou kunnen gebruiken 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 ','})")

Bijwerken:
Dit wordt verwijderd in Rails 6.1 Rails-broncode


Antwoord 3, autoriteit 58%

Zoals Mike Woodhousevermeld in zijn antwoord, dit gebeurt omdat Rails onder de motorkap een SQL-query gebruikt met een WHERE id IN... clauseom alle records in één query op te halen . Dit is sneller dan elke id afzonderlijk op te halen, maar zoals je hebt opgemerkt, blijft de volgorde van de records die je ophaalt niet behouden.

Om dit op te lossen, kunt u de records op applicatieniveau sorteren volgens de originele lijst met ID’s die u gebruikte bij het opzoeken van de record.

Op basis van de vele uitstekende antwoorden op Sorteer een array volgens de elementen van een andere array, raad ik de volgende oplossing aan:

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

Of als je iets sneller nodig hebt (maar waarschijnlijk iets minder leesbaar), kun je dit doen:

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

Antwoord 4, autoriteit 32%

Dit lijkt te werken voor postgresql(bron) – en retourneert een ActiveRecord-relatie

class Something < ActiveRecrd::Base
  scope :for_ids_with_order, ->(ids) {
    order = sanitize_sql_array(
      ["position((',' || id::text || ',') in ?)", ids.join(',') + ',']
    )
    where(:id => ids).order(Arel.sql(order))
  }    
end
# usage:
Something.for_ids_with_order([1, 3, 2])

kan ook worden uitgebreid voor andere kolommen, b.v. Voor de nameKOLOM, gebruikt u position(name::text in ?)


Antwoord 5, Autoriteit 30%

Zoals ik geantwoord hier , heb ik net een edelsteen uitgebracht (Order_as_Specified ) waarmee u inheemse SQL-bestellingen vindt:

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

Voor zover ik heb kunnen testen, werkt het in native in alle RDBMS’s en retourneert een activerecord-relatie die kan worden geketend.


Antwoord 6, Autoriteit 6%

Niet mogelijk in SQL, die helaas in alle gevallen zou werken, zou u ofwel single vondsten moeten schrijven voor elk record of bestelling in Ruby, hoewel er waarschijnlijk een manier is om het te laten werken met behulp van gepatenteerde technieken:

eerste voorbeeld:

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

Zeer inefficiënt

Tweede voorbeeld:

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

Antwoord 7, Autoriteit 5%

Er is een edelsteen find_with_order waarmee u het efficiënt kunt doen met behulp van native SQL-query.

En het ondersteunt zowel Mysqlen PostgreSQL.

Bijvoorbeeld:

Something.find_with_order(array_of_ids)

Als u relatie wilt:

Something.where_with_order(:id, array_of_ids)

Antwoord 8, Autoriteit 2%

Onder de kap, findmet een reeks ID’s zal een SELECTmet een WHERE id IN...clausule, dat zou moeten efficiënter zijn dan door de ID’s.

Dus het verzoek is tevreden in één reis naar de database, maar SELECTS zonder ORDER BYClausules zijn ongesorteerd. ActiveRecord begrijpt dit, dus we breiden onze findals volgt uit:

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

Als de volgorde van ID’s in uw array willekeurig en significant is (dwz u wilt de volgorde van rijen teruggekeerd om overeen te komen met uw array, ongeacht de reeks van ID’s die daarin zijn opgenomen), dan denk ik dat u de beste server door nabewerking zou zijn De resultaten in de code – u kunt een :orderclausule bouwen, maar het zou duivelachtig ingewikkeld zijn en helemaal niet intentions-onthullend.


Antwoord 9, Autoriteit 2%

@Gunchars Antwoord is geweldig, maar het werkt niet uit de doos in rails 2.3 omdat de HASH-klasse niet wordt besteld. Een eenvoudige oplossing is om de slaapbare klasse ‘index_byuit te breiden om de volgorde van de bestelling te gebruiken:

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

Nu @Gunchars-aanpak werkt

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

Dan

Something.find_with_relevance(array_of_ids)

Antwoord 10, Autoriteit 2%

Ervan uitgaande dat Model.pluck(:id)[1,2,3,4]retourneert en je de volgorde van [2,4,1,3]

Het concept is om de ORDER BY CASE WHENSQL-clausule te gebruiken. Bijvoorbeeld:

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 kunt u dit bereiken door een openbare methode in uw model te gebruiken om een ​​vergelijkbare structuur te construeren:

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

Doe dan:

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

Het goede hieraan. Resultaten worden geretourneerd als ActiveRecord-relaties (waardoor u methoden kunt gebruiken zoals last, count, where, pluck, enz. )


Antwoord 11

Hoewel ik het nergens in een CHANGELOG zie, lijkt het erop dat deze functionaliteit was gewijzigdmet de release van versie 5.2.0.

Hier commithet bijwerken van de documenten getagd met 5.2.0Het lijkt echter ook te zijn geweest gebackporteerdnaar versie 5.0.


Antwoord 12

Volgens het officiële document zou het in dezelfde volgorde moeten staan.

https://api.rubyonrails.org/classes /ActiveRecord/FinderMethods.html#method-i-find

Something.find(array_of_ids)

de volgorde van de resulterende array moet dezelfde volgorde zijn als array_of_ids. Ik heb dit getest in Rails 6.


Antwoord 13

Met verwijzing naar het antwoord hier

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


Antwoord 14

Ik vond de op ordergebaseerde opties erg leuk.
Mijn 2c is dat het logisch is om het in een scope toe te voegen, zodat je het kunt gebruiken in een keten met andere AR-methoden

scope :find_in_order, ->(ids) { 
  where(id: ids).order([Arel.sql('FIELD(id, ?)'), ids])
}

Antwoord 15

Er is een orderclausule in find (:order=>’…’) die dit doet bij het ophalen van records.
Je kunt hier ook hulp krijgen.

linktekst

Other episodes