画像をバイナリデータとしてDB保存する

画像をバイナリデータとしてDB保存するの説明画像
目次

お金かかるからストレージサービス使いたくない

個人でシステム開発をしている人の中で「S3とかにお金払いたくない(ケチ)」ということ思ったことがある方は私だけではないと思います。
※私がケチすぎるだけ...?

私はPaasサービスとしてRender.comを利用していますが、public/フォルダにアップロードした画像が、デプロイや再起動で消えてしまうということがあるらしく、画像をpublic/uploadsにあげたものの、すべて消えてしまうということがありました。

この記事では、そんな状況の解決策の一つになる「画像データをデータベースに保存する」手法について書いています。

また、実際に使ってみて感じた注意点についても触れていきます。

コトの背景

私の本番環境は上述の通り Render.com です。

Renderは非常に便利なPaaSですが、Webサービスのファイルシステムは「エフェメラル(非永続)」という仕様になっています。

つまり…

再デプロイや再起動のタイミングで、public フォルダに保存したファイルが削除される。

という現象が起きます。

実際に公式ドキュメントにもこう書かれています👇

“Render services have an ephemeral filesystem. Files written to the filesystem disappear on redeploy or restart.”

Render Docs

そのため、画像アップロードをpublic/uploadsに保存するだけでは、本番運用では「デプロイしたら消えていた」という悲劇が起きてしまいます。

最初に、public/uploadsに画像を保存する方法を採用していた私は、上記現象によってすべての画像ファイルが消えてしまいました。

手作業で頑張って大量の画像ファイルあげたのに、すべて消えていた時には、さすがにモチベ下がりましたね…。

データベースに画像を保存するという発想

「画像をファイルとして保存できないなら、いっそデータベースに直接入れてしまおう」というのが今回の発想です。

Railsでは、BLOB(バイナリラージオブジェクト)型のカラムを用意すれば、画像データそのものを保存できます。
この方法は昔から存在する手法です。システム開発において、直感的にはあまりよさそうな方法ではないですが「場合によっては理にかなっている」場面もあります。

Render のようにファイルが揮発する環境や、外部ストレージのコストを避けたい場合 かつ 小規模アプリや個人開発では十分実用的です。

実装方法

Railsのマイグレーション例は以下です。

class CreateUploadedImages < ActiveRecord::Migration[8.0]
  def change
    create_table :uploaded_images do |t|
      t.string :filename
      t.string :content_type
      t.binary :image_data, limit: 10.megabytes
      t.integer :byte_size

      t.timestamps
    end

    add_index :uploaded_images, :filename
  end
end

image_data カラムに t.binary を指定し、10MBまでの画像を格納できるようにしています。
content_typebyte_size を一緒に保存しておくことで、表示時にレスポンスヘッダを正しく設定できます。

保存処理は、以下のように File.binread を使って画像を読み込むだけです。

def create
  file = params[:image]
  uploaded = UploadedImage.new(
    filename: file.original_filename,
    content_type: file.content_type,
    byte_size: file.size,
    image_data: file.read
  )
  uploaded.save!
end

表示時は send_data を使ってブラウザに返します。

def show
  image = UploadedImage.find(params[:id])
  send_data image.image_data, type: image.content_type, disposition: "inline"
end

これで、DBに保存された画像をそのままブラウザで表示できます。

メリットとデメリット

メリット

  • Render のような揮発ストレージでも問題なし
  • DBのバックアップだけで復元可能
  • Active Record のトランザクションに統合できる
  • S3やCloudflare R2などの外部サービス契約が不要

小さなアプリであれば、これだけで十分運用できます。
特に、個人開発や非商用プロジェクトでは「お金をかけずに完結する」という点が魅力です。

デメリット

  • データベースが肥大化する
  • バックアップ・リストアが重くなる
  • DBキャッシュやI/Oが圧迫される
  • 配信パフォーマンスが低い(CDNを挟めない)

要するに、スケールしないのが最大の弱点です。
100枚・1000枚レベルなら問題ありませんが、数万枚を超えると確実に厳しくなります。

実際に使ってみて感じたこと

実際にこの方法を導入してみると、想像以上に「小規模開発では快適」でした。
Render の再デプロイでも画像が消えないし、外部ストレージの課金も不要。
ちょっとしたCMSや掲示板、ポートフォリオサイトなどにはぴったりです。

ただし、SELECTでbinaryカラムを安易に取得すると、サーバーが落ちることがあるので注意が必要です。
また、そのほかにもDBの肥大化やバックアップ時間の増加など、将来的に運用コストが上がる可能性があります。

個人ブログだからこそできる力技で、若干その場しのぎ感のある解決策ではありましたが、将来S3に移し替えることもできるので、現状はこれで良しとしています。

まとめ:目的とスケール感で使い分けよう

「画像をDBに保存」は一見するとバッドノウハウのように思われがちですが、
Render のような非永続環境+コスト重視の個人開発では、むしろ“最適解”になりえると思っています。

  • 少量の画像を安全に扱いたい
  • 外部ストレージを契約したくない
  • デプロイ時にファイルが消える環境を使っている

そんな方には、ぜひ検討してみてはいかがでしょうか?