Почему Rails добавляет `OR 1 = 0` к запросам, используя синтаксис хеш-предложения where с диапазоном?
В проекте, над которым я работаю, используется MySQL на RDS (специально для mysql2).
Когда я использую хэш условий, включая диапазон в выражении where
, я получаю немного странное дополнение к моему запросу.
User.where(id: [1..5])
и
User.where(id: [1...5])
Результат в следующих запросах соответственно:
SELECT `users`.* FROM `users` WHERE ((`users`.`id` BETWEEN 1 AND 5 OR 1=0))
SELECT `users`.* FROM `users` WHERE ((`users`.`id` >= 1 AND `users`.`id` < 5 OR 1=0))
Запросы отлично работают, поскольку OR FALSE
фактически не работает. Мне просто интересно, почему Rails или ARel добавляют этот фрагмент в запрос.
ИЗМЕНИТЬ
Похоже, что строка, которая может объяснить это, строка 26 в ActiveRecord::PredicateBuilder
. По-прежнему не знаю, как хэш может быть empty?
в этот момент, но, возможно, кто-то другой делает.
EDIT 2
Это интересно. Я смотрел на комментарий Филиппа, чтобы понять, почему он сделал это, так как это похоже на разъяснение, но он прав, что 1..5 != [1..5]
. Первый является инклюзивным диапазоном от 1 до 5, где последний является массивом, первым элементом которого является первый. Я попытался поместить их в вызов ARel where
, чтобы увидеть произведенный SQL и OR 1=0
не существует!
User.where(id: 1..5) #=> SELECT "users".* FROM "users" WHERE ("users"."id" BETWEEN 1 AND 5)
User.where(id: 1...5) #=> SELECT "users".* FROM "users" WHERE ("users"."id" >= 1 AND "users"."id" < 5)
Пока я до сих пор не знаю, почему AREL добавляет OR 1=0
, который всегда будет ложным и, казалось бы, ненужным. Это может быть связано с тем, как Array
и Range
обрабатываются по-разному.
Ответы
Ответ 1
Основываясь на обнаруженном факте, что [1..5]
не является правильным способом указать диапазон... Я обнаружил, почему [1..5]
ведет себя так же, как и он. Чтобы добраться туда, я сначала обнаружил, что пустой массив в хэш-состоянии создает условие 1=0
SQL:
User.where(id: []).to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE 1=0"
И если вы проверите ActiveRecord:: PredicateBuilder:: ArrayHandler код, вы увидите, что значения массива всегда разделяются на диапазоны и другие значения.
ranges, values = values.partition { |v| v.is_a?(Range) }
Это объясняет, почему вы не видите 1=0
при использовании значений без диапазона. То есть единственный способ получить 1=0
из массива без включения диапазона - это предоставить пустой массив, который дает условие 1=0
, как показано выше. И когда весь массив имеет в нем диапазон, вы получите условия диапазона (ranges
) и отдельно выполняете условие пустого массива (values
). Я предполагаю, что для этого нет веской причины... просто проще это сделать, чем избежать этого (так как набор результатов эквивалентен в любом случае). Если код раздела был немного умнее, тогда ему не пришлось бы ссылаться на дополнительный пустой массив values
и может пропустить условие 1=0
.
Что касается того, откуда приходит 1=0
... Я думаю, что это происходит из адаптера базы данных, но я не смог найти точно, где именно. Однако я бы назвал это попыткой не найти запись. Другими словами, WHERE 1=0
никогда не будет возвращать пользователей, что имеет смысл в отношении альтернативного SQL, такого как WHERE id=null
, который найдет всех пользователей, чей идентификатор является нулевым (понимая, что это не совсем правильный синтаксис SQL). И это то, что я ожидаю при попытке найти всех пользователей, чей идентификатор находится в пустом наборе (т.е. Мы не запрашиваем нильские идентификаторы или нулевые идентификаторы или что-то еще). Итак, на мой взгляд, оставляя бит о том, где именно 1=0
поступает из черного ящика, в порядке. По крайней мере, теперь мы можем понять, почему диапазон внутри массива вызывает его появление!
UPDATE
Я также обнаружил, что даже при использовании ARel напрямую вы можете получить 1=0
:
User.arel_table[:id].in([]).to_sql
# => "1=0"
Ответ 2
Это, строго говоря, предположение, поскольку я сделал что-то подобное в собственном проекте (хотя я использовал AND 1
).
По какой-то причине при генерации запроса легче всегда иметь предложение WHERE
, содержащее no-op, чем условно генерировать предложение WHERE
вообще. То есть, если вы не включаете разделы WHERE
, это приведет к созданию чего-то еще действительного.
С другой стороны, я не уверен, почему он принимает эту форму: когда я это использовал, я использую 1 [<AND (generated code)>...]
, он допускает произвольную цепочку, но я не вижу, как это позволит. Тем не менее, я все еще думаю, что это, скорее всего, результат алгоритмической схемы генерации кода.
Ответ 3
Убедитесь, что вы используете active_record-act_as. Это была проблема со мной.
Добавьте строку ниже в свой Gemfile:
gem 'active_record-acts_as', :git => 'https://github.com/hzamani/active_record-acts_as.git'
Это просто вытащит последнюю версию Gem, которая, надеюсь, будет исправлена. Работал для меня.
Ответ 4
Я думаю, что вы видите побочные эффекты рубина лично.
Я думаю, что лучший способ сделать то, что вы делаете, будет с
[email protected] :008 > [*1..5]
=> [1, 2, 3, 4, 5]
User.where(id: [*1..5]).to_sql
"SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2, 3, 4, 5)"
Так как это создает Array vs Array с элементом 1 класса Range.
ИЛИ
используйте явный диапазон для запуска МЕЖДУ В AREL.
# with end element, i.e. exclude_end=false
[email protected] :013 > User.where(id: Range.new(1,5)).to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`id` BETWEEN 1 AND 5)"
# without end element, i.e. exclude_end=true
[email protected] :022 > User.where(id: Range.new(1, 5, true)).to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`id` >= 1 AND `users`.`id` < 5)"
Ответ 5
Если вы заботитесь о контроле над запрошенными вами запросами и полной мощности языка SQL и функций базы данных, я бы предложил перейти от ActiveRecord/Arel к Sequel.
Я могу честно сказать, что у вас с ActiveRecord намного больше причудливых и беспризорных времен, особенно когда вы переходите от простых запросов, похожих на crud. Когда вы начинаете пытаться запросить свои данные в гневе, возможно, вам нужно присоединиться к нескольким таблицам соединений здесь и там и понять, что вам действительно нужны условия соединения или объединить все запросы типа.
Он также значительно быстрее и надежнее в генерации запросов и обработке результатов и намного проще компоновать запросы, которые вы хотите. Он также имеет реальную документацию, которую вы действительно можете читать в отличие от isl.
Мне просто хотелось бы, чтобы я обнаружил это намного раньше, а не сохранялся с уровнем доступа к данным по умолчанию rails.