ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

Rails 5.2で追加予定のActiveStorageを使ってみる

こんにちは、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_attachmentsactive_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でファイルを選択してユーザーを作成すると...ちゃんと作成できました!!ファイルの参照も正しくできているようです。

f:id:ryu39:20170928101623p:plain

作成したファイルはデフォルトの設定では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

同じようにファイル付きでユーザーを作成することができます。

アップロードしたファイルは、バケットの直下にユニークな文字列をつけて保存されるようです。

f:id:ryu39:20170928102522p:plain

ファイルの参照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のリリースが本当に楽しみです。

それでは、最後までご覧いただきありがとうございました🙇