【问题标题】:DEPRECATION WARNING: Dangerous query method: Random Record in ActiveRecord >= 5.2弃用警告:危险的查询方法:ActiveRecord 中的随机记录 >= 5.2
【发布时间】:2018-07-31 12:17:45
【问题描述】:

到目前为止,"common" 从数据库中获取随机记录的方法是:

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

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

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

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

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

【问题讨论】:

    标签: ruby-on-rails rails-activerecord deprecation-warning ruby-on-rails-5.2


    【解决方案1】:

    如果您想继续使用order by random(),只需将其包装在Arel.sql 中即可声明它是安全的,就像弃用警告所暗示的那样:

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

    有很多选择随机行的方法,它们都有优点和缺点,但有时你绝对必须在order by 中使用 SQL 的 sn-p(例如当你需要 the order to match a Ruby array 和必须将一个大的case when ... end 表达式下载到数据库中)所以使用Arel.sql 来绕过这个“仅限属性”限制是我们都需要了解的工具。

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

    【讨论】:

    • 如何使用Arel.sql 声明它更安全? .order('RAND())' 对我来说似乎完全没问题 - 你能详细说明一下吗?
    • @davegson 如果你不将它包装在Arel.sql 调用中,你会收到一个弃用警告(这将在下一个版本中变成一个异常),所以你真的没有太多选择。只是用Arel.sql 包裹一些东西不会让任何东西更安全,它只会让你更加努力地工作。
    • 我喜欢这样少得多的代码。现在我必须告诉 AR 我正在使用 sql。
    • @baash05 你可以使用更少的代码并通过使用 AREL 来大幅提高可读性;)
    • 这让我笑了
    【解决方案2】:

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

    【讨论】:

    • 基准测试的道具。如果您使用的是 PostgreSQL,这个question 可能会很有趣。
    • @muistooshort 谢谢,是的,我在发帖之前就看到了。
    【解决方案3】:

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

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

    【讨论】:

    • 有道理。我看到这个解决方案的唯一问题是它两次而不是一次命中数据库,并且理论上它可能由于竞争条件而失败。
    • @Daniel 但公平地说,order by random() 在处理大型结果集时可能会非常昂贵。
    • 是的。这是正确的!我想这取决于表的大小以及数据库服务器的延迟。很高兴知道这两种选择都存在。
    • 请注意,对于大型 postgres 表,Model.count 可能需要 很长时间 时间。
    • @muistooshort 我发现 RAND() 快得多stackoverflow.com/a/47178248/1651458
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-14
    • 2010-12-10
    • 2011-03-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多