ActiveStorageを試し、image_tagのエラー文などについて少し調べた

最初に

Qiita: ActiveStorageを使ってお手軽にファイルアップロードを試す
この記事で失敗するという話を見て、久しくActiveStorageを使ってなかったので試してみた。

結論からいうと、画像なしでユーザー登録すると、画像が見つからずview側でエラーが起こる。 ただ、このエラー文がわかりにくく(image_tagによるもの)、それについて少し調べた。 あと、解決方法のメソッドattached?, present?, blank?について気になった点があったので、少し調べた。

Qiita記事と異なった些細な点について、いくつか

Qiita: ActiveStorageを使ってお手軽にファイルアップロードを試す
記事通りにやるだけだが、いくつか記事と異なる点があった。

バージョン違い

今回、自分は最新のRubyRailsを使った。

  • Ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin20]
  • Rails 6.1.4

mini_magick -> image_processing

また、Rails 6からmini_magickではなくimage_processingをデフォルトで使うことになっているらしく、 Gemfileにはimage_processingが書かれていて、その代わりに記事通りにmini_magickを使った。
今回の実験では、mini_magickで特に問題はなかった。

rails db:create

rails db:migrateの前に1回rails db:createを挟む必要があった。
sqlight3では不要ぽいが、PostgreSQLでは必要。

Viewの画面でのエラー

  <% @users.each do |user| %>
    <li>
      <p><%= user.name %></p>
      <%= image_tag user.avatar %>
    </li>
  <% end %>

<%= image_tag user.avatar %>で、ユーザーがavatar画像を持たないとエラーになる。 画像を持っていれば、別にエラーにはならない。
image_tag内で起こっているエラーだが、これがわかりにくかった。
なお、このエラーはS3を使う前でも確かめられる。

Image from Gyazo 文字をコピペすると、以下。

ArgumentError in Users#index

Can't resolve image into URL: undefined method `persisted?' for nil:NilClass

また、あとで少し書くが、user.avatarActiveStorage::Attached::Oneクラスのインスタンスで、 画像がなくても直接nilになっているわけではないようだ。画像のないActiveStorage::Attached::Oneクラスのインスタンスが、 image_tagヘルパーメソッドに渡されるとエラーになる。

ArgumentErrorが起こっているらしいが、エラーの説明文の中にundefined methodの文字が見える。
undefined methodならNoMethodErrorなのではないかと最初は思った。 どうも調べてみると、Railsimage_tagヘルパーメソッド内部でnil.persisted?が呼ばれNoMethodErrorが起こり、 image(画像)をURL(文字列)に変換できなかったため、 そういうエラーが起きるような引数があったということでArgumentErrorとなっているらしい。
nil.persisted?が呼ばれたという情報はRails内部のことで、出来ればもっと隠蔽して、もっとわかりやすいエラーにして欲しいなと思った。

image_tag(1)

image_tag1を与えてみると、同じ「Can't resolve image into URL: 」というエラー説明文が表示された。

Image from Gyazo

ArgumentError in Users#index

Can't resolve image into URL: undefined method `to_model' for 1:Integer
Did you mean?  to_yaml

image_tag(nil)

nilを与えると、次のようなエラーになった。nilは、場所となる画像URLを生成できないから、エラーになるらしい。

Image from Gyazo

ArgumentError in Users#index

Nil location provided. Can't build URI.

image_tag(適当な文字列)

適当な文字列を与えると、次のようなエラーとなった。

Image from Gyazo

Sprockets::Rails::Helper::AssetNotFound in Users#index

The asset "foo" is not present in the asset pipeline.

fooという場所が存在しないから、エラーになるらしい。 引数の種類としては変ではないせいか、ArgumentErrorにはならなかった。

エラー回避方法

<%= image_tag user.avatar %>を以下のどれかに変更して、 画像が存在するときにだけ、image_taguser.avatarを渡してあげれば解決する。

<%= image_tag user.avatar if user.avatar.present? %>
<%= image_tag user.avatar if user.avatar.attached? %>
<%= image_tag user.avatar unless user.avatar.blank? %>

いずれも画像を持つかどうかを調べるメソッドだが、 この3つのpresent?, attached?, blank?の関係性について見ていく。

present?メソッドなどの詳細

ここでuser.avatarというのはActiveStorage::Attached::Oneクラスのインスタンスである。 メソッドの中身を紐解くと、present?attached?は機能的に同じメソッドである。

present?は、ActiveSupportによるRails拡張のメソッドで、 Objectクラスに定義しているため、それを継承しているActiveStorage::Attached::Oneでも使えている。

present?は、内部で単にblank?メソッドを呼んで否定しているだけである。

def present?
  !blank?
end

そして、blank?attached?メソッドは、ActiveStorage::Attached::Oneに定義されている。
Rails API: ActiveStorage::Attached::Oneのドキュメントから確かめることができる。

def blank?
  !attached?
end

ActiveStorage::Attached::One#blank?は、単にattached?を呼んで否定するように定義されている。
つまり、present?メソッドは、attached?否定の否定なので、機能的には結局attached?と等しかった。 attached?の方がきっと僅かに速いだけなので、この2つはエイリアスと呼んでいいと思う。

最後に

Railsのエラー文はもっとわかりやすくなって欲しいなと思った。終わり。