[Rails5.2]ActiveStorageの仕組み（図あり）と使ってみてわかったこと

遅ればせながらRails5.2で導入されたActiveStorageを使ってみました。

GitHubのReadmeやRailsGuidesの説明だけでなくソースコードも（一部）読んで調べたので、大まかな仕組みを図を使って説明してみます。

# ActiveStorageとは？
>Active StorageとはAmazon S3、Google Cloud Storage、Microsoft Azure Storageのような クラウドストレージサービスへのファイルのアップロードとそれらのファイルをActive Recordオブジェクトに添付することを容易にします。 開発およびテスト用のローカルディスクベースのサービスが付属しており、ファイルをバックアップおよび移行用の従属サービスにミラーリングすることができます。

（https://railsguides.jp/active_storage_overview.html より引用）

ということでRails純正のアップローダーになります。

# 具体的な使い方
RailsGuidesのセットアップをそのままやって設定しましょう。
https://railsguides.jp/active_storage_overview.html

設定に使うファイルは以下のとおりです。

- config/storage.yml
- config/environments/development.rb
- config/environments/production.rb

Amazon S3やGoogle Cloud Storageなどのクラウドストレージサービスを使わずに、まずローカルで試してみるだけならデフォルト設定のままで良いです。

デフォルト設定なら添付ファイルは、

- development/production環境：  `/storage`
- test環境：  `/tmp/storage`

に置きます。

設定後にささっと必要なことだけやるには、GitHubのReadmeのExamplesが簡潔でわかりやすいです。
https://github.com/rails/rails/tree/master/activestorage

```ruby
# Userモデルにファイルを添付するためにavatarを関連付けする
class User < ApplicationRecord
  has_one_attached :avatar
end

# ファイルを添付する
user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")

# 添付の有無を確認する
user.avatar.attached? # => true

# 添付ファイルを外す
user.avatar.purge
```

後ほど説明しますが、基本的には関連付けの仕組みを使ってモデルと添付したいファイルとを紐づけしています。
`attach`メソッドで添付して、`purge`メソッドで添付を外すことができます。

# ActiveStorageにおけるアップロードの仕組みについて
ActiveStorageにおけるアップロードは、モデルと添付したいファイルをActiveStorageにてあらかじめ用意されているAttachmentとBlobという2つのモデルを介して関連付けすることで実現されています。

![ActiveStorage_association.png](https://qiita-image-store.s3.amazonaws.com/0/277297/2055c23e-31f2-fa51-c719-464b93b81042.png)

Userモデルで`has_one_attached :avatar`と設定すると、実際には

- has_one :avatar_attachment
- has_one :avatar_blob, through: :avatar_attachment

のような`has_one :through`関連付けが行われます。

これによって、図のようにUserモデルがAttachmentモデルを介してBlobモデルへ関連付けされます。

ちなみに`has_one_attached`に渡した`avatar`はAttachmentオブジェクトのname属性の値となります。

以下のように`attach`メソッドを使ってファイル添付を実行すると、Blobオブジェクトが新規に生成されて各属性（filename, byte_sizeなど）に添付ファイルに関する値が設定されます。

```ruby
user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
```

以下に添付ファイルも加えた全体像を図示します。

![ActiveStorage_mapping.png](https://qiita-image-store.s3.amazonaws.com/0/277297/1f591f83-65d9-3eea-4296-959c80faffd8.png)


ActiveRecordによってUserオブジェクトがデータベース上のデータにマッピングされているように、Blobオブジェクトがローカルまたはクラウドストレージ上のファイルにマッピングされます。

こうして前述のUser, Attachment, Blobモデル間の関連付けにより、Userオブジェクトと添付ファイルを紐付けることができるようになります。

# 使ってみてわかったこと
CarrierWaveもfogも特に詳しいわけではないので軽い気持ちでActiveStorageを使ってみることにしましたが、仕組みもシンプルなものであることがわかりましたし、すんなりと使うことができました。

ただ、いくつか注意点があったので記載しておきます。

## Rails 5.2でも`rails active_storage:install`が必要だった
RailsGuidesには以下のように不要と書いてありましたが、実際には必要となったので実行しました。

>新しいRails 5.2アプリケーションでrails active_storage:installを実行する必要はありません。マイグレーションは自動的に生成されます。

`rails active_storage:install`自体は大したことはやっていません。
ActiveStorageの中にある、AttachmentモデルとBlobモデルの属性を定義したマイグレーションファイルのコピーをdb/migrateに生成するだけです。

`rails db:migrate`を実行すれば、db/schema.rbに`active_storage_attachments`、`active_storage_blobs`というテーブル（を生成するためのコード）が追加されます。

## rails consoleでattachメソッドを実行すると余計なログが出力されたままになる
これはなんだかよくわかりません。
以下のように添付を実行したとすると、一度`nil`が返ってきて`irb(main):002:0>`が表示されるのですが、すぐに次のログが出力されて、ログが出たままになってしまいます。

```ruby
irb(main):001:0> user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
.
.
Enqueued ActiveStorage::AnalyzeJob (Job ID: 0be98741-e05a-4d95-a6da-5ae2b2662cdb) to Async(default) with arguments: #<GlobalID:0x00007ff2ab2519a8 @uri=#<URI::GID gid://foo/ActiveStorage::Blob/1>>
=> nil
irb(main):002:0>   ActiveStorage::Blob Load (1.0ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
.
.
Performed ActiveStorage::AnalyzeJob (Job ID: 0be98741-e05a-4d95-a6da-5ae2b2662cdb) from Async(default) in 36.57ms
```

戻り値を返すタイミングとログを吐き出すタイミングがずれてしまっているということでしょうか？
次のコマンドを打つためにいちいちCtrl-cとかやっているのでちょっと面倒です。

## 添付の有無は`attach?`を使う(`nil?`や`blank?`や`present?`では例外が発生してしまう)
ファイルが添付されているかどうかを確認してから次の処理を実行させるという条件分岐を書くときにハマりました。

GitHubのActiveStorageのReadmeにも、RailsGuidesにもさらっと`attached?`が説明してあるのですが、それに気が付かずに`nil?`や`blank?`や`present?`を使うと例外が発生して、うまくいきませんでした。

そんなときは`attached?`メソッドを使いましょう。

```ruby
# 添付の有無を確認する
user.avatar.attached? # => true
```

## テストにMinitestのfixuturesを使うとかなり大変
RSpecを使っている方には無縁かもしれませんが、Minitestのfixturesを使って添付に関連した箇所のテストをやろうとしてハマりました。

テスト用データとして、

- users.ymlにUserモデルのテスト用オブジェクトを記載する
- tmp/storageに添付用のファイルを置く

ところまでは問題ないのですが、このままではAttachmentモデルやBlobモデルのテストデータがないためにエラーとなってしまいます。

ActiveStorageの仕組みを考慮すれば、以下のstackoverflowの回答に説明してあるように無理やりfixturesを使ってテストすることも可能ではありますが、現実的ではないと思います。

https://stackoverflow.com/questions/50453596/activestorage-fixtures-attachments

仕方ないので、テストの中でいちいち`attach`メソッドで添付し、テストが終わったあとに`purge`メソッドで添付を外すということをやりました。
（`purge`で外さないと次にテストしたときにすでに添付状態になっていました）

ここはActiveStorageの改善を待ちつつ、なにかいい方法を考えていかないといけませんね。

## HerokuではRailsアプリのstorageの下に置いた添付画像ファイルを表示できない
Heroku公式にもありますが、HerokuのEphemeral Diskという仕組みの影響でRailsアプリのstorageディレクトリに置いたファイルの添付がうまくいかず、画像ファイルをページに表示できませんでした。

https://devcenter.heroku.com/articles/active-storage-on-heroku

これはクラウドストレージを使うしかないようです。

# まとめ
初めてActiveStorageを使ってみましたが、仕組み自体はシンプルなのでこれからも使っていこうと思います。ただ現時点では至れり尽くせりという状態ではない部分もありますので、その点はご注意ください。

コードを読んだり、図を書いたりしていい勉強になりました。