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.
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.
Je suis fan de cette solution:
Model.offset(Rand(Model.count)).first
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.