ПРЕДУПРЕЖДЕНИЕ ОБ УСТРАНЕНИИ: Опасный метод запроса: произвольная запись в ActiveRecord> = 5.2

До сих пор "общий" способ получения случайной записи из базы данных был:

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

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

Но, когда вы делаете это в Rails 5.2, в нем отображается следующее предупреждение об отказе:

ПРЕДУПРЕЖДЕНИЕ О ДЕПРЕКАЦИИ: Опасный метод запроса (метод, аргументы которого используются как необработанный SQL), вызываемый с неатрибутными аргументами: "RANDOM()". Неатрибутные аргументы будут исключены в Rails 6.0. Этот метод нельзя вызывать с предоставленными пользователем значениями, такими как параметры запроса или атрибуты модели. Знаменитые значения могут быть переданы путем их переноса в Arel.sql().

Я не очень хорошо знаком с Arel, поэтому я не уверен, что было бы правильным способом исправить это.

Ответ 1

Если вы хотите продолжить использовать order by random() тогда просто объявите его безопасным, обернув его в Arel.sql как предполагает предупреждение об устаревании:

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

Существует множество способов выбора случайной строки, и все они имеют свои преимущества и недостатки, но бывают случаи, когда вам абсолютно необходимо использовать фрагмент SQL в order by (например, когда вам нужен порядок, совпадающий с массивом Ruby, и нужно получить большой case when... end выражение до базы данных), поэтому использование Arel.sql чтобы обойти это ограничение "только атрибуты" - это инструмент, о котором мы все должны знать.

Отредактировано: в примере кода отсутствуют закрывающие скобки.

Ответ 2

Я поклонник этого решения:

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

Ответ 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() для получения случайного идентификатора и просто делаем поиск по этой единственной записи. Однако чем больше количество удаленных строк (пропущенных идентификаторов), тем больше вероятность того, что цикл while будет выполняться несколько раз. Это может быть излишним, но может стоить увеличения производительности на 62% и даже выше, если вы никогда не удаляете строки. Проверьте, лучше ли это для вашего варианта использования.