ActiveStorageの挙動を調べる

Rails5.2がRC2になっていたのでActiveStorageの挙動を少し調べてみました。

準備

まずActiveStorageを使うためにmigrationファイルを作成します。

bin/rails active_storage:install
bin/rails db:migrate

ActiveStorageではCarrierwaveなどと違いファイルの情報を別テーブルに保存するため、これを実行してActiveStorage用のテーブルを作成します。

テーブルがないと↓のように怒られます。

ActiveRecord::StatementInvalid: Could not find table 'active_storage_attachments'

migrateを実行するとactive_storage_blobsactive_storage_attachments というテーブルが作成されます。

class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
  def change
    create_table :active_storage_blobs do |t|
      t.string   :key,        null: false
      t.string   :filename,   null: false
      t.string   :content_type
      t.text     :metadata
      t.bigint   :byte_size,  null: false
      t.string   :checksum,   null: false
      t.datetime :created_at, null: false

      t.index [ :key ], unique: true
    end

    create_table :active_storage_attachments do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false
      t.references :blob,     null: false

      t.datetime :created_at, null: false

      t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
    end
  end
end

モデルの設定

Userというモデルにavatarという名前でデータをアップロードできる様にします。

has_one_attachedだと一つだけ、has_many_attachedではデータをアップロードできます。

class User < ApplicationRecord
  has_one_attached :avatar
end

アップロード

has_xxx_attachedで設定した名前のメソッドがはえるのでそれにattachでアップロードします。

user.avatar.attach(io: File.open(Rails.root.join("./tmp/avatar.jpg")), filename: "avatar.jpg", content_type: "image/jpg")

実行時のログ

先にkeyが作られてJobで後からアップロード処理される様です。

  ActiveStorage::Attachment Load (0.1ms)  SELECT  "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 1], ["record_type", "User"], ["name", "avatar"], ["LIMIT", 1]]
  Disk Storage (4.6ms) Uploaded file to key: dHerwrM2wUCeBdbGv8fBg41W (checksum: qtC9awH9X+Lp5VABpTx0RQ==)
   (0.1ms)  begin transaction
  ActiveStorage::Blob Create (0.4ms)  INSERT INTO "active_storage_blobs" ("key", "filename", "content_type", "metadata", "byte_size", "checksum", "created_at") VALUES (?, ?, ?, ?, ?, ?, ?)  [["key", "dHerwrM2wUCeBdbGv8fBg41W"], ["filename", "avatar.jpg"], ["content_type", "image/jpeg"], ["metadata", "{\"identified\":true}"], ["byte_size", 85092], ["checksum", "qtC9awH9X+Lp5VABpTx0RQ=="], ["created_at", "2018-03-31 14:08:08.224345"]]
   (0.8ms)  commit transaction
   (0.1ms)  begin transaction
  ActiveStorage::Attachment Create (0.6ms)  INSERT INTO "active_storage_attachments" ("name", "record_type", "record_id", "blob_id", "created_at") VALUES (?, ?, ?, ?, ?)  [["name", "avatar"], ["record_type", "User"], ["record_id", 1], ["blob_id", 1], ["created_at", "2018-03-31 14:08:08.249993"]]
  User Update (0.2ms)  UPDATE "users" SET "updated_at" = ? WHERE "users"."id" = ?  [["updated_at", "2018-03-31 14:08:08.252816"], ["id", 1]]
   (0.8ms)  commit transaction
Enqueued ActiveStorage::AnalyzeJob (Job ID: 15433d76-d866-4d41-b32b-5d2b5e04bf4c) to Async(default) with arguments: #<GlobalID:0x007faf8a1fdc40 @uri=#<URI::GID gid://rails52/ActiveStorage::Blob/1>>
=> nil
ActiveStorage::Blob Load (0.1ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
Performing ActiveStorage::AnalyzeJob (Job ID: 15433d76-d866-4d41-b32b-5d2b5e04bf4c) from Async(default) with arguments: #<GlobalID:0x007faf8a26dec8 @uri=#<URI::GID gid://rails52/ActiveStorage::Blob/1>>
Skipping image analysis because the mini_magick gem isn't installed
   (0.1ms)  begin transaction
  ActiveStorage::Blob Update (0.2ms)  UPDATE "active_storage_blobs" SET "metadata" = ? WHERE "active_storage_blobs"."id" = ?  [["metadata", "{\"identified\":true,\"analyzed\":true}"], ["id", 1]]
   (0.8ms)  commit transaction
Performed ActiveStorage::AnalyzeJob (Job ID: 15433d76-d866-4d41-b32b-5d2b5e04bf4c) from Async(default) in 48.51ms

active_storage_attachmentsテーブル

カラム名
id 1
name avatar
record_type User
record_id 1
blob_id 1
created_at 2018-03-31 14:08:08.249993

active_storage_blobsテーブル

カラム名
id 1
key dHerwrM2wUCeBdbGv8fBg41W
filename avatar.jpg
content_type image/jpeg
metadata {“identified”:true,“analyzed”:true}
byte_size 85092
checksum qtC9awH9X+Lp5VABpTx0RQ==
created_at 2018-03-31 14:08:08.224345

表示

viewに

<%= image_tag(url_for(@user.avatar)) %>

とすれば画像が表示されます。

URL

今回ローカルストレージを使用して発行されたURLは

/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--02c53088de34ddb4f797a239219dbe05b2b5f8a5/avatar.jpg

でした。

アクセスしてみる

発行されたURLにアクセスするとcontent_typeなどのパラメータがついたURLにリダイレクトされる様です。

Started GET "/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--02c53088de34ddb4f797a239219dbe05b2b5f8a5/avatar.jpg" for 127.0.0.1 at 2018-04-01 00:12:46 +0900
Processing by ActiveStorage::BlobsController#show as JPEG
  Parameters: {"signed_id"=>"eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--02c53088de34ddb4f797a239219dbe05b2b5f8a5", "filename"=>"avatar"}
  ActiveStorage::Blob Load (0.2ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ vendor/bundle/ruby/2.4.0/gems/activerecord-5.2.0.rc2/lib/active_record/log_subscriber.rb:98
  Disk Storage (1.4ms) Generated URL for file at key: dHerwrM2wUCeBdbGv8fBg41W (/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaDFrU0dWeWQzSk5NbmRWUTJWQ1pHSkhkamhtUW1jME1WY0dPZ1pGVkE9PSIsImV4cCI6IjIwMTgtMDMtMzFUMTU6MTc6NDYuNzA4WiIsInB1ciI6ImJsb2Jfa2V5In19--34377fd4cb46b8971454ae8431fe71d42ac979cc/avatar.jpg?content_type=image%2Fjpeg&disposition=inline%3B+filename%3D%22avatar.jpg%22%3B+filename%2A%3DUTF-8%27%27avatar.jpg)
Redirected to http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaDFrU0dWeWQzSk5NbmRWUTJWQ1pHSkhkamhtUW1jME1WY0dPZ1pGVkE9PSIsImV4cCI6IjIwMTgtMDMtMzFUMTU6MTc6NDYuNzA4WiIsInB1ciI6ImJsb2Jfa2V5In19--34377fd4cb46b8971454ae8431fe71d42ac979cc/avatar.jpg?content_type=image%2Fjpeg&disposition=inline%3B+filename%3D%22avatar.jpg%22%3B+filename%2A%3DUTF-8%27%27avatar.jpg
Completed 302 Found in 4ms (ActiveRecord: 0.2ms)


Started GET "/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaDFrU0dWeWQzSk5NbmRWUTJWQ1pHSkhkamhtUW1jME1WY0dPZ1pGVkE9PSIsImV4cCI6IjIwMTgtMDMtMzFUMTU6MTc6NDYuNzA4WiIsInB1ciI6ImJsb2Jfa2V5In19--34377fd4cb46b8971454ae8431fe71d42ac979cc/avatar.jpg?content_type=image%2Fjpeg&disposition=inline%3B+filename%3D%22avatar.jpg%22%3B+filename%2A%3DUTF-8%27%27avatar.jpg" for 127.0.0.1 at 2018-04-01 00:12:46 +0900
Processing by ActiveStorage::DiskController#show as JPEG
  Parameters: {"content_type"=>"image/jpeg", "disposition"=>"inline; filename=\"avatar.jpg\"; filename*=UTF-8''avatar.jpg", "encoded_key"=>"eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaDFrU0dWeWQzSk5NbmRWUTJWQ1pHSkhkamhtUW1jME1WY0dPZ1pGVkE9PSIsImV4cCI6IjIwMTgtMDMtMzFUMTU6MTc6NDYuNzA4WiIsInB1ciI6ImJsb2Jfa2V5In19--34377fd4cb46b8971454ae8431fe71d42ac979cc", "filename"=>"avatar"}
  Disk Storage (0.4ms) Downloaded file from key: dHerwrM2wUCeBdbGv8fBg41W
  Rendering text template
  Rendered text template (0.0ms)
Sent data  (0.4ms)
Completed 200 OK in 2ms (Views: 0.3ms | ActiveRecord: 0.0ms)

今まで使っていたCarrierwaveでは保存したファイルのURLが直接発行されていたのですがActiveStorageでは挙動が少し違う様です。

本番でS3などを使った場合挙動がどうなるか今度調べてみようと思います。