Избегайте ошибки PG::InvalidTextRepresentation при использовании Postgres UUID в Rails

Я начал использовать тип Postgres UUID для полей идентификатора всех моих моделей. Отлично работает и поддерживается (по большей части) в Rails 4:

create_table :users, id: :uuid do |t|
  # ...
end

Проблема в том, что Postgres выдаст ошибку, если вы попытаетесь найти строку, где id равен X, но X не является правильно отформатированной строкой UUID.

> User.find "3ac093e2-3a5e-4744-b49f-117b032adc6c"
ActiveRecord::RecordNotFound # good, will cause a 404
> User.find "foobar"
PG::InvalidTextRepresentation: ERROR # bad, will cause a 500

Поэтому, если мой пользователь находится на странице, где UUID находится в URL-адресе, а затем попытается изменить UUID, он получит ошибку 500 вместо 404. Или, возможно, он получит ссылку на объект, который больше не существует.

Как я могу избежать этого сценария СУХИМ способом? Я не могу просто спасти PG::InvalidTextRepresentation и отобразить 404, потому что другие вещи также может вызвать эту ошибку.

ОБНОВЛЕНИЕ

Я думаю, что регулярное выражение в формате параметра идентификатора чистое, и оно поднимает 404, если оно не совпадает:

resources :users, id: /uuid-regex-here/

Но у меня все еще есть проблема оставаться СУХИМ; Я не хочу помещать это на каждый ресурс в моих маршрутах. Я могу объявить несколько ресурсов в одном выражении, но только если нет других вариантов, таких как действия членов. Возможно, лучше задать вопрос: есть ли способ установить регулярное выражение id для всех маршрутов?


person tybro0103    schedule 24.01.2014    source источник
comment
Но почему ты пропускаешь foobar? Разве ваш маршрут не должен поймать это еще до того, как он попадет в руки вашей модели?   -  person Denis de Bernardy    schedule 25.01.2014
comment
@Denis Я полагаю, я мог бы наложить ограничение на параметр id, чтобы он соответствовал регулярному выражению UUID. Но мне пришлось бы сделать это для каждого отдельного ресурса в моих маршрутах... или вы имели в виду что-то другое?   -  person tybro0103    schedule 25.01.2014
comment
Возможно, подойдет before_filter для ваших контроллеров.   -  person mu is too short    schedule 25.01.2014
comment
@tybro0103: Говоря лично, если бы я забыл сделать свои собственные маршруты проверяющими, что идентификатор имеет правильный формат, или если какой-то случайный вызов куда-то отправил идентификатор в неправильном формате, я бы действительно хотел, чтобы они кашляли об ошибке. Я недостаточно знаком с Rails, чтобы точно сказать, как вы можете их массово автоматически проверять, но я предполагаю, что mu что-то понял с его предложением.   -  person Denis de Bernardy    schedule 25.01.2014
comment
@Denis: Основная проблема заключается в том, что ребята из Rails слишком ленивы, чтобы помещать проверку в нужное место, они предполагают, что Model.find вызовет RecordNotFound, если запись не будет найдена по какой-либо причине (включая ошибку типа, такую ​​как M.find('pancakes') когда ПК является целым числом). В модели Rails добавление ограничений к маршрутам, вероятно, было бы правильным, фильтр — это быстрый хак. Rails изобилует невысказанными предположениями (попробуйте добавить маршрут, который содержит адрес электронной почты, и посмотрите, что произойдет), и когда вы отклоняетесь от общепринятого пути, он уходит в сторону.   -  person mu is too short    schedule 25.01.2014
comment
@muistooshort: разве нет возможности добавить ограничения? stackoverflow.com/questions/5921771/   -  person Denis de Bernardy    schedule 25.01.2014
comment
@Denis: Да, вы можете добавить ограничения к маршрутам, но если у вас много маршрутов...   -  person mu is too short    schedule 25.01.2014
comment
@muistooshort: нет возможности получить доступ к маршрутам и их ограничениям после их определения, чтобы обработать весь список?   -  person Denis de Bernardy    schedule 25.01.2014
comment
@Denis: Не в разумных пределах, о которых я знаю. Соглашение о конфигурации имеет свои недостатки, и недостатки, как правило, очень крутые и сбрасываются в ров, заполненный рыболовными крючками и бешеными электрическими угрями. Документация по маршрутизации тоже довольно скудная.   -  person mu is too short    schedule 25.01.2014
comment
@muistooshort Я не согласен с вашей «основной» проблемой. Это не строка против целого... '3ac093e2-44...' и 'foobar' являются строками. Я понимаю, почему PG вызывает ошибку, но я думаю, что разумно, чтобы User.find('foobar') поднимал NotFound, потому что идентификатор является строкой. Тем не менее, я понимаю, что происходит, я просто не уверен, что смогу решить эту проблему. Действие before_action СУХОЕ, но немного хакерское. Я думаю, что регулярное выражение в маршрутах было бы чистым, но не уверен, как сделать его сухим.   -  person tybro0103    schedule 25.01.2014
comment
@Denis это динамические маршруты, где идентификатор (или в данном случае uuid) является частью URL-адреса (а не параметром строки запроса). Таким образом, /posts/1, /posts/2, /posts/3 сопоставляются с одним и тем же действием, а часть идентификатора рассматривается как параметр. Я мог бы добавить регулярное выражение, чтобы часть идентификатора выглядела как uuid, я не думаю, что есть способ сделать это глобально для всех параметров идентификатора.   -  person tybro0103    schedule 25.01.2014
comment
AR предполагает, что M.find(id) вызовет ActiveRecord::RecordNotFound, если id по какой-либо причине не идентифицирует запись, и это предположение ошибочно. Основная проблема заключается в том, что вы отклоняетесь от Единого Истинного Пути Rails и теперь расплачиваетесь за это. Я полагаю, вы могли бы попытаться установить обезьяний патч find, но это противно и не поможет вам ни с чем, что не вызывает find.   -  person mu is too short    schedule 25.01.2014
comment
@muistooshort да, я полагаю. Я думаю, что поддержка UUID является новой и не кажется полностью продуманной. Я думаю, что они должны просто спасти от ошибки типа pg внутри find.   -  person tybro0103    schedule 25.01.2014
comment
Но find — не единственное, что может вызвать эту ошибку. Патч обезьяны find, если вы считаете, что это достаточно хорошо.   -  person mu is too short    schedule 25.01.2014
comment
@muistooshort верно, поэтому я не думаю, что это достаточно хорошо :(   -  person tybro0103    schedule 25.01.2014


Ответы (2)


Вы можете добавить ограничение маршрутизации для нескольких маршрутов одновременно через constraints() do ... end.

В итоге я сделал это и установил глобальное ограничение для всех параметров :id, чтобы оно соответствовало регулярному выражению UUID:

MyApp::Application.routes.draw do
  constraints(id: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i) do

    # my routes here

  end
end

Таким образом, /posts/123 или /posts/foobar больше не соответствуют /posts/:id и 404 до вызова действия контроллера, что позволяет избежать ошибки типа PG.

Все мои модели будут использовать UUID для своих идентификаторов, так что это чисто и СУХО. Если бы у меня были модели с целочисленными идентификаторами, это было бы немного менее чисто.

person tybro0103    schedule 27.01.2014
comment
Если вы используете гем UUID, вы можете использовать их метод проверки вместо создания собственного github.com/assaf/uuid/blob/master/lib/uuid.rb#L199 - person Brian Hahn; 05.02.2014
comment
@BrianHahn Приятно знать. Хотя на самом деле я не качаю свой собственный... это просто регулярное выражение. - person tybro0103; 05.02.2014
comment
Я имел в виду, что это будет ваш собственный в контексте уже имеющегося драгоценного камня UUID. Ваше регулярное выражение выглядит нормально, когда гем UUID не является зависимостью в вашем проекте! - person Brian Hahn; 06.02.2014

Если вы не хотите добавлять ограничения ко всем маршрутам для перехвата недопустимых UUID, вы можете добавить before_filter, что-то вроде этого:

before_filter do
  if(params.has_key?(:id))
    uuid = params[:id].strip.downcase.gsub('-', '').gsub(/\A\{?(\h{32})\}?\z/, '\1')
    raise ActiveRecord::RecordNotFound if(uuid.blank?)
  end
end

Обратите внимание, что UUID могут иметь различные формы (см. прекрасное руководство). ), поэтому лучше нормализовать их перед их проверкой или выполнять нормализацию и проверку одновременно.

Вы можете поместить это в свой ApplicationController, если знаете, что все ваши :id параметры должны быть UUID, или поместить логику в метод ApplicationController и before_filter :make_sure_id_is_a_uuid в контроллеры, которые в ней нуждаются.

person mu is too short    schedule 24.01.2014
comment
Это неплохо. Я закончу с этим, если не найду способ добавить регулярное выражение к маршрутам только один раз. - person tybro0103; 25.01.2014
comment
Еще одна возможность исправления обезьяны - это преобразование типов внутри драйвера AR PostgreSQL, где-то там есть большой оператор case. - person mu is too short; 25.01.2014