write ahead log

ロールフォワード用

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ってコメントあったっけ?)

MySQLpostgreSQL限定なのだろうか.

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とcocoonを使って明細のあるフォームを作る

一昔前にRailsを触っていた時にはnested_formというプラグインが良く使われていた気がするのですが,最近はcocoonというものが良いそうです.

どちらを選んでもどうせ大したことはしませんし, どちらでも良いのですがnested_formの方はあまりメンテされている感じでもないのでcocoonを使う事にしました.

メンテされないなら自分で直すぐらいの気持ちでやっていきたいものですけどね.

サンプルプロジェクトの内容

今回は簡単な注文書を作るアプリにしてみようと思います.

細かい作り込みは無視して注文書のヘッダ(鏡とかとも言いますね)に

  • 表題
  • 顧客名

で, 明細に

  • 品名
  • 金額
  • 数量

を持たせるというとても単純な構成です.

当然, 明細は複数あるのでここでcocoonを使います.

本当に業務などで使うには全然足りませんがサンプルには十分かなと.

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

$ rails new cocoon_sample

プラグインの導入

とりあえずGemfileをいじります. 以下を追記.

まだjQuery生きてます.

gem 'jquery-rails'
gem 'cocoon'

いつも通り.

$ bundle install

application.jsへ以下を追加.

//= require turbolinks
//= require jquery  <=---ここ
//= require cocoon  <=---ここ
//= require_tree .

注文書管理ページを作る

scaffoldで雑に用意.

$ rails g scaffold orders title:string customer:string
$ rails db:migrate

注文書明細のモデルを用意する

$ rails g model OrderDetails order:references item:string price:integer quantity:integer
$ rails db:migrate

モデルを関連付ける

モデル2つを関連付けてやります.

# app/model/order.rb

class Order < ApplicationRecord
  has_many :order_details
end

こちらは明細側. モデル生成時にもう行追加されてますが.

class OrderDetail < ApplicationRecord
  belongs_to :order
end

ネストしたモデルで更新できるようにする

# app/model/order.rb
class Order < ApplicationRecord
  has_many :order_details

  # 削除も受け入れる
  accepts_nested_attributes_for :order_details, allow_destroy: true
end

Strong Parameterの対応が要るのでコントローラを以下のように変更.

# app/controller/orders_controller.rb

    def order_params
      params.require(:order).permit(:title, :customer, order_details_attributes: [:id, :_destroy, :item, :price, :quantity])
    end

ビューの用意

明細を部分テンプレートにして使うらしい.

以下は親側の入力フォーム.

<!-- app/views/orders/_form.html.erb -->
...
  <div class="details">
    <%= link_to_add_association '行を追加', form, :order_details %>
    <table>
      <%= form.fields_for :order_details do |detail| %>
        <%= render 'order_detail_fields', f: detail %>
      <% end %>
    </table>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
...

以下が子側(明細). 部分テンプレート名は規約があるっぽい([モデル名]fields.html.erb)

規約外の名前や共通テンプレートを使う方法は公式に記載されている

<!-- app/views/orders/_order_detail_fields.html.erb -->
<tr class="nested-fields">
  <td><%= f.text_field :item %></td>
  <td><%= f.text_field :price %></td>
  <td><%= f.text_field :quantity %></td>
  <td><%= link_to_remove_association "行削除", f %></td>
</tr>

部分テンプレート名に規約がある以外はわかりやすいと思う.

追加するタグの位置を調整したい

公式に解説がある.

上述の例だとテーブルにはやはりヘッダが欲しくなる.

cocoonは行追加する際にはデフォルトでは行追加のlink_to_add_associationの親要素へ追加しようとする.

ここを制御するにはオプションを指定する必要がある.

例を見るとすぐわかる.

親側のビューのコードが以下(子は変わっていない)

  <div class="details">
    <!-- #order-detailsの下に追加するように指定 -->
    <%= link_to_add_association '行を追加', form, :order_details,
        data: {
          association_insertion_node: '#order-details',
          association_insertion_method: 'append'
        } %>
    <table>
      <thead>
        <tr>
        <th>品名</th>
        <th>数量</th>
        <th>単価</th>
        <th></th>
        </tr>
      </thead>
      <tbody id="order-details">
        <%= form.fields_for :order_details do |detail| %>
          <%= render 'order_detail_fields', f: detail %>
        <% end %>
      </tbody>
    </table>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>

他にも制御できるので公式を一度見るとよさそう.

行の追加・削除前後にコールバックを挟みたい

  • 最小・最大の行数を設定したい
  • 行追加時に合計金額を計算したい

こういう事をしたいときにコールバックを挟めると便利.

公式サポートされているコールバックがかかれている.

例として最大5行までしか追加できない明細にしてみた.

5行目が出来ると追加ボタンを隠す.

まずビュー側のボタンへidを付ける.

<!-- idを加えた -->
    <%= link_to_add_association '行を追加', form, :order_details, id: 'add-link',
        data: {
          association_insertion_node: '#order-details',
          association_insertion_method: 'append'
        } %>

次にjs(Coffee)で制御をかける.

# app/asset/javascript/orders.coffee
$ ->
    # 5行以上ある場合は追加ボタンを隠す
    $('#order-details').on 'cocoon:before-insert', ->
        if $('#order-details .nested-fields').length >= 5 
            $('#add-link').hide();
        else
            $('#add-link').show();

まとめとか

公式読めば大抵の事ができるので非常に便利です.

後で自分で参照しそうなのでgithubにいれておきました.

github.com

あと、公式読むと気付きづらいのですが, ERBのサンプルはwikiにありました.

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>

これで綺麗に出た.

Visual Studio Installer Projectで上書きインストールができない

インストーラを作りたい

昔々, Visual Studio にはInstaller Projectというインストーラを作成するためのプロジェクトを作成する機能がありました.

機能が少なくても標準でついてきていたので非常に便利だったようなのですが2005くらいから消えてしまったそうです.

(伝聞なのは私自身使ったことがないためです)

しかし未だにVS2017が出てもなお, 私はWindowsフォームアプリケーションをせっせと作るお仕事をしているので, インストーラが標準で作れないのは非常に不便です.

と思ったらVS2015からアドオンとして復活しています.

Visual Studio: Marketplace - Microsoft Visual Studio 2015 Installer Projects

具体的な使い方は他サイトにたくさんあるので参照してみてください.

おすすめです.

Microsoft Visual Studio 2015 Installer Projects をインストールする (Visual Studioの使い方 Tips)

Microsoft Visual Studio 2015 Installer Projects を利用してインストーラーを作成する

バージョンアップインストールが出来ない

上記で作れたのは良いのですが, インストール済みのプログラムを更新する方法がわからなくて困っていました.

解決したのでメモしておきます.

因みにこの問題, 海外でも結構困っている人が多いようです.

Visual Studio forum - Setup project does not uninstall previous version

解決法

スクリーンショットを取るのが面倒なのでテキストでだけ.

重要なのは以下の3つです.

  • インストーラプロジェクトのプロパティウィンドウで「RemovePreviousVersions」をtrueに設定する
  • リリースの度にインストーラプロジェクトのプロパティウィンドウで「Version」を向上させてやる
  • 入れ替えるDLLやexeのプロジェクトのプロパティウィンドウで「ファイルバージョン」を向上させてやる

最後のがハマりました.

アセンブリバージョンを一生懸命変えていたのですが, どうもインストーラプロジェクトが見るのは「ファイルバージョン」の様です.

アセンブリバージョンはビルドの度に簡単に番号を向上させる方法もあったりするのですが, ファイルバージョンはなさそうなので中々面倒です.

とはいえ, 目的は達せられたのでよしとします.