31

到目前为止,从数据库中获取随机记录的“常用”方法是:

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

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

但是,在 Rails 5.2 中执行此操作时,会显示以下弃用警告:

弃用警告:使用非属性参数调用的危险查询方法(其参数用作原始 SQL 的方法):“RANDOM()”。Rails 6.0 将不允许使用非属性参数。不应使用用户提供的值(例如请求参数或模型属性)调用此方法。可以通过将已知安全值包装在 Arel.sql() 中来传递它们。

我对 Arel 不是很熟悉,所以我不确定解决这个问题的正确方法是什么。

4

3 回答 3

51

如果您想继续使用,那么只需像弃用警告建议order by random()的那样将其包装起来声明它是安全的:Arel.sql

Model.order(Arel.sql('random()')).first # PostgreSQL
Model.order(Arel.sql('rand()')).first   # MySQL

有很多选择随机行的方法,它们都有优点和缺点,但有时您绝对order by必须在大表达式到数据库)所以用来绕过这个“仅限属性”限制是我们都需要了解的工具。case when ... endArel.sql

编辑:示例代码缺少右括号。

于 2018-02-21T02:28:27.583 回答
5

我是这个解决方案的粉丝:

Model.offset(rand(Model.count)).first
于 2018-02-21T02:13:53.817 回答
3

如果记录很多,而删除的记录不多,这可能会更有效。就我而言,我必须使用.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% 甚至更高,这可能是值得的。测试它是否更适合您的用例。

于 2018-04-30T03:30:32.233 回答