概要
MITライセンスに基いて翻訳・公開いたします。
- リポジトリ: paperclip/MIGRATING.md at master · thoughtbot/paperclip
- 原文更新日: 2018/05/14
- サイト: thoughtbot
- ライセンス: MIT
翻訳後に更新が入っていますのでご注意ください。
PaperclipからActiveStorageへの移行ガイド by thoughtbot(翻訳)
PaperclipとActiveStorageは、いずれも同じような問題に対して類似のソリューションを提供しますので、一方から他方への移行は単純なデータ書き換えとなります。
PaperclipからActiveStorageへの移行手順は次のとおりです。
- ActiveStorageデータベースマイグレーションを適用する
- ストレージを設定する
- データベースのデータをコピーする
- ファイルをコピーする
- アプリのテストを更新する
- アプリのビューを更新する
- アプリのコントローラを更新する
- アプリのモデルを更新する
1. ActiveStorageデータベースマイグレーションを適用する
ActiveStorageのインストール指示に沿って進めます。Gemfileにmini_magick
gemを追加しておきたいと思うことでしょう。
rails active_storage:install
2. ストレージを設定する
こちらもActiveStorageの設定指示に沿って進めます(訳注: リンク先はedgeguidesです)。
3. データベースのデータをコピーする
ActiveStorageは、active_storage_blobs
テーブルとactive_storage_attachments
テーブルをファイルメタデータで検索できることを前提とします。Paperclipはこれらのファイルメタデータを、関連付けられたオブジェクトのテーブルに直接保存します。
avatar
を1つ持つUser
を例に説明します。Paperclipの場合は次のようになります。
class User < ApplicationRecord
has_attached_file :avatar
end
Paperclipマイグレーションによって、次のようなテーブルが生成されます。
create_table "users", force: :cascade do |t|
t.string "avatar_file_name"
t.string "avatar_content_type"
t.integer "avatar_file_size"
t.datetime "avatar_updated_at"
end
これを以下のテーブルに変換します。
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.integer "record_id", null: false
t.integer "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade 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"], name: "index_active_storage_blobs_on_key", unique: true
end
ファイルを元の置き場所から変えないことを前提にする場合、以下がマイグレーションとなります。それ以外の場合は、次のセクションを先に読んでマイグレーションを適宜変更してください。
Dir[Rails.root.join("app/models/**/*.rb")].sort.each { |file| require file }
class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
require 'open-uri'
def up
# PostgreSQLの場合
get_blob_id = 'LASTVAL()'
# MariaDBの場合
# get_blob_id = 'LAST_INSERT_ID()'
# SQLiteの場合
# get_blob_id = 'LAST_INSERT_ROWID()'
active_storage_blob_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
INSERT INTO active_storage_blobs (
key, filename, content_type, metadata, byte_size,
checksum, created_at
) VALUES (?, ?, ?, '{}', ?, ?, ?)
SQL
active_storage_attachment_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
INSERT INTO active_storage_attachments (
name, record_type, record_id, blob_id, created_at
) VALUES (?, ?, ?, #{get_blob_id}, ?)
SQL
models = ActiveRecord::Base.descendants.reject(&:abstract_class?)
transaction do
models.each do |model|
attachments = model.column_names.map do |c|
if c =~ /(.+)_file_name$/
$1
end
end.compact
model.find_each.each do |instance|
attachments.each do |attachment|
active_storage_blob_statement.execute(
key(instance, attachment),
instance.send("#{attachment}_file_name"),
instance.send("#{attachment}_content_type"),
instance.send("#{attachment}_file_size"),
checksum(instance.send(attachment)),
instance.updated_at.iso8601
)
active_storage_attachment_statement.
execute(attachment, model.name, instance.id, instance.updated_at.iso8601)
end
end
end
end
active_storage_attachment_statement.close
active_storage_blob_statement.close
end
def down
raise ActiveRecord::IrreversibleMigration
end
private
def key(instance, attachment)
SecureRandom.uuid
# または以下を使用
# instance.send("#{attachment}_file_name")
end
def checksum(attachment)
# ディスク上に保存されたローカルファイル
url = attachment.path
Digest::MD5.base64digest(File.read(url))
# 別のコンピュータに保存されたリモートファイル
# url = attachment.url
# Digest::MD5.base64digest(Net::HTTP.get(URI(url)))
end
end
4. ファイルをコピーする
上記のマイグレーションではファイルはそのままになります。しかしPaperclipとActiveStorageでは、ファイルのデフォルトの置き場所が異なっています。
Paperclipの場合、デフォルトでは以下のように置かれます。
public/system/users/avatars/000/000/004/original/the-mystery-of-life.png
ActiveStorageの場合は以下のようになります。
storage/xM/RX/xMRXuT6nqpoiConJFQJFt6c9
このxMRXuT6nqpoiConJFQJFt6c9
がactive_storage_blobs.key
の値に該当します。上のマイグレーションでは単にファイル名を使いましたが、代わりにUUIDを使うこともできます。
外部のファイルホスト(S3、Azure Storage、GCSなど)に配置されているファイルの移行は、本ドキュメントの冒頭の説明の範囲外となります。ローカルディスクストレージがあるかどうかの探索は以下のようになります。
#!bin/rails runner
class ActiveStorageBlob < ActiveRecord::Base
end
class ActiveStorageAttachment < ActiveRecord::Base
belongs_to :blob, class_name: 'ActiveStorageBlob'
belongs_to :record, polymorphic: true
end
ActiveStorageAttachment.find_each do |attachment|
name = attachment.name
source = attachment.record.send(name).path
dest_dir = File.join(
"storage",
attachment.blob.key.first(2),
attachment.blob.key.first(4).last(2))
dest = File.join(dest_dir, attachment.blob.key)
FileUtils.mkdir_p(dest_dir)
puts "Moving #{source} to #{dest}"
FileUtils.cp(source, dest)
end
5. アプリのテストを更新する
have_attached_file
に代わる独自のマッチャーを書く必要があります。以下は、Paperclipが提供するマッチャーとコンセプト的に近いマッチャーです。
RSpec::Matchers.define :have_attached_file do |name|
matches do |record|
file = record.send(name)
file.respond_to?(:variant) && file.respond_to?(:attach)
end
end
6. ビューを更新する
Paperclipの場合は以下のような感じになります。
image_tag @user.avatar.url(:medium)
ActiveStorageの場合は以下のような感じになります。
image_tag @user.avatar.variant(resize: "250x250")
7. コントローラを更新する
コントローラの更新は不要なはずです。しかし、上述のデータベーススキーマをチェックしてみるとJOIN
があるかもしれません。
たとえばコントローラが次のようになっているとします。
def index
@users = User.all.order(:name)
end
ビューは次のようになっているとします。
<ul>
<% @users.each do |user| %>
<li><%= image_tag user.avatar.variant(resize: "10x10"), alt: user.name %></li>
<% end %>
</ul>
ループ内で添付ファイルを毎回読み込んでいるので、これではN+1問題が発生してしまいます。
そこで、コントローラやモデルが変更なしでも問題なく動作しているとしても、ループを再チェックして必要ならincludes
を足したいと思うことでしょう。ActiveStorageは、avatar_attachment
リレーションシップとavatar_blob
リレーションシップをhas-oneリレーションに追加し、avatar_attachments
リレーションとavatar_blobs
リレーションをhas-manyに追加します。
def index
@users = User.all.order(:name).includes(:avatar_attachment)
end
8. アプリのモデルを更新する
Rails edgeguidesのファイルをレコードに添付する方法に沿って進めます。たとえば、avatar
を1つ持つUser
は次のように表されます。
class User < ApplicationRecord
has_one_attached :avatar
end
すべてのリサイズは、ビューでvariantとして行われます。
9. Paperclipを除去する
Gemfile
からPaperclip gemを削除してbundle
を実行します。テストを実行して完了を確認してください。