Viewを介さずActiveStorageを使いたい

#解決したいこと
`ActiveStorage 使い方`みたいな感じでググると、viewに画像アップロードのインターフェースを設置した上で、`form_with`を使ってアップロードできます、という記事が多く見られます。
そこで本記事では、viewのアップロードを介さずにモデルと画像とを紐付ける方法をご紹介します。
#どんなユースケース
画像のアップロード機能無しにモデルと画像を紐付けたいことなんてある？という方もいるかと思うので、先に自分が実際に導入したいモチベーションとなったユースケースについて説明します。

僭越ではありますが私は現在、TwitterAPIを通じて、ネット古着屋の情報を収集するサービス[「Clotion」](https://clotion.herokuapp.com/)を鋭意開発中です。(よかったら見ていってください)

このサイトを実現するに当たり、各Twitterアカウントが掲載した画像を取得する必要がありました。各TwitterアカウントはShopモデルとして定義しているので、`Shopモデルがimageモデルを複数持つ` という状況を作ろうとしたときに、`viewを使わずにモデルと画像を紐つけたい`というユースケースに当たるわけです。

#実現方法
では早速どう実現したかについてご説明します。
ActiveStorageって何？とか導入方法は割愛をさせていただきます。[Railsガイド](https://railsguides.jp/active_storage_overview.html)をざっと眺めれば概要はつかめると思います。

ActiveStorageのインストールとmigrationが完了したところがスタート地点です。

まず、モデルへリレーションを定義します。`1対1`のリレーションなら`has_one_attached`、`1対N`のリレーションなら`has_many_attached`を利用します。今回は、1つの古着屋が複数の投稿画像を持つという関係になるので`has_many_attached`を使いました。

```shop.rb
class Shop < ApplicationRecord
  # validationとかいろいろ

  # 今回の主役
  has_many_attached :images
end
```

`images`という名前は任意に付け替えられます。imagesモデルを用意する必要もないです。
イメージとしては、各Shopモデルに対して`shop.images`とすればそのshopが持つ画像を取ってこれる感じです。

ではshopモデルへ画像を割り当てていきましょう。割り当てには`attach`メソッドを使います。

`has_many_attached`(or has_one_attached) でリレーションを定義すると、いくつかActiveStorageを使って画像情報を操作するためのヘルパーが定義されます。モデルと画像情報の紐付けには`attach`、モデルが画像情報を保持しているかどうかを聞く`attached?`、モデルから画像情報を削除する`purge`などがよく使うメソッドかなと思います。

```fetch_images_with_active_storage.rb
Shop.all.each do |s|
  # TwitterAPIを介して、各古着屋の投稿から画像を取得する
  # imagesは各画像のURLの配列
  images = twitter_client.fetch_images(s.twitter_user_id)
  images.each do |image|
    #'s'に対して'image'を割り当てたい
    s.image.attach(........)
    # 引数に何渡せばいいんだ？？？？
  end
end
```

整理するとimagesをeachで回している`image`は画像のURLを持っています。画像情報ではなくURLを保持している、というところがとても厄介で、URLから画像情報としてRails(Ruby)で扱えるように一手間を加える必要がありそう、というところまではわかるのですが、これをどう実現するかというところで詰まりました。

## OpenURI
これをRubyの標準ライブラリであるOpenURIを使って解決しました。これは何かというと、`http/ftp上のリソースをRubyオブジェクトへマッピングしてよしなにできるライブラリ`です。詳しくは[ruby-doc](https://docs.ruby-lang.org/ja/latest/library/open=2duri.html)を参照してください。
困っていた部分は、`URL情報からどうRails(Ruby)で扱えるようにするか`というポイントだったので、ちょうど使ってみることにしました。`open`メソッドを使っています。

```fetch_images_with_active_storage.rb
# requireする必要がある
require 'open-uri'

Shop.all.each do |s|
  # TwitterAPIを介して、各古着屋の投稿から画像を取得する
  # imagesは各画像のURLの配列
  images = twitter_client.fetch_images(s.twitter_user_id)
  images.each_with_index do |image, i|
    # fileストリームのイメージでioと命名する
    io = open(image)
    s.image.attach(io: io, filename: "#{s.id}_#{i}")
  end
end
```

`open`メソッドで取得したストリームをioとしてattachへ渡しています。また、filenameが無いとActiveRecordから怒られるので、適当に名前をつけてます。今回のユースケースでは、shopモデルを介してイテレートした結果を表示するだけで良かったのでfilenameは適当につけましたが、filenameを参照して何かしたい場合はもう少しまともな名前をつけたほうが良さそうです。

これを実行すると

```
=> [#<ActiveStorage::Attachment:0x00007fcc470a7b78
  id: 1,
  name: "images",
  record_type: "Shop",
  record_id: 1,
  blob_id: 1,
  created_at: Sat, 07 Dec 2019 15:13:18 UTC +00:00>]
```
と、良さげなログが出てきます

```irb
[1] pry(main)> Shop.first.images
=> #<ActiveStorage::Attached::Many:0x00007fcc4798d2d8
 @dependent=:purge_later,
 @name="images",
 @record=
  #<Shop:0x00007fcc49429808
   id: 4,
   name: "𝚌𝚘𝚕𝚞𝚖𝚗",
   url: "https://t.co/zYSbCPqfAz",
   created_at: Mon, 25 Nov 2019 13:29:37 UTC +00:00,
   updated_at: Sat, 07 Dec 2019 15:13:18 UTC +00:00,
   twitter_user_id: 1040246647136051202,
   twitter_url: "https://twitter.com/column_tyo",
   twitter_thumbnail_image_url: "https://pbs.twimg.com/profile_images/1148918451223711744/GcOAVrns_normal.jpg">>
```

これもなんとなくできてそうな感があります。

実際にviewの中で`image_tag`へこのimage情報を渡すことで表示することができました。
開発しているClotionではこのようにして古着の画像情報を表示しています。

# ActiveStorageのパワー
ここまではローカル上の開発のお話でした。つまり、取得した画像はローカルのストレージに保存されるような仕組みです。
取り扱い方は若干ピーキーですが、画像に関するモデルの定義なしにリレーションの記述だけでこれだけのことができるようになるのは確かに便利だなーと感じていました。

ローカルで画像保存ができるようになったので、外部のクラウドサービス(今回はamazon s3を使いました)と連携しようとしたときにこの機能の本当のパワーを感じました。
というのもちょちょっとconfigをいじっただけで、コードはそのままでs3を使う形に変更することができました。これには本当に感動しました。
s3へのリプレースには[こちらの記事](https://qiita.com/daichi41/items/af2a56ea46c13ca55fd3)を参考にしました。

#まとめ
使い方に癖がありますが(Railsへ追加されるActiveXXXはどれも癖があるイメージ)、ActiveStorageとOpenURIによってウェブ上にある画像のURLからRubyオブジェクトへマッピングし、それをrailsのdbへ登録することができました。
特殊なユースケースだったかもしれませんが、この世界の誰かの一助になれば幸いです。