web-dev-qa-db-ja.com

廃止の警告:危険なクエリメソッド:ActiveRecordのランダムレコード> = 5.2

これまで、データベースからランダムなレコードを取得する "common" の方法は次のとおりです。

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

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

ただし、これをRails 5.2で実行すると、次の非推奨警告が表示されます。

非推奨警告:危険なクエリメソッド(引数が生のSQLとして使用されるメソッド)は、非属性引数: "RANDOM()"で呼び出されます。属性以外の引数はRails 6.0で許可されません。このメソッドは、リクエストパラメーターやモデル属性などのユーザー指定の値では呼び出さないでください。既知の安全な値はラッピングして渡すことができますArel.sql()で。

私はArelにあまり詳しくないので、これを修正する正しい方法は何なのかわかりません。

21
Daniel

order by random()の使用を継続したい場合は、非推奨の警告が示唆するようにArel.sqlでラップすることで安全に宣言するだけです:

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

ランダムな行を選択する方法はたくさんあり、それらにはすべて長所と短所がありますが、order byでSQLのスニペットを絶対に使用する必要がある場合があります(必要な場合など Ruby配列 に一致し、大きなcase when ... end式をデータベースに取得する必要があるため)Arel.sqlを使用してこの「属性のみ」の制限を回避する私たち全員が知る必要があるツール。

編集済み:サンプルコードに閉じ括弧がありません。

33
mu is too short

私はこのソリューションのファンです:

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

多くのレコードがあり、削除されたレコードが少ない場合、これはより効率的です。私の場合、デフォルトのスコープは結合を使用するため、_.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%向上させる価値があり、行を削除しない場合はさらに高くなります。ユースケースに適しているかどうかをテストします。

2
lacostenycoder