#解決したいこと
ActiveStorage 使い方
みたいな感じでググると、viewに画像アップロードのインターフェースを設置した上で、form_with
を使ってアップロードできます、という記事が多く見られます。
そこで本記事では、viewのアップロードを介さずにモデルと画像とを紐付ける方法をご紹介します。
#どんなユースケース
画像のアップロード機能無しにモデルと画像を紐付けたいことなんてある?という方もいるかと思うので、先に自分が実際に導入したいモチベーションとなったユースケースについて説明します。
僭越ではありますが私は現在、TwitterAPIを通じて、ネット古着屋の情報を収集するサービス「Clotion」を鋭意開発中です。(よかったら見ていってください)
このサイトを実現するに当たり、各Twitterアカウントが掲載した画像を取得する必要がありました。各TwitterアカウントはShopモデルとして定義しているので、Shopモデルがimageモデルを複数持つ
という状況を作ろうとしたときに、viewを使わずにモデルと画像を紐つけたい
というユースケースに当たるわけです。
#実現方法
では早速どう実現したかについてご説明します。
ActiveStorageって何?とか導入方法は割愛をさせていただきます。Railsガイドをざっと眺めれば概要はつかめると思います。
ActiveStorageのインストールとmigrationが完了したところがスタート地点です。
まず、モデルへリレーションを定義します。1対1
のリレーションならhas_one_attached
、1対N
のリレーションならhas_many_attached
を利用します。今回は、1つの古着屋が複数の投稿画像を持つという関係になるのでhas_many_attached
を使いました。
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
などがよく使うメソッドかなと思います。
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を参照してください。
困っていた部分は、URL情報からどうRails(Ruby)で扱えるようにするか
というポイントだったので、ちょうど使ってみることにしました。open
メソッドを使っています。
# 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>]
と、良さげなログが出てきます
[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へのリプレースにはこちらの記事を参考にしました。
#まとめ
使い方に癖がありますが(Railsへ追加されるActiveXXXはどれも癖があるイメージ)、ActiveStorageとOpenURIによってウェブ上にある画像のURLからRubyオブジェクトへマッピングし、それをrailsのdbへ登録することができました。
特殊なユースケースだったかもしれませんが、この世界の誰かの一助になれば幸いです。