弃用警告:危险查询方法:ActiveRecord >= 5.2 中的随机记录
DEPRECATION WARNING: Dangerous query method: Random Record in ActiveRecord >= 5.2
到目前为止,"common" 从数据库中获取随机记录的方法是:
# Postgress
Model.order("RANDOM()").first
# MySQL
Model.order("RAND()").first
但是,在 Rails 5.2 中执行此操作时,会显示以下弃用警告:
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "RANDOM()". Non-attribute arguments will be disallowed in Rails 6.0. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql().
我对 Arel 不是很熟悉,所以我不确定解决这个问题的正确方法是什么。
我喜欢这个解决方案:
Model.offset(rand(Model.count)).first
如果您想继续使用 order by random()
,则只需将其包装在 Arel.sql
中声明它是安全的,就像弃用警告所建议的那样:
Model.order(Arel.sql('random()')).first # PostgreSQL
Model.order(Arel.sql('rand()')).first # MySQL
有很多方法可以选择随机行,它们各有利弊,但有时您绝对必须在 order by
中使用 SQL 的片段(例如当你需要 the order to match a Ruby array 并且必须得到一个很大的 case when ... end
表达式到数据库)所以使用 Arel.sql
来绕过这个“仅限属性”的限制是我们都需要知道的一个工具。
已编辑:示例代码缺少右括号。
记录多,删除的记录不多,这样效率可能更高。在我的例子中,我必须使用 .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" = LIMIT 1 [["id", 4511]]
这是因为我们使用 rand()
来获取一个随机 ID,然后只对该单个记录进行查找。但是,删除的行数(跳过的 ID)越多,while 循环执行多次的可能性就越大。这可能有点矫枉过正,但值得 62% 的性能提高,如果您从不删除行,甚至更高。测试它是否更适合您的用例。
到目前为止,"common" 从数据库中获取随机记录的方法是:
# Postgress
Model.order("RANDOM()").first
# MySQL
Model.order("RAND()").first
但是,在 Rails 5.2 中执行此操作时,会显示以下弃用警告:
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "RANDOM()". Non-attribute arguments will be disallowed in Rails 6.0. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql().
我对 Arel 不是很熟悉,所以我不确定解决这个问题的正确方法是什么。
我喜欢这个解决方案:
Model.offset(rand(Model.count)).first
如果您想继续使用 order by random()
,则只需将其包装在 Arel.sql
中声明它是安全的,就像弃用警告所建议的那样:
Model.order(Arel.sql('random()')).first # PostgreSQL
Model.order(Arel.sql('rand()')).first # MySQL
有很多方法可以选择随机行,它们各有利弊,但有时您绝对必须在 order by
中使用 SQL 的片段(例如当你需要 the order to match a Ruby array 并且必须得到一个很大的 case when ... end
表达式到数据库)所以使用 Arel.sql
来绕过这个“仅限属性”的限制是我们都需要知道的一个工具。
已编辑:示例代码缺少右括号。
记录多,删除的记录不多,这样效率可能更高。在我的例子中,我必须使用 .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" = LIMIT 1 [["id", 4511]]
这是因为我们使用 rand()
来获取一个随机 ID,然后只对该单个记录进行查找。但是,删除的行数(跳过的 ID)越多,while 循环执行多次的可能性就越大。这可能有点矫枉过正,但值得 62% 的性能提高,如果您从不删除行,甚至更高。测试它是否更适合您的用例。