web-dev-qa-db-fra.com

AVERTISSEMENT DE DÉPRÉCIATION: Méthode de requête dangereuse: enregistrement aléatoire dans ActiveRecord> = 5.2

Jusqu'à présent, la façon "commune" d'obtenir un enregistrement aléatoire de la base de données a été:

# Postgress
Model.order("RANDOM()").first 

# MySQL
Model.order("Rand()").first

Mais, lorsque vous faites cela dans Rails 5.2, il affiche l'avertissement de dépréciation suivant:

AVERTISSEMENT DE DÉPRÉCIATION: Méthode de requête dangereuse (méthode dont les arguments sont utilisés en tant que SQL brut) appelée avec un ou des arguments sans attribut: "RANDOM ()". Les arguments sans attribut seront interdits dans Rails 6.0. Cette méthode ne doit pas être appelée avec des valeurs fournies par l'utilisateur, telles que des paramètres de demande ou des attributs de modèle. Des valeurs sûres connues peuvent être transmises en les encapsulant) dans Arel.sql ().

Je ne connais pas vraiment Arel, donc je ne sais pas quelle serait la bonne façon de résoudre ce problème.

21
Daniel

Si vous souhaitez continuer à utiliser order by random(), déclarez-le simplement en l'enveloppant dans Arel.sql comme l'indique l'avertissement de dépréciation:

Model.order(Arel.sql('random()')).first

Il existe de nombreuses façons de sélectionner une ligne aléatoire et elles présentent toutes des avantages et des inconvénients, mais il peut arriver que vous deviez absolument utiliser un extrait de code SQL dans un order by (comme lorsque vous avez besoin de l'ordre pour correspondre à un Ruby array et devez obtenir un gros case when ... end expression jusqu'à la base de données) donc en utilisant Arel.sql pour contourner cette restriction "attributs uniquement" est un outil que nous devons tous connaître.

Modifié: il manque une parenthèse fermante à l'exemple de code.

33
mu is too short

Je suis fan de cette solution:

Model.offset(Rand(Model.count)).first
4
Anthony L

Avec de nombreux enregistrements et pas beaucoup d'enregistrements supprimés, cela peut être plus efficace. Dans mon cas, je dois utiliser .unscoped Car la portée par défaut utilise une jointure. Si votre modèle n'utilise pas une telle étendue par défaut, vous pouvez omettre le .unscoped Partout où il apparaît.

Patient.unscoped.count #=> 134049

class Patient
  def self.random
    return nil unless Patient.unscoped.any?
    until @patient do
      @patient = Patient.unscoped.find Rand(Patient.unscoped.last.id)
    end
    @patient
  end
end

#Compare with other solutions offered here in my use case

puts Benchmark.measure{10.times{Patient.unscoped.order(Arel.sql('RANDOM()')).first }}
#=>0.010000   0.000000   0.010000 (  1.222340)
Patient.unscoped.order(Arel.sql('RANDOM()')).first
Patient Load (121.1ms)  SELECT  "patients".* FROM "patients"  ORDER BY RANDOM() LIMIT 1

puts Benchmark.measure {10.times {Patient.unscoped.offset(Rand(Patient.unscoped.count)).first }}
#=>0.020000   0.000000   0.020000 (  0.318977)
Patient.unscoped.offset(Rand(Patient.unscoped.count)).first
(11.7ms)  SELECT COUNT(*) FROM "patients"
Patient Load (33.4ms)  SELECT  "patients".* FROM "patients"  ORDER BY "patients"."id" ASC LIMIT 1 OFFSET 106284

puts Benchmark.measure{10.times{Patient.random}}
#=>0.010000   0.000000   0.010000 (  0.148306)

Patient.random
(14.8ms)  SELECT COUNT(*) FROM "patients"
#also
Patient.unscoped.find Rand(Patient.unscoped.last.id)
Patient Load (0.3ms)  SELECT  "patients".* FROM "patients"  ORDER BY "patients"."id" DESC LIMIT 1
Patient Load (0.4ms)  SELECT  "patients".* FROM "patients" WHERE "patients"."id" = $1 LIMIT 1  [["id", 4511]]

La raison en est que nous utilisons Rand() pour obtenir un ID aléatoire et simplement faire une recherche sur cet enregistrement unique. Cependant, plus le nombre de lignes supprimées (identifiants ignorés) est élevé, plus la boucle while s'exécutera probablement plusieurs fois. Cela peut être exagéré mais pourrait valoir une augmentation de 62% des performances et même plus si vous ne supprimez jamais de lignes. Testez si c'est mieux pour votre cas d'utilisation.

2
lacostenycoder