こんにちは、Webエンジニアの本間です。 先週、広島で RubyKaigi 2017 が開催されましたね。私は参加できなかったのですが、興味深い発表が多かったので、公開されているスライドは一通りチェックしたいなと思っています。
さて、今回のブログでは、少し乗り遅れた感がありますがRails 5.2で追加予定の ActiveStorage を使って見て、感想や気になる点などを書いていこうかなと思います。
※注意:現在開発中のalphaバージョンのRails 5.2を使用しています。記事公開後の修正によってはサンプルコードが動作しなかったり、記載された内容と動作が異なる可能性があります。
概要
アプリケーションを開発していると、文書や画像などのファイルをデータベースのレコードと紐づけて保管したいケースがそれなりの頻度であります。 これまでRailsにはこのようなケースに対する統一的な実装方法がなく、 carrierwave をはじめとしたgemを使うか、自前で実装するかのどちらかでした。 ActiveStorageはこのユースケースに対して、Rails単体で実現するための機能になります。
また、以下のように環境に応じて保存先のストレージを切り替えたいという要望も出てきます。
- ローカルでの開発やユニットテストでは、ローカルのディスクを使用
- 本番環境では、クラウドストレージ(Amazon S3など)を使用
これを自前の実装でやるのは結構大変ですが、ActiveStorageでは非常に簡単に実現することができます。現時点では、以下のストレージが選択可能なようです。
- ローカルディスク
- Amazon S3
- Google Cloud Storage
- Azure Storage
使って見た
今回、ActiveStorageを使ってサンプルアプリケーションを作成しました。下記URLにローカルでの動かし方を含めておいてあります。
https://bitbucket.org/unifa-public/activestorage_test
scaffoldで自動生成される画面にActiveStorageのファイル保存、ダウンロードを加えただけの内容になっています。herokuでもしばらく動かしていますので、とりあえず触って見たい方はこちらからでも使うことができます。
https://powerful-mesa-96001.herokuapp.com/users
準備
まず開発中のRailsを使って rails new
を実行しています。開発中のRailsを使う方法は Railsガイド Ruby on Rails に貢献する方法 を参考にしました。
rails new
した後ですが、今回はAmazon S3との連携もテストする予定なので、aws-sdk-s3
をGemfileに追加しています。
これを追加しておかないと、あとで rails s
した時にエラーになってしまいます。おそらく他の外部ストレージを使うときも同様の対応が必要と思われます。(余談ですが、AWS SDK v3からは機能ごとにgemが分かれたため、必要なgemのみを追加、更新しやすくなっており大変ありがたいです)
# snip... gem 'aws-sdk-s3'
次に config/storage.yml
に使用するストレージの情報を記載します。デフォルトではローカルディスクとテスト用の設定がありますが、これに加えAmazon S3の設定を追加しました。
# snip... amazon: service: S3 access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> region: ap-northeast-1 bucket: <%= ENV['AWS_S3_BUCKET'] %>
ActiveStorage用のテーブルの作成
以下のコマンドを実行してActiveStorage用のテーブルのマイグレーションファイルの生成、およびマイグレーションを実行します。
$ rails active_storage:install $ rails db:migrate
active_storage_attachments
と active_storage_blobs
という2つのテーブルが生成されるようです。
Modelの修正
ファイルと紐づけたいModelクラスにて has_one_attached
or has_many_attached
を宣言するだけです(2つのメソッドの違いは1ファイルのみか、複数ファイルか)。すごく簡単!!
class User < ApplicationRecord has_one_attached :avatar end
Controllerの修正
修正のポイントは以下になります。
- 一覧表示時はN+1問題が発生する可能性があるため、外部ファイルを触る場合
with_attached_avatar
を呼び出す。 - 現状、作成、更新時は「Model自体の保存」と「Modelに紐づけるファイルのアップロード、保存」を分けて記述する必要がある模様。2行に分かれるためトランザクジョンで囲っておいた方が良いはず。
- 更新時のファイルの置き換えや削除は、自動的に非同期で関連するレコード、ファイル削除してくれる(ActiveStorage::PurgeJob)ため特別な対応は不要。
以下はサンプルアプリケーションのindexとcreateアクションの抜粋です。
class UsersController < ApplicationController # snip... def index @users = User.with_attached_avatar.all end def create @user = User.new(user_params) ActiveRecord::Base.transaction do unless @user.save render :new and return end if params[:user][:avatar].present? @user.avatar.attach(params[:user][:avatar]) end end redirect_to @user, notice: 'User was successfully created.' end # snip... end
Viewの修正
修正箇所は2箇所です。まず、ユーザー作成/更新時の画面でファイルアップロード用の項目を追加しています。
<%= form_with(model: user, local: true) do |form| %> : <div class="field"> <%= form.label :avatar %> <%= form.file_field :avatar %> </div> : <% end %>
次に詳細画面でアバター画像を表示するための修正です。
url_for(@user.avatar)
で参照用の一時URLを取得することができます。
今回のサンプルアプリケーションでは、これをimgタグで使用することで画像を表示しています。
<%= image_tag(url_for(@user.avatar)) %>
ActiveStorage用にコード追加、修正した点は以上になります。自前で実装すると非常に面倒な機能が、このコード量だけで実現できるのは本当に凄いなと思いました。
ローカルディスクで動かす
それでは早速動かして見ます。
$ rails s -d $ open http://localhost:3000/users
ユーザー作成画面に遷移し、Avatarでファイルを選択してユーザーを作成すると...ちゃんと作成できました!!ファイルの参照も正しくできているようです。
作成したファイルはデフォルトの設定ではRailsルートの storage
以下にいくつかのディレクトリ階層を経て保管されているようです。
ファイルの参照は、http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbH.../ファイル名
というURLにアクセスしていますが、すぐにリダイレクトされて実際のファイルのURLにアクセスするようです。(ローカルディスクの場合、同じくlocalhostへのリクエストになる)
Amazon S3で動かす
次に外部ストレージをAmazon S3にして動かして見ます。config/environments/development.rb
の31行目を修正します。
# config.active_storage.service = :local config.active_storage.service = :amazon
今回の設定ではAWSに関するパラメーターを環境変数で取得するようにしているため、これらの環境変数をセットしてからRailsを起動します。
$ kill $( cat tmp/pids/server.pid ) $ export AWS_ACCESS_KEY_ID=AWSのアクセスキー $ export AWS_SECRET_ACCESS_KEY=AWSのシークレット $ export AWS_S3_BUCKET=バケット名 $ rails s -d $ open http://localhost:3000/users
同じようにファイル付きでユーザーを作成することができます。
アップロードしたファイルは、バケットの直下にユニークな文字列をつけて保存されるようです。
ファイルの参照URLはローカルディスクと同じなのですが、リダイレクト先がS3のPresigned URLになっていますね。 以上が今回触って見た内容になります。
Direct Upload
今回のサンプルアプリケーションでは試していないのですが、ファイルを直接外部ストレージにアップロードする Direct Upload の機能もあるようです。 これをクラウドストレージに対して使用すれば、大きなファイルをRailsサーバーに負荷をかけることなくアップロードできるようになります。
少し設定を追加するだけでできそうなので、ぜひ使ってみたい機能だと思いました。
気になる点
使って見て良かった点だけでなく、少し気になる点もありましたので以下に記載しておきます。
- 削除時は非同期に処理されるが、作成・更新は同期処理なのでレスポンスを返すまでに時間がかかる可能性あり。
- 使用するストレージの指定はアプリケーション全体で共通、ファイルごとに変更はできない。
- ファイルが複数ある時でアップロードが途中で失敗した場合、アップロード済みファイルがゴミとして残ってしまいそう。
- 現状、コントローラーでは「Userの作成」と「アバターのアップロード、保存」のコードが分かれているが、以下のように一行で書けると個人的に好き。
class UsersController < ApplicationController def create @user = User.new(user_params) if @user.save redirect_to @user, notice: 'User was successfully created.' else render :new end end private def user_params params.require(:user).permit(:name, :age, :avatar) end end
まとめ
今回、ActiveStorageを触って見ましたが、本当に少ないコード量でやりたいことがサクッとできるという点が、まさにRailsっぽい機能だと思いました。 今後さらに機能が追加される可能性もありますので、Rails 5.2のリリースが本当に楽しみです。
それでは、最後までご覧いただきありがとうございました🙇