弃用警告:危险的查询方法:在ActiveRecord >= 5.2中随机记录。

32

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

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

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

但是,在Rails 5.2中这样做时,会显示以下弃用警告:

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

我对Arel并不是很熟悉,所以不确定修复此问题的正确方法。

3个回答

52
如果您想继续使用 order by random(),则只需像弃用警告建议的那样将其包装在 Arel.sql 中以使其安全。
Model.order(Arel.sql('random()')).first # PostgreSQL
Model.order(Arel.sql('rand()')).first   # MySQL

选取随机行的方法有很多种,它们都有优缺点,但是在某些情况下你必须使用 SQL 语句来进行排序(例如当你需要让 排序与 Ruby 数组匹配 并且必须将一个大的 case when ... end 表达式放到数据库中时),所以我们都需要了解使用 Arel.sql 来避免这个“仅限属性”的限制。

编辑:示例代码缺少一个闭括号。


使用Arel.sql声明是否更安全?.order('RAND())'对我来说完全没问题 - 你能详细解释一下吗? - davegson
1
如果你不将它包装在 Arel.sql 调用中,你会收到一个弃用警告(在下一个版本中将变成异常),所以你没有太多选择。仅仅将某些东西包装在 Arel.sql 中并不能使任何东西更安全,它只是让你更努力地去射自己的脚。 - mu is too short
3
我喜欢代码量变少的感觉。现在我得告诉AR我在使用SQL。 - baash05
1
@baash05,您可以使用AREL来完成所有操作,甚至可以使用更少的代码并大大提高可读性 ;) - mu is too short
1
那让我笑了。 - baash05
显示剩余2条评论

5

我很喜欢这个解决方案:

Model.offset(rand(Model.count)).first

5
有道理。我认为这种解决方案存在的唯一问题是它会对数据库进行两次访问,而不是一次,并且在理论上由于竞争条件可能会失败。 - Daniel
6
但是说实话,如果使用order by random()处理大量结果集的话,可能会非常耗费资源。 - mu is too short
1
是的,没错!我猜这取决于表的大小以及与数据库服务器的延迟。知道两种选项都存在是很好的。 - Daniel
3
请注意,对于大型PostgreSQL表,Model.count可能需要很长时间。 - kwerle
1
@muistooshort 我发现RAND()要快得多:https://dev59.com/wXE85IYBdhLWcg3wejjn#47178248 - Sam

3

如果有很多记录,而且删除的记录不多,这种方法可能更有效。在我的情况下,我必须使用.unscoped因为默认作用域使用了一个join。如果你的模型没有使用这样的默认作用域,你可以省略任何出现的.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%的性能提升甚至更高。测试一下,看看是否适用于您的用例。

1
用于基准测试的道具。如果您正在使用PostgreSQL,则可能会对此问题感兴趣。 - mu is too short
@muistooshort 谢谢,是的,在发布这个之前我确实看到了那个。 - lacostenycoder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接