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

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>

これで綺麗に出た.

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のプロジェクトのプロパティウィンドウで「ファイルバージョン」を向上させてやる

最後のがハマりました.

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

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

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

CentOS7上にApache + PassengerでSinatraの動く環境を作る

Rubyを入れる(rbenvの導入)

rbenvで入れます.

rootへrbenvを入れますが, まずは環境を整えます.

$ sudo yum install git gcc gcc-c++ openssl-devel readline-devel

rbenvのリポジトリをclone. システム全体へ導入するので/usr/localへ入れます.

$ sudo git clone https://github.com/sstephenson/rbenv.git /usr/local/rbenv

rootへ昇格します.

$ su -

rbenv.shを作成します.

# vi /etc/profile.d/rbenv.sh
# 以下を記述
  export RBENV_ROOT="/usr/local/rbenv"
  export PATH="${RBENV_ROOT}/bin:${PATH}"
  eval "$(rbenv init --no-rehash -)"

設定ファイルを読み直します.

# source /etc/profile.d/rbenv.sh

ruby-buildを導入していきます.

# mkdir -p /usr/local/rbenv/plugins
# cd /usr/local/rbenv/plugins
# git clone https://github.com/sstephenson/ruby-build.git ./ruby-build
# cd ./ruby-build/
# ./install.sh

ようやくrubyの導入.

まずはrbenvで入れられるrubyのリストを表示します.

# rbenv install -l

今回は2.5.0にします.

# rbenv install 2.5.0
# rbenv global 2.5.0

最後にバージョンを確認.

ruby -v

SELinuxを無効にする

ごめんなさいごめんなさいごめんなさい.

# vi /etc/selinux/config
# 以下を変更
#SELINUX=enforcing
SELINUX=disabled

Passengerを入れる

gemでとりあえず投入.

# gem install passenger

続いてapacheモジュールの方も入れる.

# passenger-install-apache2-module

TUIが表示されるので, 1 => Ruby でEnter

一度abortします.

不足しているライブラリが表示されるので(親切!)インストールします.

# yum -y install libcurl-devel httpd httpd-devel apr-devel apr-util-devel

再度挑戦.

# passenger-install-apache2-module

入りました.

apacheの設定のスニペットが表示されます.

見逃しても以下で再表示できます.

# passenger-install-apache2-module --snippet
LoadModule passenger_module /usr/local/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/passenger-5.2.1/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
  PassengerRoot /usr/local/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/passenger-5.2.1
  PassengerDefaultRuby /usr/local/rbenv/versions/2.5.0/bin/ruby
</IfModule>

/etc/htpd/conf.dの下にpassenger.confを作ります. 上記のスニペットをコピペします.

# passenger-install-apache2-module --snippetの内容
LoadModule passenger_module /usr/local/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/passenger-5.2.1/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
  PassengerRoot /usr/local/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/passenger-5.2.1
  PassengerDefaultRuby /usr/local/rbenv/versions/2.5.0/bin/ruby
</IfModule>

# これはついで
# Passengerが追加するHTTPヘッダを削除する
Header always unset "X-Powered-By"
Header always unset "X-Rack-Cache"
Header always unset "X-Content-Digest"
Header always unset "X-Runtime"

# virtualhostの設定
<VirtualHost *:80>
  # ドメイン
  ServerName ドメイン名
  # アプリのルート
  DocumentRoot "/var/www/apps/sample_app/public"
  RackEnv production
</VirtualHost>

設定ファイルをチェック.

# apachectl configtest
AH00112: Warning: DocumentRoot [/var/www/apps/sample_app/public] does not exist
Syntax OK

apacheを再起動

# apachectl restart

アプリの設定

とりあえずbundlerを入れます.

# gem install bundler

passenger.confで指定したディレクトリを作ります.

tmpはpassengerのリロードに使います.

# mkdir -p /var/www/apps/sample_app/public
# mkdir -p /var/www/apps/sample_app/tmp

アプリのディレクトリへ移動して以下の様にアプリを作っておきます.

# cd /var/www/apps/sample_app

アプリ本体.

# app.rb
require 'sinatra'

get '/' do
  'Hello World!'
end

rackupファイル.

# config.ru
require File.expand_path(File.dirname(__FILE__)) + '/app'
run Sinatra::Application

Gemfile

source "https://rubygems.org"

gem 'sinatra'

最終的にディレクトリ構成はこんな感じ.

# pwd
/var/www
# tree .
.
├ apps
│   └ sample_app
│       ├ app.rb
│       ├ config.ru
│       ├ Gemfile
│       ├ Gemfile.lock
│       ├ public
│       └ tmp
├ cgi-bin
└ html

bundleを実行

# bundle install

ブラウザでアクセスすれば挨拶されます.

いやー, これ長い.