これまで、データベースからランダムなレコードを取得する "common" の方法は次のとおりです。
# Postgress
Model.order("RANDOM()").first
# MySQL
Model.order("Rand()").first
ただし、これをRails 5.2で実行すると、次の非推奨警告が表示されます。
非推奨警告:危険なクエリメソッド(引数が生のSQLとして使用されるメソッド)は、非属性引数: "RANDOM()"で呼び出されます。属性以外の引数はRails 6.0で許可されません。このメソッドは、リクエストパラメーターやモデル属性などのユーザー指定の値では呼び出さないでください。既知の安全な値はラッピングして渡すことができますArel.sql()で。
私はArelにあまり詳しくないので、これを修正する正しい方法は何なのかわかりません。
order by random()
の使用を継続したい場合は、非推奨の警告が示唆するようにArel.sql
でラップすることで安全に宣言するだけです:
Model.order(Arel.sql('random()')).first
ランダムな行を選択する方法はたくさんあり、それらにはすべて長所と短所がありますが、order by
でSQLのスニペットを絶対に使用する必要がある場合があります(必要な場合など Ruby配列 に一致し、大きなcase when ... end
式をデータベースに取得する必要があるため)Arel.sql
を使用してこの「属性のみ」の制限を回避する私たち全員が知る必要があるツール。
編集済み:サンプルコードに閉じ括弧がありません。
私はこのソリューションのファンです:
Model.offset(Rand(Model.count)).first
多くのレコードがあり、削除されたレコードが少ない場合、これはより効率的です。私の場合、デフォルトのスコープは結合を使用するため、_.unscoped
_を使用する必要があります。モデルがこのようなデフォルトのスコープを使用しない場合は、_.unscoped
_が表示されている場所を省略できます。
_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]]
_
これは、Rand()
を使用してランダムIDを取得し、その単一のレコードで検索を実行するだけだからです。ただし、削除された行(スキップされたID)の数が多いほど、whileループが複数回実行される可能性が高くなります。やり過ぎかもしれませんが、パフォーマンスを62%向上させる価値があり、行を削除しない場合はさらに高くなります。ユースケースに適しているかどうかをテストします。