こたつとみかんとプログラミング

33才実務未経験ですがウェブエンジニアにジョブチェンジするために勉強したことをアップするためのブログです。

ページネーションの実装(kaminari)

kaminari を使ってみたのでメモ。
will_paginate よりもこっちのほうがとっつきやすい?ような気がした。

gem をインストール(bundle install)

gem はこちら
https://github.com/kaminari/kaminari

controller でページ番号に対応する範囲のデータを取得
# app/controller/tasks_controller.rb

def index
  @tasks = current_user.tasks.page(params[:page])
 ...
end

gemをインストールすると、表示するページ番号がparams[:page]でアクションに渡される。
page というスコープを使うだけで params[:page] に表示されるべきデータ範囲を検索できる。

view にページネーションのための情報を表示
# app/views/tasks/index.html.erb

<div class="mb-3">
  <%= paginate @task %>
  <%= page_entries_info @task %>
</div>

以下の2つのヘルパーメソッドを使用する。
ヘルパーメソッドの定義はここ
kaminari/helper_methods.rb at master · kaminari/kaminari · GitHub

paginate
現在、どのどのページを表示しているかを取得・表示する。
また、他のページに移動するためのリンクを生成し、表示する。

page_entries_info
全データが何件あり、現在どのデータが表示されているかの情報を表示

(必要あれば)locales の設定

kaminari は内部に英語の翻訳しか持っていないので、ja の翻訳ファイルを追加する。

ja:
  views:
    pagination:
      first: "&laquo; 最初"
      last: "最後 &raquo;"
      previous: "&lsaquo; 前"
      next: "次 &rsaquo;"
      truncate: "..."
  helpers:
    page_entries_info:
      one_page:
        display_entries:
          zero: ""
          one: "<strong>1-1</strong>/1件中"
          other: "<strong>1-%{count}</strong>/%{count}件中"
      more_pages:
        display_entries: "<strong>%{first}-%{last}</strong>/%{total}件中"

デザインの適用

$ bin/rails g kaminari:views bootstrap4

上記は bootstrap4 のデザインテンプレートを使用する例。
このコマンドで、app/view/kaminari 配下にビューテンプレートファイルが追加される。
他のテンプレートを使用するときはこちらを参照
GitHub - amatsuda/kaminari_themes

(必要あれば)表示件数の変更

3通りある。

1. perスコープで指定する。

# app/views/tasks/index.html.erb

def index
  @tasks = current_user.tasks.page(params[:page]).per(50)
 ...
end

2. modelに設定する

# app/models/task.rb
class Task < ApplicationRecord
  paginates_per 50
  ...

3. kaminari 用の config ファイルを作って設定する

$ bin/rails g kaminari:config
# config/initializers/kaminari_config.rb

# frozen_string_literal: true
Kaminari.configure do |config|
  config.default_per_page = 50
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
  # config.max_pages = nil
  # config.params_on_first_page = false
end
  ...

参考記事
https://github.com/kaminari/kaminari
https://www.amazon.co.jp/%E7%8F%BE%E5%A0%B4%E3%81%A7%E4%BD%BF%E3%81%88%E3%82%8B-Ruby-Rails-5%E9%80%9F%E7%BF%92%E5%AE%9F%E8%B7%B5%E3%82%AC%E3%82%A4%E3%83%89-%E5%A4%A7%E5%A0%B4%E5%AF%A7%E5%AD%90/dp/4839962227

ruby で配列の最大値・最小値を取得する

配列から最大値、最小値を取得する際は、max, min が使える。

array = [110,21,13,24,15]
# 要素の最大値を取得
p array.max
# => 110
p array.max(3)
# => [110, 24, 21]

# 要素の最小値を取得
p array.min
# => 13
p array.min(3)
# => [13, 15, 21]

# 文字列の場合
array = ["110","21","13","24","15"]

# 文字列はアルファベット順に評価されてしまう
p array.max          
# => "24"
p array.max(3)
# => ["24", "21", "15"]
p array.min
# => "110"
p array.min(3)
# => ["110", "13", "15"]

# 数値が文字列になっている場合は変換する
p array.map(&:to_i).max
p array.map(&:to_i).max(3)
p array.map(&:to_i).min
p array.map(&:to_i).min(3)

条件付きの最大値・最小値の取得は、max_by, min_by を使う。

array = ["banana", "orange", "melon", "strawberry", "grape"]
p array.max_by { |x| x.length }   
# => "strawberry"
p array.min_by { |x| x.length } 
# => "melon"

ruby の標準入力と出力

久々に paiza で ruby の問題を解いたのだが、結構忘れていたので振り返られるようにメモ。

ruby

1行に1要素だけ存在する場合

# 標準入力
Tokyo
line = gets
p line
# 出力結果
"Tokyo"

1行に複数要素が存在する場合

# 標準入力
Tokyo Nagoya Osaka
line = gets.split(" ")
p line
# 出力結果
["Tokyo", "Nagoya", "Osaka"]

gets は一行読み込んで、読み込みに成功した時にはその文字列を返す。
split は指定されたセレクタで分割し、配列で返す。

複数行に一つずつ要素が存在する場合

# 標準入力
Tokyo
Nagoya
Osaka
line = readlines.map(&:chomp)
p line
# 出力結果
["Tokyo", "Nagoya", "Osaka"]

readlines はすべての行を読み取り、1行ごと配列に格納する。
また、map を使って配列のすべての要素に chomp で改行なしにしている。

複数行に複数要素が存在する場合

# 標準入力
Tokyo Nagoya Osaka
Japan Korea China
lines = []
while line = gets
    lines << line.chomp.split(' ')
end

p lines
# 出力結果
[["Tokyo", "Nagoya", "Osaka"], ["Japan", "Korea", "China"]]

while line = gets ですべての行を取得するまで繰り返し…している。

参考記事
Ruby 標準入力から値を受け取る方法 - Qiita
IO#gets (Ruby 2.7.0 リファレンスマニュアル)
IO.readlines (Ruby 2.7.0 リファレンスマニュアル)

haml記法まとめ

今週からお試しで働く会社が haml で書いているということで、erbファイルを haml に書き換えた。

Rails での使い方

  • Gemfile に haml-rails を追加して、bundle install
  • ファイル拡張子の部分を erb から haml に変更。または 同名で拡張子が haml のファイルを作る。
  • erb 拡張子ファイルを削除。(同じファイルがあると、erbの方が優先して読み込まれるため)
自動変換について

haml-rails をインストールすると、erb2haml という hamlに自動変換するgem も一緒にインストールしてくれる。
以下のコマンドで自動変換。

# erbファイルを残して変換
$ rake haml:convert_erbs

# erbを削除して変換
$ rake haml:replace_erbs

今回は書く練習をしたかったので、自動変換を使わずファイルをひとつづつ自分でコードを記述していった。(ファイル数が多くてめちゃ大変だった。大量にある場合は自動変換したほうが絶対便利。
以下、記法まとめ。

基本的に、各タグは 「%」 が先頭

!!!
%html
  %title これはテストです
  %meta
  %body
    %header
      = yield
    %footer
  • 「!!!」で「docutype html」になる。
  • 各タグの頭に「%」をつける。閉じタグいらない。
  • インデントで書く。(スペースずれなど一個でもあると作動しない)
  • 上のコードを html に直すとこうなる↓
<!DOCTYPE html>
<html>
  <title>これはテストです</title>
  <meta></meta>
  <body>
    <header></header>
    <!----- ※ 各ページ内容が入る ----->
    <footer></footer>
  </body>
</html>

divは省略が可能。 class と id の指定方法

/ コメント
.collapse.navbar-collapse#head-menu
  %h1 メニュー
  %ul.language_list
    %li Ruby
    %li Rails
    %li HTML
  %p
    これは、わかりやすいサイトです。
    %br
    これは、わかりやすいサイトです。
  • コメントは「/」を先頭につける。
  • divのみ「div」省略できる。
  • 各タグにプラスして「.」で class 指定、「#」で id 指定できる。
  • 複数 class がある場合は、「.」でつなげる。

ruby を使う場合は、「-」または「=」で

  = render "ayouts/header", record: @record
  = link_to "コラム", articles_path, class: "nav-link"

  / if
  - if user_signed_in?
    = render "layouts/login_user_header", record: record
  - elsif admin_signed_in?
    = render "layouts/admin_user_header"
  - else
    = render "layouts/no_login_user_header"

  / form_for
  = form_for(@user, url: users_path) do |f|
    .field
      = f.label :name
      = f.text_field :name

    .field
    = f.label :email
    = f.email_field :email

    .field
    = f.submit "ログイン"
  • haml 内で ruby を使用する場合は、「-」「=」を使用する。
  • <%= ... %>は = ...のように記述する。(render, link_to, formなど)
  • <% ... %>は - ...のように記述する。(if文, ループなど)

通常の文字列と変数を1行で表す場合

  %p= "#{current_user.name}さん、こんにちは"
  • タグ名の後にスペース空けず「=」を使用する。

provide / yield の使い方

- provide(:h1, "コラム")

  %h1.head_title
    = yield(:h1)

aria, data などの接頭辞がつく属性など

  .modal#modal-login{ aria: { hidden: :true, labelledby: "exampleModalLabel" }, role: :dialog, tabindex: "-1" }
    .modal-dialog{ role: :dialog }
      .modal-content

  %table.table.table-hover.table-bordered
        %thead.thead-light
          %tr
            %th.text-center{ rowspan: 2 } 年月日
            %th.text-center{ colspan: 3 }<i class="fas fa-cloud-sun"></i>
  • class や id 以外の属性は、{}で囲う
  • aria, data など共通の接頭辞がつく属性が複数ある場合、その接頭辞同士でまとめることができる。

javascript の書き方(.js.haml

$("#recordModal").html("<%= escape_javascript(render 'form') %>")

<%= ... %> の部分を #{} に変更する

$("#modal-record").html("#{escape_javascript(render 'form', record: @record)}")

普通に html ファイル内にスクリプトを記述するときは、%script 以下に書けば良いらしい。

Docker の image を軽くする

Docker に無事既存の web アプリを載せることはできたものの、めちゃ重い。web アプリのイメージだけで 5GB もあった。
イメージを軽くするには、ベースのイメージを alpine Linux なるものに変えると軽くなるということだったので、変えてみた。

# Node.js & Yarn
FROM node:10.15.1-alpine as node

RUN apk add --no-cache --virtual .ruby-builddeps bash curl \
  && curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.22.4

# Ruby & Bundler & postgresql-client
FROM ruby:2.6.5-alpine

COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /opt/yarn-* /opt/yarn
RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn

ENV RUN_PACKAGES="nodejs postgresql postgresql-dev tzdata" \
    DEV_PACKAGES="build-base curl-dev gcc libc-dev libxml2-dev linux-headers make" \
    CHROME_PACKAGES="chromium chromium-chromedriver dbus mesa-dri-swrast ttf-freefont udev wait4ports xorg-server xvfb zlib-dev"

RUN apk --update --no-cache add ${RUN_PACKAGES} \
  && apk --update --no-cache add ${DEV_PACKAGES} \
  && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
  && apk del --purge tzdata

ENV ENTRYKIT_VERSION 0.4.0

# Entrykit & ChromeDriver
RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && mv entrykit /bin/entrykit \
  && chmod +x /bin/entrykit \
  && entrykit --symlink \
  && apk --update --no-cache add ${CHROME_PACKAGES}

# 作業ディレクトリの作成、設定
RUN mkdir /ketsuatsu_app
WORKDIR /ketsuatsu_app

# bundle install & Delete Unnecessary Packages_and_Cache & Copy File
COPY Gemfile /ketsuatsu_app/Gemfile
COPY Gemfile.lock /ketsuatsu_app/Gemfile.lock
RUN gem install bundler --version 2.1.2 \
  && bundle install --path vendor/bundle \
  && find vendor/bundle/ruby -path '*/gems/*/ext/*/Makefile' -exec dirname {} \; | xargs -n1 -P$(nproc) -I{} make -C {} clean \
  && apk del ${DEV_PACKAGES} \
  && apk del .ruby-builddeps
COPY . /ketsuatsu_app

ENTRYPOINT [ \
  "prehook", "bundle install -j3 --path vendor/bundle", "--", \
  "prehook", "ruby -v", "--", \
  "prehook", "node -v", "--" \
]
# Node.js & Yarn
FROM node:10.15.1-alpine as node

RUN apk add --no-cache --virtual .ruby-builddeps bash curl \
  && curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.22.4

最初に Node.js 10.15.1 のベースイメージを読み込み、nodeという名前をイメージにつけている。
次に alpine では使えない bashcurl コマンドをイメージに加えてコマンドを使えるようにして、
Yarn 1.22.4 をインストールしている。

このイメージには、下記のフォルダに Node.js と Yarn がインストールされている。

/usr/local/bin/node
/opt/yarn-v1.21.1
# Ruby & Bundler & postgresql-client
FROM ruby:2.6.5-alpine

COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /opt/yarn-* /opt/yarn
RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn

次に、ruby 2.6.5 のベースイメージを読み込み、先ほど作ったnodeイメージの Node.js と Yarn を ruby のイメージにコピーしている。
そして、ln -s コマンドでシンボリックリンクを作成している。
Node.js は /usr/local/bin/node に、 Yarn は /usr/local/bin/yarn にコピーされたことになる。

ENV RUN_PACKAGES="nodejs postgresql postgresql-dev tzdata" \
    DEV_PACKAGES="build-base curl-dev gcc libc-dev libxml2-dev linux-headers make" \
    CHROME_PACKAGES="chromium chromium-chromedriver dbus mesa-dri-swrast ttf-freefont udev wait4ports xorg-server xvfb zlib-dev"

複数のパッケージを変数に格納してあとで使いやすくしている。

RUN apk --update --no-cache add ${RUN_PACKAGES} \
  && apk --update --no-cache add ${DEV_PACKAGES} \
  && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
  && apk del --purge tzdata

先ほど変数に格納した必要なパッケージをインストール。
RUN と DEV に分けているのは、 RUN の方はそのまま Docker 上で使用するが、 DEV の方は環境構築が終わったら必要なくなるパッケージなので、あとで削除するためにこの分け方をしている。
apk del --purge パッケージ名 でそのパッケージに関連するファイルもまとめて削除できる。

# Entrykit & ChromeDriver
RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && mv entrykit /bin/entrykit \
  && chmod +x /bin/entrykit \
  && entrykit --symlink \
  && apk --update --no-cache add ${CHROME_PACKAGES}

Entrykit と ChromeDriver のインストール。
前回は ChromeDriver のインストールのために直接 URL を叩いていたが、 apt-get から apk に変わったとき、ChromeDriver インストール時に必要になる apt-key というコマンドが使用できないことが判明。これを何に置き換えれば良いかわからず苦戦したが、別の記事で必要なパッケージを読み込む方法があったので、そちらを採用した。

# bundle install & Delete Unnecessary Packages_and_Cache & Copy File
COPY Gemfile /ketsuatsu_app/Gemfile
COPY Gemfile.lock /ketsuatsu_app/Gemfile.lock
RUN gem install bundler --version 2.1.2 \
  && bundle install --path vendor/bundle \
  && find vendor/bundle/ruby -path '*/gems/*/ext/*/Makefile' -exec dirname {} \; | xargs -n1 -P$(nproc) -I{} make -C {} clean \
  && apk del ${DEV_PACKAGES} \
COPY . /ketsuatsu_app

この箇所はあまり変わりないが、最後にファイルコピーの前に必要ないパッケージとキャッシュを削除している。

また、ここでは割愛するが、docker-compose.yml の方でもdbのイメージに alpine を採用している。
以上でビルドした結果、5GBあったのが2.5GBくらいまで削減できた。

しかし、エラーが多数発生。今回はローカル環境で作った既存アプリを dockerに乗せて軽量化しようとしたが、Docker のイメージのほとんどは Ubuntu 環境であり、ローカル(Mac OS / つまり unix)環境のものを乗せようとするとうまく表示されないとのこと。
やるなら、最初から Docker 上で作るときのみ alpine は使った方が良さそう。

参考記事
Dockerのマルチステージビルドを使う - Qiita
Docker イメージサイズを抑えながら Ruby on Rails + PostgreSQL の開発環境を作成する - bitA Tech Blog
Rails 6.0 × MySQL8でDocker環境構築(Alpineベース) - Qiita
RailsのDockerイメージを一番小さくする方法 - Qiita

未来日・過去日の判定(カスタムvalidation)

class Record < ApplicationRecord

  ...

  validate :cannot_be_in_the_future

  ...

  # 日付に未来日は設定不可
  def cannot_be_in_the_future
      if date.present? && date > Date.today
        errors.add(:date, :cannot_be_future_date)
      end
    end

  # 日付に過去日は設定不可
  def cannot_be_in_the_past
      if date.present? && date < Date.today
        errors.add(:date, :cannot_be_past_date)
      end
    end
end

これでも良いが、rails側に未来日を判定する Date.future?、過去日を判定する Date.past? があるので、これの方がわかりやすい。

class Record < ApplicationRecord

  ...

  validate :cannot_be_in_the_future
  validate :cannot_be_in_the_past

  ...

  # 日付に未来日は設定不可
  def cannot_be_in_the_future
      if date.present? && date.future?
        errors.add(:date, :cannot_be_future_date)
      end
    end

  # 日付に過去日は設定不可
  def cannot_be_in_the_past
      if date.present? && date.past?
        errors.add(:date, :cannot_be_past_date)
      end
    end
end

このままだと英語がそのまま表示されてしまうので、ja.yml に対応する日本語を登録する。

# config/locales/ja.yml

ja:
  activerecord:
    errors:
      models:
        record:
          attributes:
            date:
              cannot_be_future_date: "未来の日付は記録できません。"
              cannot_be_past_date: "過去の日付は記録できません。"

参考記事:
Ruby on Rails:過去日・未来日を判定する - Madogiwa Blog
Ruby on Rails:モデルに独自のバリデーションを実装する - Madogiwa Blog

migration ファイルの null: false について

コードレビューにて、migration ファイルに null: false をつけたほうが良いという指摘を受けたので、メモ。
結論としては、null: false も presence: true も両方設定したほうが良い。

null: false

指定したカラムがデータベースにカラの状態で保存されることを防ぐ。

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :reviews do |t|
      t.string  :name
      t.string  :email null: false
      t.timestamps null: false
    end
  end
end

presence: true

ActiveRecord のバリデーション。(Railsアプリケーションの判定)
指定したカラムが空の状態で保存されることを防ぐ。

class User < ApplicationRecord
  validates :name, presence: true
end

presence: true は Rails アプリケーションの判定であるが、直接DBにデータを登録しようと思えばできてしまう。そこで、DB へ空のデータが保存できないようにするために、null: false が必要になってくる。