write ahead log

ロールフォワード用

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

RailsのI18n色々

Rails5触ってみようとチュートリアル読んでからRailsの入門メモが増えてる...

とりあえずサンプルプロジェクトを用意する

scaffoldでタスクリストのページを用意します.

$ rails new i18n_sample
$ rails g scaffold Task title:string done:boolean
$ rails db:migrate

バリデーションメッセージも見たいので適当にチェックをモデルへ追加します.

# app/models/task.rb
class Task < ApplicationRecord
  validates :title, presence: true
end

rails-i18nのgemを導入する

バリデーションメッセージなどを国際化したいのでgemを導入する.

Gemfileに以下を追記.

gem 'rails-i18n'

忘れぬよう.

$ bundle install

I18nのデフォルトロケールを日本語にする

config/application.rbにデフォルトロケールを設定してやります.

# config/application.rb
...
module I18nSample
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.1

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.
    
    # locale setting                   #<== 追加
    config.i18n.default_locale = :ja   #<== 追加
  end
...
end

この時点でサーバを再起動してやると基本的なメッセージは日本語化されています.

素晴らしいですね.

日本語の言語ファイルを作成する

モデルの属性名などは当然まだ日本語化されていないので, 言語ファイルを作成します.

config/locale/ja.ymlを作成します.

$ touch config/locale/ja.yml

基本と言語ファイルの構成

言語ファイルの構成はざっくりこんな感じのようです.

規約的に以下のようになっているだけなので, 共通で使うものを言語名(jaとか)の下の階層に差し込んで色々な箇所から使ったりも普通にできます.

[言語名]:
    activerecord:
        models:
            [モデル名]:
            [モデル名]:
            ...
        attributes:
            [モデル名]:
                [属性名]: [対応リテラル]
                [属性名]: [対応リテラル]
            [モデル名]:
                [属性名]: [対応リテラル]
                [属性名]: [対応リテラル]
            ...
    [コントローラ名]:
        [アクション名]:
            [リテラル]: [対応リテラル]
            [リテラル]: [対応リテラル]
            ...

プログラムから利用する際にはtというメソッドを使います.

このメソッドの引数は上記yamlの構成をパスの様に指定して使う.

t('activerecord.models.[モデル名]')

みたいな感じ.

モデルの言語を登録する

scaffoldの内容に合わせて以下にしてみました.

ja:
    activerecord:
        models:
            task: タスク
        attributes:
            task:
                title: タスク
                done: 完了

これだけで, form_with+labelを使って作られている部分も変わってたりしてて良い感じです.

バリデーションメッセージの属性名もちゃんと日本語化されます.

モデルの名前と属性名を得る

上述の規約通りに言語ファイルを作成しておくと, ActiveRecordのメソッドで簡単に国際化されたリテラルを取得する事ができます.

例えばタスクのモデル名は以下の様に得られます.

   Task.model_name.human

属性名も以下の様に簡単に. 引数が属性名ですね.

Task.human_attribute_name(:title)

最初, humanって?と思いましたが, 人間が読めるという意味なんでしょうね.

ネストしたモデルの場合

この例ではないけれど, accept_nested_attributes_forを利用してネストしたモデルを受け入れる場合には一工夫必要になる.

例えば今回利用している例でサブタスクという概念があった場合, Modelの定義では以下の様になると思う.

class Task < ApplicationRecord
  has_many :sub_tasks
end

class SubTask < ApplicationRecord
  belongs_to :task
end

こういう場合の言語ファイルは以下になります.

ja:
    activerecord:
        models:
            task: タスク
        attributes:
            task:
                title: タスク
                done: 完了
            task/posts:
                title: サブタスク名
                done: 完了

親モデル/子モデル(複数形)です.

ビューのリテラルを多言語化する

例として * activerecordのリテラルでビューを表示 * ビュー用に追加したリテラルを利用 をやってみる.

# config/locale/ja.yml
ja:
    activerecord:
        models:
            task: タスク
        attributes:
            task:
                title: タスク
                done: 完了
    tasks:
        index:
            are_you_sure: 本当に削除しますか?

activerecordに設定したリテラルを利用する部分.

...
    <tr>
      <th><%= t('activerecord.attributes.task.title') %></th>
      <th><%= t('activerecord.attributes.task.done') %></th>
      <th colspan="3"></th>
...

追加したビュー用のリテラルを利用する部分.

     <td><%= link_to 'Destroy', task, method: :delete, data: { confirm: t('.are_you_sure') } %></td>

規約に従っておけば

t('tasks.index.are_you_sure')

のように冗長な形ではなく

t('.are_you_sure')

でよくなるので便利.

日付と時刻の国際化

国際化で困るのは単に言語だけではなく日時フォーマットもだけど, ここら辺のサポートが行き届いているのは良いなぁと.

yamlに以下のフォーマットで書いておくと日付と時間がtメソッドでフォーマットされる.

# config/locale/ja.yml
ja:
    date:
        formats:
            default: "%Y/%m/%d"
            long: "%Y年%m月%d日(%a)"
            short: "%m/%d"
    time:
        formats:
            default: "%Y/%m/%d %H:%M:%S"
            long: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
            short: "%y/%m/%d %H:%M"

使う時はこんな感じ. よしなにしてくれてよい.

<%= l(task.created_at) %>
<%= l(task.created_at, format: :short) %>

ざっくり見てきたけど, 式展開など多様な機能があるようなので, これ以上は一度Railsガイドを読んだ方が良いのだろうなぁ.

Rails5でwill_paginateを使う

ただの初心者メモ.

とりあえずプロジェクトを作る

$ rails new pagination

Gemを導入する

Gemfileに以下を追記.

gem 'will_paginate'

いつも通りbundleで更新する.

$ bundle install

サンプルページの用意

とりあえずScaffoldで準備しておく

$ rails g scaffold user name:string email:string
$ rails db:migrate

テストデータを用意

fixtureで適当に量産する.

#test/fixtures/users.yml

<% 1000.times do |n| %>
user_<%= n %>:
  name: <%= "user_#{n}" %>
  email: <%= "user_#{n}@example.com" %>
<% end %>
$rails db:fixtures:load

コントローラを変更

paginateメソッド経由でインスタンスを得るようにしてやる.

何ページ目にいるかはpageパラメータに入っている.

  def index
    @users = User.all.paginate(page: params[:page])
  end

ページ単位の表示数も指定してやれる.

  def index
    @users = User.all.paginate(page: params[:page], per_page: 20)
  end

ビューを変更

ページネーションを出したい所へ以下を入れてやるだけ.

<%= will_paginate @users %>

とてもシンプルで良い.

Rails5にbootstrapを導入する

基本は公式に従うだけ.

Gemを導入する

Gemfileに以下を追記.

gem 'jquery-rails'
gem 'bootstrap-sass', '~> 3.3.7'

いつも通り更新.

$ bundle install

application.cssをリネーム

$ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss

application.scssを編集

以下を削除.

 *= require_tree .
 *= require_self

以下を追記.

// 必ずこの順番でないとダメ
@import "bootstrap-sprockets";
@import "bootstrap";

application.jsを編集

以下を記述.

//= require jquery
//= require bootstrap-sprockets
//= require_tree .

試す

雑にscaffoldで準備して試す

$ rails g scaffold user name:string email:string
$ rails db:migrate
$ rails s

出てきたけどなんか変.

containerを書いておく

bootstrapってcontainer書いておかないとダメだった.

<!-- containerのdivで囲む -->
<div class="container">
    <%= yield %>
</div>

これで綺麗に出た.