あかんわ

覚えたことをブログに書くようにすれば多少はやる気が出るかと思ったんです

RailsでPostgreSQLに格納した画像を表示する

PostgreSQLデータベースにbinary型のデータとして画像を格納し、Railsアプリで表示する方法を調べました。

目次

いかにしてその心情に至ったか

HerokuにデプロイするRailsアプリを作成していて、アバター画像をプロフィールに登録する方法を調べたら、PaasであるHerokuには画像を保存する事ができないため、ファイルサーバとしてAnazon S3を使用し、Herokuと連携させるのが一般的な方法であると知りました。
しかしながら、いくらAmazon S3に無料枠があるとは言え、個人的に利用するだけのアプリのたかだか数十kb程度のアバター画像のためにファイルサーバを準備するのも大袈裟で面倒な気がしたので、データベースに直接格納する方法が無いか調べると、PostgreSQLデータベースに画像ファイルをバイナリデータとして格納する方法があると知り試してみました。

データベースに画像を格納する

マイグレーションファイルを生成

マイグレーションファイルを生成して、データベースに画像を格納するためのモデルの準備をします。

モデルを新しく作る場合

画像の格納先としてbinary型のavatarカラムを持つUserモデルをgenerateし、マイグレーションファイルを生成します。

$rails generate model User name:string email:string avatar:binary

今回はbinary型のavatarカラムの他にも、string型のnameカラムとemailカラムをUserモデルに待たせましたので、マイグレーションファイルのchangeメソッドに含まれるcreate_tableブロックにはt.string :namet.string :emailt.bynary :avatarが記述されています。

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.bynary :avatar

      t.timestamps null: false
    end
  end
end
画像用のカラムをモデルに追加する場合

generateしたモデルに後からカラムを追加する場合は、generate migrationで追加するカラムと追加先のテーブルを指定します。

$rails generate migration add_avatar_to_users avatar:bynary

今回はUserモデルのusersテーブルにbinary型のavatarカラムを追加するので、add_avatar_to_usersgenerate migrationし、changeメソッドにadd_column :users, :avatar, :bynaryが含まれるマイグレーションファイルを生成します。

class AddAvatarToUsers < ActiveRecord::Migration
  def change
    add_column :users, :avatar, :bynary
  end
end

DBにマイグレーションを適用

rake db:migrateで、データベースにマイグレーションファイルの内容を適用します。

$rake db:migrate

無事にデータベースにマイグレート出来たかを確認するため、psql raislapp_developmentで開発環境のデータベースに接続し、usersテーブルを見てみます。
idColumnやその他のColumnに加えて、byteaTypeのavatarColumnが作成されていれば一安心して次の作業に進みます。

$psql railsapp_development
psql (9.5.1)
Type "help" for help.

railsapp_development=# \d users
                              Table "public.users"
   Column  |            Type         |                      Modifiers                        
-----------+-------------------------+---------------------------------------------------------
 id        | integer                 | not null default nextval('users_id_seq'::regclass) 
 name      | character varying       | 
 email     | character varying       | 
 avatar    | bytea                   | 


railsapp_development=# 

SQLコマンドでDBに画像を格納

ブラウザから画像をRailsアプリにアップロードするためのフォームとかはまだ作成して無いので、とりあえず、SQLコマンドで画像をデータベースに格納してみます。 SQLコマンドでファイルをPostgreSQLデータベースに格納するためには、PosgreSQLデータベースのデータディレクトリからの絶対パス相対パスをコマンド入力中に記述する必要があるらしいので、画像ファイルをデータディレクト*1の直下に格納しました。

データベースに新しい行を追加して画像を格納する場合

VALUES( ... )で追加先の行(id)と格納する値を指定し、INSERT INTOコマンドでusersテーブルに追加します。

$psql railsapp_development
psql (9.5.1)
Type "help" for help.

railsapp_development=# INSERT INTO users VALUES (1, 'b0npu nomi', 'b0npu@example.com', 'default_avatar.png');
INSERT 0 1
railsapp_development=# 

usersテーブルには、画像を格納するavatarカラムの他にnameemailのカラムを作成していますので、VALUES ( ... )の括弧ので、id, name, mail, avatarの値を記述しています。

画像を追加してデータベースの行を更新する場合

pg_read_binary_file関数を使用し、UPDATEコマンドでusersテーブルに画像ファイルをSETします。

$psql railsapp_development
psql (9.5.1)
Type "help" for help.

railsapp_development=# UPDATE users SET avatar = pg_read_binary_file('default_avatar.png') WHERE id = 1;
UPDATE 1
railsapp_development=# 

データベースに画像ファイルが格納されると、avatarカラムに\xから始まるバイナリ文字列が格納されます。

$psql railsapp_development
psql (9.5.1)
Type "help" for help.

railsapp_development=# SELECT * FROM users;
-[ RECORD 1 ]---+-----------------------------------------------------------------------------------------
id              | 1
name            | b0npu nomi
email           | b0npu@example.com
avatar          | \x89504e470d0a1a0a0000000d4948445200000080000000800806000000c33e61cb000000017352(長いので以下略)

参考記事

Railsアプリに画像を表示する

Controllerにアクションメソッドを追加

バイナリデータの文字列として格納されている画像ファイルをRailsアプリで表示するためには、send_dataを利用すればいいらしいので、UsersコントローラにUserモデルのavatar属性をsend_dataで送るためのアクションメソッドavatar_forを記述します。 ちなみに、Usersコントローラには画像を表示するビューshow.html.erbのためのアクションメソッドshowも記述しています。

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end

  def avatar_for
    @user = User.find(params[:id])
    send_data(@user.avatar)
  end
end

Routesにメンバルーティングを追加

Usersコントローラに記述したアクションメソッドavatar_forを使用するため、ルーティング*2avatar_forへのメンバールーティングを追加したusersへのリソースフルルーティングを記述します。

Rails.application.routes.draw do
  resources :users do
    member do
      get 'avatar_for'
    end
  end

Viewで画像を表示

画像を表示するビューshow.html.erbを作成し、image_tagavatar_forへのルーティングヘルパーavatar_for_user_pathを記述して画像を表示します。

  <%= image_tag(avatar_for_user_path, alt: @user.name, :size => '40x40') %>

参考記事

画像をフォームから登録する

注:調べた限りでは以下の方法でフォームから画像をアップロードできるらしいのですが、私の環境では頻繁にアップロードに失敗し*3正しく表示できない場合があるため、原因を調査中*4です。

Viewにフォームを表示する

画像をアップロードするためのビューnew.html.erbform_forでフォーム作成し、画像ファイルのためのfile_fieldを記述します。

<%= form_for(@users) do |f| %>

  <%= f.file_field :avatar %>

  <%= f.submit "Submit" %>
<% end %>

Controllerにメソッドを追加する

アクションメソッドcreateでフォームで入力された値をparamsから取得し、Userモデルに格納します。 また、アップロードされた画像やフォームから入力された値をセキュアに取得するために、privateメソッドのuser_paramsUserモデルが受け取れるパラメータを制限します。

class UsersController < ApplicationController
 
  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def avatar_for
    @user = User.find(params[:id])
    send_data(@user.avatar)
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :avatar)
    end
end

参考記事

開発環境

*1:私の場合は/usr/local/var/postgresでした

*2:rootと紛らわしいのでroutesはルーティングと呼ぶらしい

*3:54bytesのみデータベースに格納される

*4:原因がわかれば記事の内容を修正するか追記します