Подтвердить что ты не робот

Почему 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 обрабатываются по-разному.

4b9b3361

Ответ 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.