【Rails】ActiveStorageで、既存のオブジェクトに紐づいたファイルを他のオブジェクトに流用して紐づける方法

まとめ

  • UserモデルとAuthorモデルがある
  • 既存のユーザー(user)にアタッチしてあるファイルを、そのまま流用して別の「著者」(author)にアタッチしたい

上記のことが、以下のコードで可能という記事です。

author.authors_image.attach(user.users_image.blob)

やってみる

  • rails newして、User / Author モデルを作る
  • ActiveStorageを導入して、has_one_attachedでモデルとファイルを紐づける設定をする
  • ビューで画像を表示できるようにしておく
    • 下のサンプルコードはUserのビューのものだけだが、Authorのビューでも同じことをしておく
class User < ApplicationRecord
  has_one_attached :users_image
end

class Author < ApplicationRecord
  has_one_attached :authors_image
end
<div id="<%= dom_id user %>">
  <p>
    <strong>Name:</strong>
    <%= user.name %>
  </p>

  <p>
    <strong>image:</strong>
    <% if user.users_image.present? %>
      <%= image_tag user.users_image %>
    <% end %>
  </p>

</div>

適当にユーザーを作って、画像をアタッチして保存。

user.users_imageをビューで表示

次に、適当に著者を作成(この時点では画像は持たせない)。

適当に画像を持たない著者を作成

Railsコンソールを開いて、ユーザーと著者を取得し、冒頭のコマンドを実行。

$ user = User.first
$ author = Author.first
$ author.authors_image.attach(user.users_image.blob)
attach-test-app(dev)> author.authors_image.attach(user.users_image.blob)
  ActiveStorage::Attachment Load (0.1ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = 2 AND "active_storage_attachments"."record_type" = 'User' AND "active_storage_attachments"."name" = 'users_image' LIMIT 1 /*application='AttachTestApp'*/
  ActiveStorage::Blob Load (0.1ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = 2 LIMIT 1 /*application='AttachTestApp'*/
  TRANSACTION (0.5ms)  BEGIN immediate TRANSACTION /*application='AttachTestApp'*/
  ActiveStorage::Blob Load (4.4ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" INNER JOIN "active_storage_attachments" ON "active_storage_blobs"."id" = "active_storage_attachments"."blob_id" WHERE "active_storage_attachments"."record_id" = 3 AND "active_storage_attachments"."record_type" = 'Author' AND "active_storage_attachments"."name" = 'authors_image' LIMIT 1 /*application='AttachTestApp'*/
  ActiveStorage::Attachment Load (0.2ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = 3 AND "active_storage_attachments"."record_type" = 'Author' AND "active_storage_attachments"."name" = 'authors_image' LIMIT 1 /*application='AttachTestApp'*/
  ActiveStorage::Attachment Create (0.2ms)  INSERT INTO "active_storage_attachments" ("name", "record_type", "record_id", "blob_id", "created_at") VALUES ('authors_image', 'Author', 3, 2, '2025-02-23 02:56:27.784094') RETURNING "id" /*application='AttachTestApp'*/
  Author Update (0.1ms)  UPDATE "authors" SET "updated_at" = '2025-02-23 02:56:27.788402' WHERE "authors"."id" = 3 /*application='AttachTestApp'*/
  TRANSACTION (0.1ms)  COMMIT TRANSACTION /*application='AttachTestApp'*/
=>
#<ActiveStorage::Attached::One:0x00007f509baccea0
 @name="authors_image",
 @record=
  #<Author:0x00007f509b672620
   id: 3,
   name: "著者です",
   created_at: "2025-02-23 02:52:29.138843000 +0000",
   updated_at: "2025-02-23 02:56:27.788402000 +0000">>

著者のページを更新すると、ビュー上のコードimage_tag(author.authors_image)によってユーザーと同じ画像が表示されます。

ユーザーと同じ画像が著者ページで表示される

ユーザー / 著者に紐づく画像が同じものであることは、Railsコンソールからも確かめられます。

attach-test-app(dev)> user.users_image.blob == author.authors_image.blob
=> true

なぜできるのか

ActiveStorageでオブジェクトとファイルを紐づける際、オブジェクト(今回であればユーザーや著者)とファイル(1)は下のように中間テーブル(2)を通して結ばれます。

  1. ファイルに関するデータのためのテーブル: active_storage_blobs
  2. 中間テーブル: active_storage_attachments
    1. 1のblobsとオブジェクトを接続する役割

railsguides.jp

冒頭のコードは、「著者オブジェクトの画像として、ユーザーオブジェクトに紐づく画像のファイルデータ(blob)をアタッチしてね」という意味だったわけですね(attach(user.users_image)だとうまくいきません)。

author.authors_image.attach(user.users_image.blob)

※更に調べてみると、user.users_imageで取得できるオブジェクトのクラスはActiveStorage::AttachmentではなくActiveStorage::Attached::Oneです。このオブジェクトはアタッチされたファイルのラッパーのようなクラスのようで、ファイルがアタッチされているかを確認するattached?メソッドもここに定義されていました。

api.rubyonrails.org

実行結果のSQLでも分かる通り、このときに作られているのは中間テーブルのレコードだけです(すでにストレージにある画像を著書に新たに紐づけるためのレコード)。

実際、実行後の中間テーブルのレコードの数は2個ですが、ファイルデータのためのテーブルのそれは1個のみ(画像自体は1枚しかアップロードしていない)であることが確認できます。

attach-test-app(dev)> ActiveStorage::Attachment.count
  ActiveStorage::Attachment Count (0.7ms)  SELECT COUNT(*) FROM "active_storage_attachments" /*application='AttachTestApp'*/
=> 2
attach-test-app(dev)> ActiveStorage::Blob.count
  ActiveStorage::Blob Count (0.9ms)  SELECT COUNT(*) FROM "active_storage_blobs" /*application='AttachTestApp'*/
=> 1

同じ画像を使うために再度ファイルをアップロードする必要がないのはよさそうですね。ただ、同じ画像を使い回すこと自体には「どちらか一方で削除・更新が起こったら?」という問題が付いてきそうなので、注意が必要かなとも思いました。

なんでこんな記事を書いたのか

実際にこういうことができないかを考える機会があり、さっと調べたところ日本語の記事がパッと出てこなかったので書いておこうと思ったためです。

Railsリポジトリでは英語でこれについてのやりとりがなされていましたので、大変参考にさせていただきました。またActiveStorageの構造の話は『パーフェクトRuby on Rails』にもしっかり書かれており、大変助かりました。

github.com

以上です。