write ahead log

ロールフォワード用

Rails5でDeviseを使って認証機能を作る

えらく多機能なので試しながらメモる.

とりあえず使う

とりあえず使ってみる.

認証をするからにはサンプルが欲しいので用意しよう.

サンプルプロジェクトを考える

メモを管理するサービスということにしよう.

コントローラは

  • StaticPages(トップページ管理)
  • Notes(メモの管理)

の2つを用意する.

機能は認証とメモ管理のみ.

プロジェクトを作る

$ rails new notes

Deviseのインストール

gem 'devise'

をGemfileへ書いて

$ bundle install

とりあえずトップページとメモ機能を作る

とりあえずなのでscaffoldで.

$ rails g controller StaticPages
$ rails g scaffold note subject:string body:string
$ rails db:migrate

StaticPagesへはconfig/routes.rbを編集してルートも追加.

Rails.application.routes.draw do
   root to: 'static_pages#index'  #追加
   resources :notes
end

雑にトップページも作っておく.

<!-- app/views/static_pages/index.html.erb  -->
<h1>Top</h1>

ここまででメモ機能は動作する.

Deviseのセットアップ

ここからDeviseを使っていく.

Deviseを使うにはまず以下のコマンドでセットアップする.

$ rails g devise:install

そうすると丁寧に以下のメッセージを出してくれる.

===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

1はメーラの設定, Deviseは認証やパスワードリマインダーにメールを使えるのでそのため. 2はルートページURLの設定, これは認証時の遷移先で必要になるだろう. 3はエラーメッセージなどを表示するタグをviewに入れてくれということ. 4はカスタムのviewが欲しい時に使えという事.

Deviseはデフォルトでviewを用意するのでカスタムする時には4のコマンドでまずはviewのファイルを生成してやらないとならない.
(viewをカスタムしない場面もあまりなさそうなものだけど)

2のルートページの設定は上述で行ったので, 1, 3を行います.

カスタムviewは後回し.

1. メーラの設定

# config/environments/development.rb
# 追加
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

3.エラーメッセージタグのviewへの追加

app/views/layout/application.html.erb

<!-- ここから -->
<div class="notice">
  <%= notice %>
</div>
<div class="alert">
  <%= alert %>
</div>
<!-- ここまで -->
<%= yield %>

認証に使うモデルの作成

認証するためのアカウント情報を保持するためのモデルを作成します.

作成にはDeviseが用意しているコマンドを使います.

$ rails g devise User

作成されたファイルを少し覗いてみます.

# app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

deviseの利用するモジュールがわらわら書かれています(後述)

マイグレーションファイルも見てみます.

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

すごいわらわら出来てます...

コメントアウトされているものをアンコメントするとConfirmableなどのモジュールも利用できそうな気配.

とりあえずデフォルトの認証機能さえ使えれば良いのでマイグレーション.

$ rails db:migrate

サインインとサインアウトのリンクをつける

サインインとサインアウトの画面へのリンクをトップページに作りたいので, routesを確認してみます.

imai@IMAI-PC:~/lab/projects/notes$ rails routes
                  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
            user_session POST   /users/sign_in(.:format)       devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy
       new_user_password GET    /users/password/new(.:format)  devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
           user_password PATCH  /users/password(.:format)      devise/passwords#update
                         PUT    /users/password(.:format)      devise/passwords#update
                         POST   /users/password(.:format)      devise/passwords#create
cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit
       user_registration PATCH  /users(.:format)               devise/registrations#update
                         PUT    /users(.:format)               devise/registrations#update
                         DELETE /users(.:format)               devise/registrations#destroy
                         POST   /users(.:format)               devise/registrations#create
                         [中略]

とりあえずusersの部分だけ見てもたくさん生成されています.

とはいえリンクに使えそうなものは以下でしょう.

  • new_user_registration
  • new_user_session
  • destroy_user_session

これらを使ってリンクを用意します.

トップページからのリンクを作る

ユーザ登録(sign up)とログイン(sign in)は必要でしょう.

トップページ(app/views/static_pages/index.html.erb)にリンクを加えます.

<h1>Top</h1>

<%= link_to 'sign in', new_user_session_path %>

<%= link_to 'sign up', new_user_registration_path %>

ログアウトのリンクを作る

ログインしたらログアウトのリンクも欲しいのでレイアウト(app/views/layouts/application.html.erb)へ加えます.

  <body>
  <!-- ここから -->
    <div class="session-control-box">
      <% if signed_in? %>
        <%= link_to 'sign out', destroy_user_session_path, method: :delete %>
      <% end %>
    </div>
  <!-- ここまで -->

signed_in?はdeviseが用意してくれるヘルパーメソッドです.

その名の通りログイン中だったらtrueを返します.

ログイン後のリダイレクトを設定する

ログイン後にはメモの一覧へ移動してほしいのでヘルパーメソッドの利用を追加します.

app/controllers/application_controller.rbに

  • after_sign_in_path_for
  • after_sign_out_path_for

の利用を追加します.

名前の通りこのメソッドの戻り値のパスへリダイレクトされます.

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def after_sign_in_path_for(resource)
    notes_path
  end

  def after_sign_out_path_for(resource)
    root_path
  end
end

これでログイン後とログアウト後の挙動を変更できます.

ログイン前はアクセスできないようにする

リダイレクトされてもログイン前にメモ管理のエリアにアクセスできてしまっては本末転倒なので, 認証機能を追加します.

class NotesController < ApplicationController
  before_action :authenticate_user!  #追加
  before_action :set_note, only: [:show, :edit, :update, :destroy]

authenticate_[モデル名]メソッドがdeviseで用意されているのでこれを使うだけです.

未認証の場合はログイン画面へリダイレクトされます.

ログインした人間のノートだけ参照できるようにする

このままではログインさえすれば誰のメモでも見れてしまうので, メモに所有者情報を持たせてログインしているユーザのメモだけが見れるように変更します.

まずはmigrationでnoteへuserへの外部キーを追加します.

$ rails g migration AddUserToNotes user:references
$ rails db:migrate

モデルへアソシエーションもつけておきます.

ユーザ側.

# app/models/user.rb
class User < ApplicationRecord
  has_many :notes

メモ側.

# app/models/note.rb
class Note < ApplicationRecord
  belongs_to :user
end

次にscaffoldで作ったコントローラをちょっと変更.

  def index
    #[変更]
    #@notes = Note.all
    @notes = current_user.notes
  end
    [中略]

  def new
    #[変更]
    #@note = Note.new
    @note = current_user.notes.build
  end
    [中略]

  def create
    #[変更]
    #@note = Note.new(note_params)
    @note = current_user.notes.build(note_params)
    [中略]

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_note
      #[変更]
      #@note = Note.find(params[:id])
      @note = current_user.notes.find_by(id: params[:id])
      redirect_to notes_url if @note.nil?
    end

current_[モデル名]ヘルパーメソッドがdeviseによって用意されるのでログイン中のユーザを取得するのは簡単です.

ここまでやると普通にアプリケーションっぽくなります.

i18nに対応する

deviseそのままでは英語メッセージが表示されるのでI18N対応してやる.

なんとそのままの名前のgemがあった.

tigrish/devise-i18n - GitHub

余談だがこの方, kaminariとか色々なgemのi18n対応をやってるみたいで地味にすごい.

Gemfileに追記して更新する.

# 追記
gem 'devise-i18n'
$ bundle install

デフォルトロケールを設定していない場合はこちらも設定.

# config/application.rb

# 以下を記述
config.i18n.default_locale = :ja

これだけでメッセージが日本語化される.ありがたい.

公式にも一応色々記載があるので込み入ったことをしたい場合は読むとよいかも.

専用のwikiもある.

ViewとControllerをカスタマイズする

ちょっと試すぐらいならデフォルトでも良いけど, 流石にこのままのviewでは実用に耐えない.

モデルもちょっといじって, 登録時にユーザ名も登録するようにしてみる.

Userモデルの変更

migrationでユーザ名を追加する.

$ rails g migration AddNameToUsers name:string
$ rails db:migrate

deviseのviewの用意

deviseでviewをカスタマイズするにはまず以下のコマンドでビューを生成する.

$ rails g devise:views users(モデル名)

するとdeviseで使うビューが用意される.

これをカスタマイズしていく.

deviseのcontrollerの用意

controllerもviewと同様で, 下記のコマンドで生成する.

$ rails g devise:controllers users

親切にroutes.rbを変更しろと教えてくれる.

      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb
===============================================================================

Some setup you must do manually if you haven't yet:

  Ensure you have overridden routes for generated controllers in your routes.rb.
  For example:

    Rails.application.routes.draw do
      devise_for :users, controllers: {
        sessions: 'users/sessions'
      }
    end

===============================================================================

生成されたコントローラがapp/controllers/users/に用意されるが, これらは全てコメントアウトされている.

なのでこれをベースに使うものをアンコメントしていかないとならない.

各コントローラと機能の対応は多分以下の感じじゃないかな.

コントローラ名 機能
confirmations_controller.rb 登録後のメールでの確認
passwords_controller.rb パスワードリマインダー
registrations_controller.rb ユーザ登録
sessions_controller.rb セッション(ログイン/ログアウト)
omni_auth_callbacks_controller.rb omni_authによるOAuth制御のコールバック
unlocks_controller.rb ログインに一定回数失敗したらアカウントロックするアレ

とりあえず

  • ユーザ登録
  • セッション管理

だけは使いたいので以下の2つのファイルの中身をアンコメントする.

  • registrations_controller
  • sessions_controller

routesを変更

コンソールで出た指示通りに.

  #devise_for :users
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }

ここまででコントローラとビューをカスタムする前と同じ挙動になる.

viewの変更

app/views/users/registrations/new.html.erbを変更して名前欄を追加する.

<!-- ここから -->
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>
<!-- ここまで -->

  <div class="field">

ついでにログイン中のユーザ名を見えるようにしておく.

<!-- app/views/layout/application.html.erbへ追加 -->
        <p><%= current_user.name %></p>

controllerの変更

strong parametersを追加するだけですが.

  def configure_sign_up_params
    #devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end

公式にstrong parametersに関する記載があったので, 困ったら読めば良いかな.

localeファイルの編集

普通にjaのymlを用意してActiveRecordの部分に書いた.

# config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: ユーザ名

これだけでうまく動作します.素敵ですね.

testをしたい

公式にも章が組まれている.

ControllerのテストもIntegrationテストもどちらでもIntegrationHelpersをincludeすると

  • sign_in
  • sign_out

の両メソッドが利用できるので, これで困ることはほぼないと思う.

# test/controllers/note_controller_test.rb
class NotesControllerTest < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers

  setup do
    sign_in users(:yamada)
  end

ちょっと調べる必要があったのがFixturesの書き方で, 以下の様に書いた.

# test/fixtures/users.yml
yamada:
    email: yamada@example.com
    encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>

モジュールの一覧

deviseには以下の10個のモジュールがあり, それぞれ用途に応じて使い分ける事ができる.

モジュール名 機能 デフォルト
database_authenticatable DBに保存するパスワードの暗号化 有効
registerable サインアップ処理 有効
recoverable パスワードリセット機能 有効
rememberable クッキーにログイン情報を保持する 有効
trackable サインイン回数・時刻・IPアドレスを保存 有効
validatable メールアドレスとパスワードでのバリデーション 有効
confirmable メール送信による登録確認
lockable 一定回数ログインに失敗した際のアカウントロック
timeoutable 一定時間でセッションを削除する
omniauthable OmniAuthのサポート

ちょっとすぐに全部は試せそうにない.心折れた.

とはいえこれだけでかなり使えそうです.

参考

Documentation for plataformatec/devise (master)

letter_opener_webを使ってメール送信をプレビューする

letter_opener_webはRailsで開発中のメールをブラウザで見れる便利なgem.

WSL環境やvagrant上での開発などではブラウザでメールが見られるのは非常に便利.

インストール

Gemfileを編集.

開発時のみ入れるように.

gem 'letter_opener_web', :group => :development

インストール

$ bundle install

設定

config/environments/development.rbを編集.

  # 以下2行を追加
  config.action_mailer.default_url_options = { host: 'localhost:3000' }
  config.action_mailer.delivery_method = :letter_opener_web

config/routes.rbを編集.

  # 以下3行を追加
  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

これで

localhost:3000/letter_opener

へブラウザでアクセスするとブラウザ上でメールが確認できます.

試してみる

mailerとcontrollerを適当に作って試してみる.

mailerを作る

$ rails g mailer HelloMailer

挨拶メールを送るだけ.

class HelloMailer < ApplicationMailer
  default from: "foo@example.com"

  def greeting
    to = "bar@example.com"
    subject = "greeting!"

    mail(subject: subject, to: to) do |format| 
      format.text
    end
  end
end

Controllerを作る

$ rails g controller send_mails

indexへアクセスしたらメールを送るようにする.

class SendMailsController < ApplicationController

  def index
    HelloMailer.greeting.deliver
  end

end

config/routesも追加.

  get '/send_mails', to: 'send_mails#index'

ビューを作る

ないと怒るので.

<!-- app/views/send_mails/index.html.erb -->
sent

ここまでで

localhost:3000/send_mails

へアクセスした後に

localhost:3000/letter_opener

へアクセスするとメールが見れた.

これは便利.

Rails5とCarrierWaveを使って画像アップロード機能を作る

以前はgemなしで実装したので今度はCarrierWaveを使ってみる.

あとはせっかくなので画像のリサイズなんかも試す.

インストール

CarrierWaveで画像のリサイズなんかをやるためにはRMagickやMiniMagickが必要になります.

で, これらはImageMagickのラッパーなのでやっぱりImageMagickが必要になります.

Ubuntu(WSL)の場合.

$ sudo apt install imagemagick
$ sudo apt install libmagick++-dev

サンプルプロジェクトを作成

$ rails new carrier_wave_sample

Gemfileを編集

今回はCarrierWave推奨のmini_magickを使います.

gem 'carrierwave'
gem 'mini_magick'

bundleでインストール.

$ bundle install

とりあえず足場を作る

前回同様, scaffoldで.

とりあえず掲示板っぽいものを作成.

メッセージ毎に画像を1つ登録できるようにする.

$ rails g scaffold message message:string image_path:string
$ rails db:migrate

uploaderを作る

uploaderはcarrierwaveで画像をアップロードする際の設定を行うイメージです.

CarrierWaveではRailsのgenerateコマンドに追加が行われて,

$ rails g uploader [アップローダー名]

が利用できるようになりますのでこれで作成します.

以下のようにしました.

$ rails g uploader MessageImage

アップローダーはapp/uploadersフォルダが新しく作られてそこへまとめられます.

今回生成されたものを見てみると以下のようになっています.

class MessageImageUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  # include CarrierWave::MiniMagick

  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
  [中略]

store_dirメソッドはファイルの保存先かな?とかある程度想像がつくのでわかりやすそうではあります.

Uploaderとモデルを関連付ける

モデルに画像のパスを保存するので, 今回作成したUploaderとモデルを関連付ける必要があります.

class Message < ApplicationRecord
  mount_uploader :image_path, MessageImageUploader
end

viewの編集

追加側

scaffoldで作ったビュー(_form.html.erb)のファイルパスの部分をfile_fieldに変更.

  <div class="field">
    <%= form.label :image_path %>
    <%= form.file_field :image_path, id: :message_image_path %>
  </div>

表示側

表示側(show.html.erb)も変更.

<p>
  <strong>Image path:</strong>
  <%= image_tag(@message.image_path_url) %>
</p>

ここまででアップロードと表示ができるようになります.

簡単.

アップロードした画像はpublic/の下にstore_dirメソッド定義通りのパスで作成されていました.

画像をリサイズする

今回は画像の変換にMiniMagickを使うのでコメントを外しておきます.

app/uploader/message_image_uploader.rbを変更.

class MessageImageUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  include CarrierWave::MiniMagick

  process resize_to_fit: [200, 200]

resize_to_fitは縦横比を維持したままリサイズするメソッドです.

上記では200×200のサイズになるようにアップロード時に変換するようにしています.

Module: CarrierWave::MiniMagick

リファレンスを見ると他にもいくつかメソッドがありますがよくわからんなぁと思っていたところ詳しく記載してくださっている方がいらっしゃったのでリンク張っときます.

麺処 まつば - CarrierWave + RMagick 画像のリサイズをまとめてみました

サムネイルを作る

アップロードした画像とは別にサムネイルを作る事もできます.

元々コメントアウトで記述されているのでアンコメントするだけで使えます.

  # 以下
  # Create different versions of your uploaded files:
  version :thumb do
    process resize_to_fit: [50, 50]
  end

表示するには以下の様にビューを変更します.

app/views/messages/show.html.erbを変更.

<!-- 以下を追加 -->
<p>
  <%= image_tag(@message.image_path_url(:thumb)) %>
</p>

[モデル名][画像パス保存の属性名]_urlのメソッドには引数にversionのシンボルが渡せます.

今回は:thumbを追加したので引数にこれを渡す事で作成したサムネイルの画像パスを取得できます.

画像をバリデーションする

拡張子でのバリデーション

こちらもコメントアウトをアンコメントするだけで使えます.試しにpngを禁止してみました.

バリデーションで引っかかるので便利です.

app/uploaders/message_image_uploader.rbを編集.

  def extension_whitelist
    %w(jpg jpeg gif)
  end

画像サイズでのバリデーション

画像ファイルの上限サイズを指定できます.

app/uploaders/message_image_uploader.rbを編集.

  def size_range
    1..5.megabytes
  end

I18Nの対応

エラーのバリデーションメッセージを日本語化したいところです.

公式に記載がありました.

これだけできれば色々できそうです.

Rails5で検索できるセレクトボックスを作りたい

select2というjquery-pluginがあってこれを使うと良い感じになりそう.

(jQuery捨てるとか私の様な三流には無理そうです.ほとんどこれで十分だし...)

これをRailsから利用するgemがあるのでこれを利用します.

適当なサンプルを用意する

都道府県を選ぶ画面を用意するために適当なプロジェクトとコントローラ・ビューを用意します.

$ rails new select2_sample
$ rails g controller Prefectures

config/routesも編集. 以下を追加.

  resources :prefectures

gemを追加する

Gemfileに以下を追記.

gem 'jquery-rails'
gem 'select2-rails'

いつも通り更新.

$ bundle install

適当なビューを作る

雑に都道府県をべた張り.

<!-- app/views/prefectures/index.html.erb -->

<!-- railsのヘルパーで生成 -->
<%= select_tag 'searchDropdownBox', options_for_select(["都道府県", "北海道", "青森", "岩手"]), class: "searchable" %>

<!-- 普通のHTMLタグ -->
<select>
    <option>北海道</option>
    <option>青森</option>
    <option>岩手</option>
    <option>宮城</option>
    <option>秋田</option>
    <option>山形</option>
    <option>福島</option>
    <option>茨城</option>
    <option>栃木</option>
    <option>群馬</option>
    <option>埼玉</option>
    <option>千葉</option>
    <option>東京</option>
    <option>神奈川</option>
    <option>新潟</option>
    <option>富山</option>
    [中略]
</select>

この時点で動作確認すると普通にセレクトボックスが表示されます.

Railsアプリケーションへのselect2の導入

bundleでの導入以外に以下が必要です.

  • application.jsへjqueryのrequireを追加
  • application.jsへselect2のrequireを追加
  • application.cssへselect2のrequireを追加

具体的には以下になりました.

application.js

  [中略]
//= require rails-ujs
//= require turbolinks
//= require jquery
//= require select2
//= require_tree .

application.css

  [中略]
 *= require select2
 *= require_tree .
 *= require_self

都道府県選択へselect2を導入する

bundleで導入済みなのでここからはRailsで使えるようにしていきます.

導入には以下が必要です.

  • select2を使うcoffeeを変更(この場合はprefectures.coffeeを変更)

こちらの具体例は以下.

prefectures.coffee

$ ->
    $('.searchable').select2({
        width: 200,
    })

bootstrapと一緒に使う

bootstrapと共に利用するには, まずbootstrapを導入してapplication.scssへ以下のように変更.

 [中略]
 *
 *= require select2
 */
@import "bootstrap-sprockets";
@import "bootstrap";

日本語化

application.jsに以下を追記.

//= require select2
//= require select2_locale_ja   # これ
//= require_tree .

jsで設定する際にパラメータへlanguageを追加.

$( ".searchable" ).select2({
    language: "ja"
});

Rails5にはmigrationにカラムコメントを付けられるらしい

便利そう.

使ってみた.

サンプルプロジェクトを作る

$ rails new migration_comment --database=mysql
$ rails g model user name:string email:string

config/database.ymlは適宜設定.

migrationファイルを編集する

ハッシュでcommentを渡せばよい様です.

class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.string :name, comment: "ユーザ名"
      t.string :email, comment: "メールアドレス"

      t.timestamps
    end
  end
end

migration結果を見てみる

mysqlコマンドで見てみた. ちゃんとコメントが発行されている.

mysql> show create table users\G
*************************** 1. row ***************************
       Table: users
Create Table: CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL COMMENT 'ユーザ名',
  `email` varchar(255) DEFAULT NULL COMMENT 'メールアドレス',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

残念なのは

sqlite3で確認できなかった.

(そもそもsqlite3ってコメントあったっけ?)

MySQLとpostgreSQL限定なのだろうか.

Railsでファイルアップロードを実装する

入門シリーズが続いている.

CarrierWave使うサンプル多いので, 使わないで実装してみる.

Railsは5.1.6

サンプルの内容

掲示板的に

  • メッセージ
  • 画像

のCRUDができるものにする.

とりあえず足場を作る

CRUD実装自体は目的ではないのでScaffoldで.

$ rails g scaffold message message:string
$ rails db:migrate

とりあえずメッセージ登録ができるようになる.

画像をDBへ保存するか, ファイル保存してパスを保存するか

一般的にはパス保存といわれている気がするし, 迷ったけど今回はDBへ保存する実装にした.

理由は

  • バックアップとリカバリが楽
  • アクセス制御も楽

特にアクセス制御についてはパスを保存の場合は, どうやってうまく制御するのかがわからなかった.

publicに入れてパスを保存というのが一般的なのかな?という気もするけど, エンタープライズだと画像もアクセス制御に気を付けないとならない.

UUIDみたいな当てようもないものをファイル名にしてパスを保存したりするのだろうか?

この辺よくわからない.知りたいなぁ.

まぁ, Oracleもポジショントークではあるもののこんな事言ってますし, DB保存もありではないかと.

データベースに画像を格納するメリット

画像保存用のモデルを追加する

imageモデルを作成し, 上述で作成したmessageモデルと関連させることにする.

$ rails g model image message:references filename:string data:binary
$ rails db:migrate

アソシエーションも追加.

# app/models/message.rb
class Message < ApplicationRecord
  has_one :images
end
class Image < ApplicationRecord
  belongs_to :message
end

アップロード機能を作る

下ごしらえが済んだので実装する.

view

_form.html.erbにfile_fieldを加えてやる.

1対1でアソシエーション張ったので一応fields_for使った.

  <div class="field">
    <%= form.label :message %>
    <%= form.text_field :message, id: :message_message %>
  </div>

  <!-- アップロード用に追加 -->
  <%= form.fields_for(message.image) do |f| %>
    <div class="field">
      <%= f.label :image %>
      <%= f.file_field :data, id: :image_data %>
    </div>
  <% end %>

controller

登録フォームを呼び出すときにイメージモデルのインスタンスを用意しておく.

  # GET /messages/new
  def new
    @message = Message.new
    @message.build_image  # 追加
  end

追加時にファイルを保存するように変更.

まずbuild_imageでImageモデルのインスタンスを用意しておく.

ストロングパラメータも新しく用意した.

で, データを取得してDBへ入れたいのだけど, image_params[:data]で取得できるのはActionDispatch::Http::UploadedFileのインスタンスらしい.

アップロード画像は付加情報も含めインスタンスで表現される.(実際のファイルは仮ファイルで別の場所へ保存)

ファイルのデータはこのインスタンスからreadメソッドで読み込める.

ファイル名はoriginal_filenameメソッドで取得できる.

リファレンス読むとcontent_typeも取得できるので, このサンプルのImageモデルに拡張子の項目を追加しなかったことを若干後悔.

  # POST /messages
  # POST /messages.json
  def create
    @message = Message.new(message_params)
    # ここから
    @image = @message.build_image
    @image.data = image_params[:image][:data].read
    @image.filename = image_params[:image][:data].original_filename
    # ここまで追加

    respond_to do |format|
      if @message.save
        format.html { redirect_to @message, notice: 'Message was successfully created.' }
        format.json { render :show, status: :created, location: @message }
      else
        format.html { render :new }
        format.json { render json: @message.errors, status: :unprocessable_entity }
      end
    end
  end

  private

    # Never trust parameters from the scary internet, only allow the white list through.
    def message_params
      params.require(:message).permit(:message)
    end

    # 追加したパラメータ取得メソッド
    def image_params
      params.require(:message).permit(image: [:data])
    end

ここまででアップロードは動作した.

若干ごちゃつく.

表示する

せっかくなら表示もさせたいので.

routeの追加

config/routes.rbへアップロードした画像を得るimageというルートを追加する.

  resources :messages do
    member do
      get 'image'
    end
  end

viewの変更

app/views/messages/show.html.erbへ表示用のタグを追加.

<p>
  <strong>Message:</strong>
  <%= @message.message %>
</p>

<%= image_tag(image_message_path(@message), alt: @message.image.filename) %>

controllerの変更

app/controllers/messages_controller.rbへ画像取得のアクションを追加する.

  # scaffoldで生成したサンプルなのでbefore_actionにimageも追加してやる
  before_action :set_message, only: [:show, :edit, :update, :destroy, :image]

  [中略]

  # GET /messages/1/image
  def image
    send_data @message.image.data, type: 'image/jpeg'
  end

ダウンロードリンクをつける

ついでに試した.

view

app/views/messages/show.html.erb

<%= image_tag(image_message_path(@message), alt: @message.image.filename) %>

<!-- 追加 -->
<%= link_to 'download', image_message_path(@message) %>

controller

app/controllers/messages_controller.rbのimageアクションを変更.

ファイル名をURLエンコードしてsend_dataへ渡してやるだけ.

  # GET /messages/1/image
  def image
    filename = ERB::Util.url_encode(@message.image.filename)
    send_data @message.image.data, type: 'image/jpeg', filename: filename
  end