Ruby on Railsで作るウェブアプリケーション

はじめに

前著「Ruby on Railsで作る『イベント告知サイト』」を書き終えて早6年。スタートアップ系のウェブアプリ開発トレンドはJavaScript(TypeScript)のフレームワークに移ってしまったのかなぁ、という感も正直なところ無きにしもあらずです。

前著の表紙

しかしながら、Railsの「高速プロトタイピング」の強みはまだまだ消えていないと筆者は考えています。リレーショナル・データベースのアーキテクチャも枯れたと言われて久しいですが、引き続き今でも多くのビジネスシーンで利用され、情報化社会を下支えしています。

絶頂期は過ぎたけど、歴史と深みのある大人のフレームワークとしてこれからもまだまだ動き続けるんじゃないかと、希望も込めながらもう一度このテーマで文章をまとめてみようと思い立って筆を執りました(キーボードに手を置きました)。

Ruby on Railsを体系的に学ぶには「Railsチュートリアル」や「Railsガイド」という有名なウェブサイトの利用をオススメします。本書はこうした網羅的なドキュメントには及びませんが、ちょっとした読み物的な感じで、イチ開発者の目線からみたRailsの現状や開発のコツなどに触れて頂ければ幸いです。

対象読者・前提知識

Macの基本的な操作に加え、後述するAtomのようなプログラミング向けのテキストエディタの基本的な取り扱い、及びターミナルの操作ができることを前提とします。書籍内でも必要な説明は適宜挟みますが、Rubyを仕事などで実用的に使って行きたい場合は「UNIXコマンド」と端末の操作についても並行して学んでおくことを推奨します。多くのRuby実行環境では、これらは切っても切り離せない関連性があります。巻末に「付録」として主要なUNIXコマンドの解説を付記しています。

Windows環境でもDockerやRubyをインストールすることで、原則として同じことが可能です。ターミナルアプリの操作をコマンドプロンプトに置き換えて頂ければ、ほとんどは動作するはずです。しかしながら、本書ではWindowsでの利用を想定した解説は行いませんので、予めご了承ください。

本書は、「完全な初心者」をターゲットにした入門書ではありません。解答編でなるべく説明はこころがけますが、基本的には読者自身の「調べる力」の涵養を目した構成になっています。つまり、問題が与えられた時に「今の自分にこれを解くことが出来るか?」という判断に、まずは全力で取り組んで貰いたいと思います。そして「今の自分に足りない知識は何か?」と、問いを進めてみてください。最終的に正解にたどり着かなくても、これらの問いを繰り返すことで、「自分が学ぶべきこと」の概形がぼんやりとでも見えてくれば、学びのステップは確実に一歩進んでいます。

前書との違い

前著では、Vagrantを使って仮想環境を作っていましたが、今回はDockerを使用しています。単一もしくは構成の近い少数のプロジェクトを管理するだけなら、引き続きVagrantのような仮想環境で問題ないと思いますが、業務で扱っている場合など、多数の環境を保持しなければならない場合には、Dockerの軽量さに軍配があがるように思います(学習コストとのトレードオフにはなりますが…)。中心となるRubyとRailsもバージョンアップをしています。執筆時点(2021.06)では、Ruby3.0が最新ですが、Railsとの相性問題が完全に解消されていないため、念のため一つ手前のバージョン(2.7)を使用しています。

Vagrant -> Docker
Bash -> Zsh
Ruby 2.1 -> Ruby 2.7
Rails 5 -> Rails 6.1

環境構築(一般)

最初に強みとして紹介した「高速プロトタイピング」をいきなり否定する話になってしまいますが、 Railsはここで躓く人が多いのではなかろうか、と考えています。rails newと打ってプロジェクトを開始するまでが意外とハードルが高い。名前の通りレールの上を走ればあっというまに遠くまで行ける、そういう思想のフレームワークだったはずなんですが、Rubyという処理系、そして周辺のライブラリが 多様な環境で動作するようになった代償として、あまりにも色んなタイプのレールが存在しすぎているような気がします。

例として、筆者が現在管理していたり、導入を検討してみた環境を以下に挙げてみました。

大まかに言うと、OSの上にRubyを載せて、その上にRailsが乗っかるイメージです。 全て問題なく動作している限りにおいては、それぞれの違いを意識することはないのですが、、 それはあくまで理想的な状況であって、実際には上記の組み合わせ次第で様々なエラーに出くわします。

インターネット上に解決策が存在している、と言っても、それがどの組み合わせに対して有効なものなのか…はっきり言って大抵の人はゲンナリしてしまう複雑さだと思います。上述のRailsチュートリアルを読んでも、冒頭のから「〜の環境の場合は」という注意書きが多いことに気づかれると思います。これこそが、Railsが広く普及した証左でもあるのですが、新規の参入者を戸惑わせるポイントにもなっていると思います。

macOS

https://www.apple.com/macos/big-sur/

Apple社の開発するUNIXベースのオペレーティング・システムです(2021.01時点の最新版は「Big Sur」)。シンプルなユーザインタフェースや操作性が特徴として挙げられることが多いですが、開発者にとっては「UNIXベース」である、という点がより重要になってきます。Termnal(ターミナル)というアプリを介して、CUI(コマンドライン・ユーザ・インタフェース)から様々な機能を呼び出すことが出来ます。後述するRailsやDockerもCUIからの操作を前提としていますので、Terminalを操る知識はほとんど必須と言っても過言ではないでしょう。

Terminal.app

macOSに標準添付されているアプリケーションです。「端末エミュレータ」と呼ばれることもあります。この中でさらに「シェル」と呼ばれるプログラムが起動し、ユーザからのコマンド入力やバックグラウンドプロセスを処理します。「Zsh」という名前のシェルプログラムが標準で起動します。このアプリの使い方は実際の開発や運用の現場で非常に重要になってくることがありますので、興味のある方は「Zsh」関連の書籍や情報にも目を通しておかれることをお勧めします。

ATOM

https://atom.io/

「21世紀のテキストエディタ(自称)」を標榜するちょっとお洒落な感じのエディタです。GitHub社が開発しています。使い勝手としてはSublime Textなどに近く、AptanaやRubyMineなどのIDEと比べると汎用的ですが、静的な型付けが無く、メソッドやフィールドを動的に追加でき、括弧の省略記法など自由度の高いRubyという言語の開発に於いては、リファクタリングなどの機能が活躍できる範囲はそもそも限られています。これくらいの機能性が調度良いのではないかと筆者は感じています。Gitリポジトリの状態に応じてファイルツリーの色を調整してくれたり、Markdown形式のテキストのプレビューをしてくれたり、と痒いところに手が届く機能が最初からひと通り揃っているのも魅力です。

MySQL

http://www.mysql.com/

世界で最も有名なオープンソースのデータベースシステムです。Oracle社に買収され先行きが危ぶまれましたが、現在も積極的に開発が続けられており、安定性や機能面には定評があります。本書では、直接利用しませんが、

PostgreSQL

https://www.postgresql.org/

こちらも公式サイトに「世界で最も進んだ」と書かれたデータベースシステムです。30年以上の実績があり、信頼性の高さには定評があります。本書では、このエンジンを使ったシステム運用を想定してゆきます。

Ruby

https://www.ruby-lang.org/

本書を読まれている方ならご存知でしょうけれども、折角なので改めてご紹介を。Rubyはオブジェクト指向のLL(ライトウェイト)言語です。PerlやPythonなどと並び世界的に利用されるメジャーな言語です。筆者は元々、Pythonを利用していましたが、日本語を扱う際の親和性などが気に入り、こちらに鞍替えしてきた経緯があります。実際に使ってみるとその柔軟な構文と拡張性の高さにすっかり魅了されること請け合いです。

Rubygems

https://rubygems.org/

Rubyのパッケージ管理ツールです。Linuxを使われる方ならaptやyum、(大雑把に言えば)MacのApp StoreやGoogleのGoogle Playなどを想像して頂ければ良いかもしれません。Rubyの開発に利用できる各種ライブラリやフレームワークが公開されていて、コマンドラインからそれらをインストール・管理することができます。このツールで配布される個々のライブラリやフレームワークは「gem(ジェム)」と呼ばれます。

Bundler

http://bundler.io/

Bundlerはgemの依存性を管理するためのツールです。gemは便利な仕組みですが、長期間に渡って、或いは多数のプロジェクトを同一ホストで同時並行的に運用する場合に、バージョン番号の不整合が起こってくることがあります。例えば、あるプログラムは古いバージョンのままのライブラリが必要な一方で、別のプログラムは新しいバージョンでなければ動かない、というような状況です。Bundlerはこの問題を解決してくれます。Ruby on railsで新しいプロジェクトを作成すると、自動的にBundlerのセットアップを行います。

Ruby on Rails

http://rubyonrails.org/

最後に紹介するのが、本書の主役、Ruby on Railsです。手軽にデータベースと連携したウェブアプリを立ち上げることができるウェブブレームワークとして(一部の業界では)一世を風靡しましたが、今は押しも押されもせぬ地位に収まった感があります(筆者の主観ですが)。WebrickとSQLiteという簡易ウェブサーバとデータベースを使って、ものの数分でアプリが立ち上がるスピードは確かに目を見張るものがありますが、一方で、これらの組み合わせでは実運用に耐える構成にはそのまま持ち込めないという問題もあります。つまり、プロトタイピングにはとても便利なのですが、運用までしっかり検討しだすと、実はそれほどのスピード感はない、というのが実情ではないでしょうか。しかしながら、冒頭でも述べたように、「プロトタイピング」用途での優位性はありますし、その他にも優れた機能を多く有していることも間違いありません。本書で実際にアプリケーションを構築するプロセスを通して、じっくりと一つ一つRailsの強力な機能を使いこなせるようになってゆきましょう。

Docker

https://www.docker.com/

コンテナ仮想化という技術を用いて、アプリケーションを実行する環境を提供します。OS全体を仮想化するVagrantなどと比べて、軽量な立ち上げが可能になります。また、仮想環境を構築するコマンドが設定ファイルになっており、ファイルを共有することで複数のマシンで同一の開発環境を立ち上げることが容易になるメリットもあります。

環境構築例

Docker公式のrailsインストール手順に書かれているを参考に、本書の環境に合わせて幾つか調整します。

(本書で使う)基本のイメージ

後半でHerokuへのデプロイを行うため、Herokuのデフォルト設定であるPostgreSQLをデータベースとして使用するバージョンです。

まずはrubyのイメージをベースにrailsを動かすための定義を docker-web というファイルに書きます。

FROM ruby:3.0.3

RUN apt-get update -qq && apt-get install -y nano nodejs postgresql-client \
  && gem install bundler -v "~> 2.2" -N && gem install rails -v "~> 6.1" -N \
  && mkdir /railsapp

WORKDIR /railsapp
COPY . /railsapp
# RUN bundle install

上記のイメージを使ったdocker-compose.ymlの構成が以下のようになります。

version: '3.8'
services:
  db:
    image: postgres:12.6
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - ./tmp/postgresql/data:/var/lib/postgresql/data
  web:
    build:
      context: .
      dockerfile: docker-web
    command: "rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/railsapp
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      EDITOR: nano

この内容のdocker-compose.ymlというファイルを新しいディレクトリに置いて、プロジェクトの作成を開始することができます。

mkdir practice-rails1 && cd practice-rails1
# docker-web, docker-compose.ymlを設置
docker compose run web rails new . --database=postgresql --minimal

プロジェクトの作成の中で、Railsが必要とするライブラリのインストール(bundle install)とが動作するため、ある程度時間がかかります。気長に待ちましょう。正常に完了すると、現在のディレクトリの中にrailsプロジェクトの雛形となる多数のファイルが生成されているはずです。

以降のセットアップは次章で実際にアプリケーションを作りながら説明します。

RaspberryPiでの実施など、リソースが限られている場合は、--skip-gitをつけてリポジトリの初期化をスキップすることが可能です。

[付録]MySQLを使用する場合

PostgreSQLの代わりにMySQLを使用する場合の設定ファイルは以下のようになります。

FROM ruby:2.7.5

RUN apt-get update -qq && apt-get install -y nano nodejs \
  && gem install bundler -v "~> 2.2" -N && gem install rails -v "~> 6.1" -N \
  && mkdir /railsapp

WORKDIR /railsapp
COPY . /railsapp
# RUN bundle install
version: '3.8'
services:
  db:
    image: mysql:8.0
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./tmp/mysql:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=secret
  web:
    build:
      context: .
      dockerfile: docker-web
    command: "rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/railsapp
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      EDITOR: nano

手順については、PostgreSQLの場合とほぼ同じです(次章を参照してください)。 最初のプロジェクトを作成する場合の引数と、config/database.yml に設定するユーザ名だけ留意してください。

mkdir practice-rails-mysql && cd practice-rails-mysql
# docker-web, docker-compose.ymlを設置
docker compose run web rails new . --database=mysql --minimal --skip-git
  username: root
  password: secret
  host: db

もちろん、プロジェクトを作り込むに従って細かい挙動の差が色々と見えてきます。 小規模なプロジェクトの場合はあまり問題にならないかもしれませんが、長く使われる、 或いはユーザが一気に増えるようなことが想定される場合は、データベースは慎重に選択する必要があります。

その他のライブラリを使用する

ここでは、さらに他のライブラリを使用する場合に、Dockerのイメージを再構築する方法を例示する予定でしたが、 本書で扱う範囲の拡張機能では、特にイメージ自体の修正が必要なケースは出て来ませんでした。 例えば、alpine系の軽量なDockerイメージなどをベースにしている場合は、画像処理をしたいとか、 特殊なgemを追加する際に追加のライブラリが必要になる場合が想定されます。

そうした場合には、 docker-webapt-get install にライブラリを追加して再度ビルドが必要です。 毎回ビルドしてから動作確認をするのは大変なので、以下のようにシェルで接続して色々試してから設定を書き換えるのがオススメです。

docker compose run web bash

基本編

前章までに開発用の環境が整いましたので、ここから実際に動作するアプリケーションの構築にかかろうと思います。まず最初に作るのは、とてもシンプルなお問い合わせフォームです。以下のように仕様(やりたいこと)を決めてみましょう。

とりあえず上記のようなシンプルなウェブサイトを目指してみましょう。データベースエンジンにはPostgreSQLを使って、デザインはBootstrapでお手軽に済ませてしまうこととします。

まずは新しいRailsアプリケーションを作成します。「–database=postgresql」オプションを指定することでデフォルトのSQLiteの代わりにPostgreSQLを利用できるようになります。SQLiteでお手軽に作ることも可能ですが、後々サーバへ配備して拡張性も考慮していこうとなると、PostgreSQLやMySQLなどの本格的なエンジンに載せ替えていく必要性が出てきますので、はじめからこちらを使ってゆくことにします。

新しいプロジェクトを作成する

前節と同じ内容ですが、もう一度プロジェクトを作成します。この節から開始する読者向けに書いていますので、前節のプロジェクトをリネームして使いまわしても構いません。

mkdir practice-rails1 && cd practice-rails1
# 前章のdocker-web, docker-compose.ymlを設置
docker compose run web rails new . --database=postgresql --minimal --skip-git

プロジェクトの雛形が生成されたら、docker-webの最下行にあるbundle installのコメントを外し、有効化します。

RUN bundle install

config/database.ymlを編集し、default設定に下記の項目(host,username,password)を追加します(最下行ではありません)。

default:
  :(略)
  host: db
  username: postgres
  password: secret

その後、空のデータベースを作成し、docker compose upを実行するとローカル環境にRailsのwebサーバ(Puma)が立ち上がります!

docker compose build
docker compose run web rails db:create
docker compose up

以下をブラウザのアドレスバーに打ち込むと…

http://localhost:3000/

以下のような画面が表示されたら、ここまでは順調です!

Yay! you’re on Rails!

お問い合わせの骨組みを作る(scaffold)

先程、docker compose upを実行したターミナルでは、PostgreSQLとRailsのWebサーバが引き続き動作しています。終了する場合はCtrl+Cを入力します。ここからの作業は、このサーバを立ち上げたままの状態で行いますので、もう一つ新しいターミナルのタブまたはウィンドウを準備してください(ディレクトリも同じ”practice-rails1”に居る状態にします)。

以下のコマンドを入力します。

docker compose exec web rails g scaffold inquiry name email purpose:integer body:text
docker compose exec web rails db:migrate

このコマンドを実行すると各種のファイルが生成され、以下のURLにアクセスすると、

http://localhost:3000/inquiries

このような画面が表示されます。

Inquiries

「Create Inquiry」というボタンを押すと、新しいお問い合わせを作成する画面に遷移します。 ここが今回、ユーザがお問い合わせを送信する画面になります。

Create Inquiry

purpose列は、冒頭に定義した問い合わせの種類、例えば質問・見積依頼・苦情などを格納するために作成しています。 他の列と同様に文字列型で定義して、データベースを直接覗いても、中身がわかるようにした方が、 運用面では便利ではありますが、今回、異なるタイプの型も扱ってみたいのであえて数値(integer)型を使っています。 body列も同様に、一般的な問い合わせ本文だと長くても数百文字程度かと想定されますので、文字列型で良いのですが、 異なるタイプの文字型(text)を指定しています。

この辺り、Railsが簡単そうに見えて実は運用で落とし穴にハマる最初のポイントになるかと思います。 どんな型を選ぶかという決定がリリース後の運用や規模拡大の際の設計に大きく影響します。 どのデータベースエンジンを選ぶかによっても適切な列の型は変わりますので、総合的な判断が必要です。

今のままだとトップページに何も表示されませんので、お問い合わせの新規作成画面をトップに登録します。config/routes.rb に以下の一行を追加してください。

  root to: 'inquiries#new'

メールを送信する(action_mailer)

RailsにはActionMailerというメール送信のための仕組みがを作成します。 先程のscaffoldと同様に、必要なコードの雛形を自動生成することが出来ます。 以下の例ではsent(問い合わせをしたユーザに送る受付確認)とreceived(管理者に問い合わせがあったことを知らせるメール) という2つのメールを送るメーラをnotificationsという名前で定義しています。

docker compose exec web rails g mailer notifications sent received

実際にメールを送出する処理は、コントローラ内に記載します。

app/controllers/inquiries_controller.rb

# POST /inquiries
# POST /inquiries.json
def create
  @inquiry = Inquiry.new(inquiry_params)
  if @inquiry.save
    NotificationsMailer.sent.deliver_later
    NotificationsMailer.received.deliver_later
    redirect_to @inquiry, notice: 'Inquiry was successfully created.'
  else
    render :new
  end
end

今回は、--minimalオプションをつけてプロジェクトを生成しているため、config/application.rbを編集して以下のモジュールをコメントアウト(有効化)しておく必要があります(コンテナの再起動が必要です)。

require "action_mailer/railtie"

この状態で、問い合わせを適当に作成してみると、docker composeが起動しているターミナルに以下のようなログが書き出され、 Railsがメールを送信しようとしてくれていることが確認できます(この環境では実際にメールが飛んでいくことはないです)。

web_1  | [ActiveJob] [ActionMailer::MailDeliveryJob] [638b5a34-184d-49fd-b366-1c331aeb9695] Date: Mon, 18 Jan 2021 00:47:41 +0000
web_1  | From: from@example.com
web_1  | To: to@example.org
web_1  | Message-ID: <6004daadb40ae_14d3015746@bdaa1bce2230.mail>
web_1  | Subject: Sent
web_1  | Mime-Version: 1.0
web_1  | Content-Type: multipart/alternative;
web_1  |  boundary="--==_mimepart_6004daadac24b_14d3015687";
web_1  |  charset=UTF-8
web_1  | Content-Transfer-Encoding: 7bit
web_1  |
web_1  |
web_1  | ----==_mimepart_6004daadac24b_14d3015687
web_1  | Content-Type: text/plain;
web_1  |  charset=UTF-8
web_1  | Content-Transfer-Encoding: 7bit
web_1  |
web_1  | Notifications#sent
web_1  |
web_1  | Hi, find me in app/views/notifications_mailer/sent.text.erb
web_1  |

メールサーバの設定については、最終節の配備のところで実際に試してみたいと思います。 ここでは、ログ上で送信しようとしているメールが確認できればOKとします。

参考: app/mailers/notifications_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "info@lmlab.net"

  def sent(to: 'dummy@lmlab.net')
    @greeting = "Hi"
    mail to: to
  end
  :
end

BASIC認証を行う

お問い合わせが送信されて、データベースにレコードが登録されるようになりましたが、 このままでは、誰でもお問い合わせの履歴が見えてしまいます。 BASIC認証というシンプルな仕組みを利用してパスワードを知らないユーザにはアクセスが 出来ないようにしてしまいましょう。app/controllers/application_controller.rbに 以下のような関数(authenticate)を定義します。 このクラスにはアプリケーション全体で利用できるメソッドや変数を定義することができます。

class ApplicationController < ActionController::Base
  def authenticate
    authenticate_or_request_with_http_basic do |username, password|
      pw = Rails.application.credentials.basis_auth_password
      if (username == "kanri" && password == pw)
        session[:user] = username
      end
    end
  end
end

本質的な対策ではない、ちょっとしたテクニックですが、管理者の名前としての adminroot などは、 一般的すぎてブルートフォースアタックの標的にされやすいということがあります。 ここで定義する名前は管理者にしか思いつかないような、一般的でないものの方が”ちょっとだけ”安全です。

パスワードは以下のコマンドで暗号化されたストレージの中に書き込みます(nanoエディタの使い方は省略しています)。

docker compose exec web rails credentials:edit

最下行に以下のように買いてください。

basis_auth_password: secret

authenticateメソッドは、app/controllers/inquiries_controller.rbの一番上の部分で以下のように呼び出します。 お問い合わせの新規作成は誰でもアクセスできる必要があるので、それらは除外します。

  before_action :authenticate, except: [:show, :new, :create]

一般ユーザが問い合わせをするとRailsのコントローラ上では new -> create -> show という順番で処理が遷移します。 送信完了した際にshowが呼ばれるのですが、ここに認証無しで詳細を表示してしまうと、やはり情報漏えいのリスクに繋がります。

上記authenticateメソッドでセットしたsession[:user]変数を利用して、ログイン済みの管理者であれば詳細を表示、 ログインしていない一般ユーザであれば、(問い合わせ送信後にしか表示されないので)お礼のメッセージのみを返すように、 app/views/inquiries/show.html.erbを変更します。

<% if session[:user] == "kanri" %>
  <p>
    <strong>Name:</strong>
    <%= @inquiry.name %>
  </p>
  : (中略)
  <%= link_to 'Edit', edit_inquiry_path(@inquiry) %> |
  <%= link_to 'Back', inquiries_path %>
<% else %>
  <p>Thank you for the inquiry!</p>
<% end %>

レスポンシブ対応にする(bootstrap)

このままでも、最低限の機能はありますが、スマホファーストのこのご時世に流石にちょっと物足りないかもしれません。 Bootstrapというレスポンシブのフレームワークを入れて、スマートフォンから閲覧しても使いやすいインタフェースに書き換えましょう。

https://getbootstrap.com/

幾つか方法がありますが、ここでは一番お手軽にCDNを使う方法を紹介します。 21年1月時点、バージョン5が最新ですがまだβ版のため、4系の最新版を指定します。jqueryも必要なためslim版が指定されています。app/views/layouts/application.html.erbにlink,scriptタグを追加します。

<html>
  <head>
    : (略)
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
  </head>
  <body>
    : (略)
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script>
  </body>
</html>

Rails5系までの場合、bootstrapというgemをインストールしていました。 6系ではyarnを使って導入する方法もあるようなのですが、どうもまだ手順が確立されていない印象を受けました。 yarn addした後に書き換えなければいけない箇所が多くて大変そうです。CDNはお手軽ですが、 ソースを配布してくれるサイトが落ちていたりすると一緒に影響を受けてしまうことがあるため注意は必要です。

docker compose exec web yarn install bootstrap@4.5.3  # 今回は使用しません

app/assets/stylesheets/scaffolds.scssのスタイルがBootstrapと競合してしまうことがあるため、こちらを削除しておきます。

これで、Bootstrapの導入ができましたので、あとは既存のHTML(.erb)をBootstrapの流儀で修正してゆきます。 ここから先はどんなデザインのサイトにしたいかによって幾つかのパターンがあると思いますが、ひとまずシンプルな例を紹介します。

まずは、app/views/layouts/application.html.erbの中のyieldを以下のdivタグで囲っておきます。

  :
  <body>
    <div class="container">
      <%= yield %>
    </div>
  :

続けて、app/views/inquiries/new.html.erbを以下のように修正します。

<div class="row">
  <div class="offset-md-3 col-md-6">
    <h1>New Inquiry</h1>

      <%= render 'form', inquiry: @inquiry %>

      <% if session[:user] == "admin" %>
      <%= link_to 'Back', inquiries_path %>
      <% end %>
  </div>
</div>

app/views/inquiries/_form.html.erb内のフォームの要素には以下のようにクラス名を追加します。

  : (省略)
  <div class="form-group">
    <%= form.label :body %>
    <%= form.text_area :body, class: "form-control" %>
  </div>

  <div class="actions">
    <%= form.submit nil, class: "btn btn-primary" %>
  </div>
<% end %>

app/views/inquiries/index.html.erbもこんな感じでクラスを追加してみます。

<table class="table table-striped">
  :
        <td><%= link_to 'Show', inquiry, class: "btn btn-light btn-sm" %></td>
        <td><%= link_to 'Edit', edit_inquiry_path(inquiry), class: "btn btn-light btn-sm"  %></td>
        <td><%= link_to 'Destroy', inquiry, method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-danger btn-sm" %></td>
  :
<%= link_to 'New Inquiry', new_inquiry_path, class: "btn btn-primary"  %>

改めて一覧画面を表示すると、以下のようなスタイルが適用された状態になっているはずです。

inquiries

フォームのデザインは以下のような感じで、スマートフォンなどの小さなデバイスでアクセスした際の使いやすさも考慮された形になっています。

inquiry-new

flashメッセージを利用する

bootstrapの続きでもあり、railsの基本機能でもあります。

レコードの保存などの何らかの処理が終わり別のページにリダイレクトされた後にメッセージを表示する仕組みに利用されています。scaffoldで生成されたHTMLの中にも入っていますが、bootstrapのスタイルに合わせて表示する場合、以下のようなコードをlayouts/application.html.erbに入れておくと、どんなページに遷移してもflashメッセージが表示されるようになります。

<% if notice %>
  <div id="notice" class="alert alert-success"><%= notice %></div>
<% end %>
<% if alert %>
  <div id="alert" class="alert alert-warning"><%= alert %></div>
<% end %>

画像も投稿できるようにしてみる(active_storage)

お問い合わせフォームで画像も送信できるようにします。ActiveStorageという仕組みを利用します。まずは以下のコマンドで有効化が必要です。

docker compose exec web rails active_storage:install
docker compose exec web rails db:migrate

今回は、--minimalオプションをつけてプロジェクトを生成しているため、config/application.rbを編集して以下のモジュールをコメントアウト(有効化)しておく必要があります(コンテナの再起動が必要です)。

require "active_storage/engine"

続けて、config/storage.ymlを新しく作成し、以下の内容を記載します。

local:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

config/environments/development.rbにも設定の追記が必要です(これはエラーが分かりづらいので要注意)。

  config.active_storage.service = :local

モデルに以下の記述(has_many_attached)を追加します。一つで十分な場合は、has_one_attached になります。

class Inquiry < ApplicationRecord
  has_many_attached :images
end

フォームのHTMLにファイル添付用のフィールドを追加します。

<div class="form-field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true, class: "form-control" %>
</div>

コントローラでも許可が必要です(ちょっと書き方が変わっているので要注意かもしれません)。

def inquiry_params
  params.require(:inquiry).permit(:name, :email, :purpose, :body, images:[])
end

以上でファイルをサーバに送信できるようになります。 参照も追加したフィールド名を使って簡単に行うことができます。

<% if @inquiry.images.attached? %>
  <p>
  <% @inquiry.images.each do |i| %>
    <%= image_tag i %>
  <% end %>
  </p>
<% end %>

ActiveRecord以外にも、データベースにblob列を保持したり、carrierwaveというgemを使うなど、様々な方法がありますが、次の章で利用するHerokuとの連携を考慮すると、この方法が一番お手軽です。ただs、アプリケーションの性質によってケースバイケースになることもあります。この方法は、一般的にオブジェクトストレージというアプリケーションが稼働するサーバやデータベースとは別の領域に実際のデータを保存します。そこにアクセスするためのURLが払い出される格好になるのですが、通常の設定のままだとこのURLに直接アクセスされると誰でも画像が見えてしまいます。公開前提のデータなら問題ありませんが、お問い合わせ本文に含めるような秘匿性の求められる場合には、検討が必要です。

テストを実行する

ある程度動くようになった段階(人によっては「遅すぎる」と感じてしまうかもしれませんが)テスト用のコードも記述が必要です。 ここまでの操作でもある程度の数のテストが自動生成されています。それらをまとめて実行するには以下のコマンドを実行します。

docker compose exec web rails test   

ここまで一切、ケアをしてこなかったので、ここで幾つかエラーやテストの失敗が表示されてしまうはずです。 テストの結果で、Eはエラーが起こったことを、Fはアサーション(結果の確認)に失敗したことを意味します。

# Running:

........F

なぜエラーになっているか分からない場合、@responseオブジェクトの中身を表示してみるとヒントが得られるかもしれません

test "should destroy inquiry" do
  assert_difference('Inquiry.count', -1) do
    delete inquiry_url(Inquiry.first)
    p @response
    :

コードを修正する都度、全てのテストを走らせていると時間がかかります。以下のようにファイル名と行番号を指定して、個別に実行することが可能です。このコマンド(rails test..以降)は失敗した全てのテストに対して出力されるので、コピー&ペーストで実行することができます。

docker compose exec web rails test test/controllers/inquiries_controller_test.rb:41

今回の場合、最初にBASIC認証を入れていたため、テストコードから管理画面にアクセスできずに失敗しています。 (ベストなやり方ではありませんが)以下のようにBASIC認証をテスト時には無効化することで回避できます。

class ApplicationController < ActionController::Base
  def authenticate
    return if Rails.env == "test"
    :

本来的には、もっと本格的な認証機能を入れたり、認証機能の有効性(つまり認証していないユーザに情報が見えてしまわないことの確認)も含めてテストすることが望ましいと考えられます(次の章で、deviseという認証機能を提供するgemを紹介します)。

htmlを表示またはエスケープする

Railsのビューにはコントローラから渡してきた様々な値を表示することができます。しかし、悪意を持った(もしくは何も知らない)ユーザが、htmlやJavaScriptのコードを文字列に格納して送信し、それがそのまま表示されてしまうと様々な不具合、場合によってはセキュリティ上の脅威になってしまいます。

そのためRailsの文字列をそのままビューに渡すと、タグが無効化(=エスケープ)された状態で表示されるようになっています。それをせず、htmlをそのまま表示したい場合は、以下のように記載する必要があります。

test.html_safe

逆に、不要なタグを消してしまいたい場合、sanitizeが利用できます。また、下記の例では文字列中の改行をhtmlの改行タグ(br)に変換して、改行もレンダリングされるように工夫しています。

<%= sanitize(text.gsub("\n","<br>")) %> <%# br(改行)は残る! %>

歴史上の都合でしょうか、他にも似たような目的の様々なメソッドがあります。基本的にはsanitizeだけ覚えておけば事足りそうです。

<% text = "<b>1</b>\n<i>2</i>\n3 >= a" %>
<%= text %> <%# そのまま(タグはエスケープされる) %>
<hr/>
<%= h(text) %> <%# そのままと一緒(使う意味ない?) %>
<hr/>
<%= simple_format(text) %> <%# 勝手に改行 & pタグが付く(お好みで..) %>
<hr/>
<%= strip_tags(text) %> <%# タグが消える %>
<hr/>
<%= sanitize(text) %> <%# 危ないタグだけ消える %>
<hr/>
<%= sanitize(text.gsub("\n","<br>")) %> <%# br(改行)は残る! %>

エラーをユーザに通知する

処理の途中で想定外のデータが入ってきたり、または何らかの理由で情報を表示すべきでない場合があります。 Railsではコントローラで発生したエラーは、開発環境(development)では、スタックトレースとして詳細が表示され、 運用環境(production)で発生した場合には、詳細が隠された状態でユーザに通知されます。

つまり、(極端に言えば)以下のように例外を投げるだけでエラー処理が可能です。

  def show
    raise “Page not found!”
  end

ただ、これでは、実際のユーザにとってエラーの原因が全く伝わらないことになってしまいます。 (詳細を出し過ぎても分からないですし、なによりセキュリティ上のリスクになり得ます) renderメソッドを使って場面ごとに適切なページを表示するように作ることができます。

# エラーメッセージ(text)のみを返します
render status: 404, body: "Inquiry not found"
# public/404.htmlを表示します
render status: 404, file: “#{Rails.root}/public/404.html”
# app/views/shared/not_found.html.erbを表示します
render status: 404, file: "shared/not_found"
# ステータスコード404を返します。(通常のビューが表示されます)
render status: 404

詳しくはhttpのステータスコードなどを調べてみてください。

フォームをカスタマイズする

scaffoldで生成されたコードを見ながら、基本的な書き方は押さえられますが、ここでは、リファレンスを見ないと迷ってしまいそうな、フィールドの書き方を紹介します。

セレクトボックスは以下のように作成可能です。1つめは別のモデルから、2つめは配列から選択肢を取得します。

<%= form.select :page_id, options_from_collection_for_select(Page.all, :id, :name) %>
<%= form.select :status, [["Published","published"],["Hidden","hidden"]], {}, class: "form-control" %>

今回のサンプルではpurpose列をセレクトボックスに書き換えてみます。RailsのDRY原則に乗っ取って、定数の値は一箇所(モデル内)に宣言します。

app/models/inquiry.rb

PURPOSES = ["質問","見積依頼"]

def purpose_value
  PURPOSES[purpose] || purpose.to_s
end

def self.purposes_for_select
  PURPOSES.map.with_index.to_a
end

app/views/inquiries/_form.html.erb

<%= form.select :purpose, Inquiry.purposes_for_select, {}, class: "form-control" %>

app/views/inquiries/index.html.erb

<td><%= inquiry.purpose_value %></td>

長いテキストを途中で切って表示する

truncateというメソッドが用意されています。

app/views/inquiries/index.html.erb

<td><%= inquiry.body.truncate(80) %></td>

入力のバリデーション

現在のフォームは、実は何も入力しなくても送信が出来てしまいます。以下のようにモデルに設定を追加することで入力がない場合にエラーを表示することが出来るようになります。

app/models/inquiry.rb

validates :email, presence: true

一覧表示の順番を修正する

ActiveRecordのallメソッドで、データベースに入っている全てのレコードを取得できますが、表示順が固定ではありません。決まった順序で表示したい場合、orderメソッドを追加します。下記例ではid列の順にソートして表示します。

def index
  @inquiries = Inquiry.all.order(:id)
end

[課題]拡張機能を検討する

[付録]その他のTips

もっと知りたい時

次の章で、もう少し詳しく帳票形のアプリケーションの作成に必要なライブラリなどの説明をしますが、 方向性の違うアプリケーションを作りたい場合、ここで本書を離れていくのも一つの区切り目かと思います。

Railsはバージョンごとに挙動の違う部分もあったり、プロジェクトの作り方によって様々なことが起こり得ます。 Ruby on Rails ガイドなど網羅的に開設された資料を参考にして、深掘りしていくことをおすすめします。 また、拡張機能については、Ruby gemsというライブラリを探してみてください。 同じことをするライブラリでも複数の選択肢がある場合がありますが、その場合は、例えば、ダウンロード数の多寡や ライブラリ自体の更新頻度なども一つの指標になります(あくまで指標ですが…)。

次の章では以下のようなgemを追加していきます。

gem 'devise', '~> 4.7'
gem 'font-awesome-sass', '~> 5.12'
gem 'jquery-rails', '~> 4.4'
gem 'paper_trail', '~> 10.3'
gem 'prawn', '~> 2.4.0'

gemを追加したら、docker compose buildの実行が必要です。

うまく行かなかった時

scaffoldをやり直したい時は、destroyサブコマンドが使えます。

docker compose exec web rails destroy scaffold inquiry

動作がおかしくなってしまった時は、Dockerを再起動すると改善することがあります。 特にdocker compose runを何度も実行して沢山のコンテナを立ち上げっぱなしになっている場合など、時々downを実行すると良いでしょう。

docker compose down

データベースを削除して再作成する場合、db:dropまたはdb:resetが使えます。

docker compose run web rails db:reset

他のユーザが使用中です、とエラーが出てdropできない場合、上記のdocker compose downとupを実行すると回復することがあります。

本書では説明していませんが、個人作業の段階からgitのリポジトリを作り、作業のまとまりごとにバージョンを管理するのが一番地道で確実な方法です。OSから入れ直しになるような致命的なケースでも、GitHubなどのクラウド側にソースが残っていれば復旧が可能です。

minimalプロジェクトで削除ボタンを動作させる

Rails6の--minimalオプションを使ってプロジェクトを生成するとJavaScriptが無効化されるため、 scaffoldで生成されたDestroy(削除)ボタンが動作しなくなる問題があります。 httpのdeleteメソッドを使わないようにするなど、いくつか回避策はありますが、以下はrails-ujsを有効化する方法です。

config/initializers/assets.rb

Rails.application.config.assets.paths << "#{Rails.root}/app/javascript"
Rails.application.config.assets.precompile += %w( application.js )

app/javascript/application.js を新規作成し、以下の2行を記載します。

//= require rails-ujs
//= require_tree .

app/views/layonts/application.html.erbのheadタグ内に以下のタグを追加します。

<%= javascript_include_tag 'application' %>

Windows版Dockerでファイルの更新が検知されない

config/environments/development.rbのfile_watcherを以下のように書き換えると、コンテナを再起動しなくてもファイルの変更が検知されるようになります。

config.file_watcher = ActiveSupport::FileUpdateChecker

Timezoneの設定

Time.nowはUTC(標準時)を返します。日本標準時を使いたい場合、Time.zoneを使います。

Time.zone.now.strftime("%Y%m%d")

国際化対応は単純ではない問題です。詳しい対処法についてはRailsガイドの解説などが参考になります。まずは日本語(または他の単一の言語)で提供が出来れば十分、という場合、config/application.rbでタイムゾーンとロケール(言語)を固定してしまうことも可能です。

config.time_zone = 'Tokyo'
config.i18n.default_locale = :ja

拡張編

ここでは、以下のようなプロジェクトを作ります。

普段はペーパレス化を推進する発信に余念のない筆者ですが…紙を出力するアプリを作ります。 一見して古い・無駄と思えるようなことでも、実はそれなりの意義がある場合があります。 ドラスティックな変化を求めるよりも、紙の世界とも繋がりを持ちつつ、システム化を進めていく上で 実はPDFって大事な役割を果たす場面が多いように思います。

前節と同じく、データベースエンジンにはPostgreSQLを使って、デザインはBootstrapでお手軽に済ませてしまうこととします。

共通の準備

mkdir practice-rails2 && cd practice-rails2
# 前々章のdocker-web, docker-compose.ymlを設置
docker compose run web rails new . --database=postgresql --minimal

プロジェクトの雛形が生成されたら、docker-webの最下行にあるbundle installのコメントを外し、有効化します。

RUN bundle install

config/database.ymlを編集し、default設定に下記の項目を追加します。

  host: db
  username: postgres
  password: secret

前節のプロジェクト同様に--minimalオプションを付けてプロジェクトを生成していますので、config/application.rbを編集してこのあと必要になるモジュールを予め有効化しておきます。

require "action_mailer/railtie"

その後、空のデータベースを作成し、docker compose upを実行するとローカル環境にRailsのwebサーバ(Puma)を立ち上げるところまで確認しましょう。

docker compose build
docker compose run web rails db:create
docker compose up

続けてこのアプリ上で管理したいテーブルの定義をします。

docker compose exec web rails g scaffold work user_id:integer work_on:date start_at:datetime end_at:datetime location body:text approved:boolean
docker compose exec web rails db:migrate

ホストのルート(URLのホスト名以下に何も書かない状態)からアクセスできるように、rootを定義しておきます(先程のRailsの画面が見えなくなる代わりに新しく作ったモデルの一覧ページが表示されるようになります)。

config/routes.rb

  root to: 'works#index'

このモデルを使って色んなgemの紹介をしながら、最終的に仕様を満たすアプリケーションを作ります。

各節の冒頭に書かれている、gem 'foobar'...という記述をGemfileの最後尾に追加してください。 Gemfileを変更する都度、docker compose buildが必要です。

また、scaffoldで生成したフォームの「Destroy」ボタンを有効化したい場合、前章の[付録]にある、 「minimalプロジェクトで削除ボタンを動作させる」の手順の実行が必要です。

認証機能の導入(devise)

gem 'devise', '~> 4.7.3'

deviseというgemを使います。以下の手順でインストールが必要です(userの部分は、任意のモデル名)。

docker compose exec web rails g devise:install
docker compose exec web rails g devise user
docker compose exec web rails db:migrate

config/environments/development.rb に以下の設定を書き足します。

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

あとは、認証を追加したいコントローラに以下のようにbefore_actionを定義すれば、ログインしていないユーザがアクセスできないアクションを設定することが可能です。

before_action :authenticate_user!

上記の操作だけで、ユーザの登録、認証、パスワードのリセットなどなど、ウェブサイトでユーザ管理をするのに最低限の機能が使えますが、実際には見た目をある程度整えたり、パスワード以外の情報も持たせたいというケースが多いと考えられます。そのような場合、デフォルトのビュー(html.erb)をプロジェクト内に書き出して、カスタマイズすることができます。

docker compose exec web rails g devise:views

app/views以下に多数のファイルが書き出されるので、必要に応じて、スタイルを追加したりフォームに項目を足したりすることができます。

フィールドの追加は少し複雑で、例えば、ユーザ登録時とユーザ情報の更新時に名前(name)というフィールドを 追加して管理しようとする場合、コントローラの方でも明示的に扱えるように設定する必要があります。

# in application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?

protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end

db/seeds.rbに予めユーザを作っておくと動作確認が楽になります。(このままのパスワードで運用しないでください!)

User.create(email:"boss@lmlab.net" ,password:"secret")
User.create(email:"worker1@lmlab.net" ,password:"secret")
User.create(email:"worker2@lmlab.net" ,password:"secret")
docker compose exec web rails db:seed

PDFの生成(prawn)

gem 'prawn', '~> 2.4.0'

PDF生成のライブラリは幾つか存在しているようで、描画の方法にも様々なアプローチがあります。HTMLとCSSのページをそのまま変換してくれるものもありますが、prawnはそれとは異なり、2次元の座標を指定しながら一つ一つ図形や文字、画像などを配置していく形でPDFをレイアウトします。

つまり、後から要素を挟み込んだり、用紙のサイズを変更したりといった変更に(HTMLと比べて)大きな手間がかかります。一方で、用紙上の決まった位置に常に決まった要素を配置するという”かっちりした”レイアウトが得意です。今回は、「帳票」という常に同じ形式で出力されることが期待されるドキュメントを想定してこのgemを選んでいます。実装するドキュメントの種類に応じて柔軟に検討することをお勧めします。また、そもそも「紙が必要なの?」というところから(業務などの)流れを再考するのも(新しいシステムを作ろうとしているなら)やってみても良いかもしれません。

PDFを描画するクラスはPrawn::Documentを拡張して定義します。置き場所は(実際よく悩むのですが…)、controllers/concernsの下としています。

app/controllers/concerns/report_pdf.rb

class ReportPdf < Prawn::Document
  PAGE_WIDTH = 500
  PAGE_HEIGHT = 740

  def initialize
    super :page_size => 'A4',
      :page_layout => :portrait,
      :top_margin => 50,
      :bottom_margin => 50,
      :left_margin => 50,
      :right_margin => 50,
      :compress => true

    stroke_axis
    rectangle [2, PAGE_HEIGHT-2], PAGE_WIDTH-4, PAGE_HEIGHT-4
    stroke
    # draw_text "PDF出力のサンプルです。", at: [4, 700], size: 12
  end
end

works_controller.rbにアクションを定義して、上記クラスのrenderメソッドを呼び出すことでPDFを出力することができます。

def report
  filename = 'sample.pdf'
  pdf = ReportPdf.new.render
  send_data pdf, type: 'application/pdf', filename: filename
end

routes.rbにも忘れずに追記します。

get 'report' => 'works#report'

追加したパスにアクセスすると以下のようにガイド線だけが表示されたPDFがダウンロードされるはずです。

Exported PDF

ここに座標指定で文字や画像、図形などを描画していきます。ただし、このままでは日本語を扱うことができません。 Prawnのエラーメッセージでも指摘されるように、日本語に対応したフォントを明示する必要があります。

文字情報技術促進協議会で配布されているIPAフォントを利用する場合、 以下のような指定になります。他にもGoogle Fontsなどを調べると目的にあったフォントが見つかるかもしれません。

ダウンロードしたフォントを任意のフォルダ(ここではapp/assets/fontsというフォルダを作りました)に置いて、 initializerで以下のように読み込みます。

ipaexm_path = "#{Rails.root}/app/assets/fonts/ipaexm.ttf"
ipapm_spec  = {file: ipaexm_path, font: 'IPAPMincho'}
font_families.update("ipapm" => {normal: ipapm_spec,
                            bold: ipapm_spec,
                            italic: ipapm_spec,
                            bold_italic: ipapm_spec})
font "ipapm"

その後、上記でコメントアウトされていたdraw_textが利用できるようになります。

draw_text "PDF出力のサンプルです。", at: [4, 700], size: 12
Exported PDF

画像を配置したり、任意の図形を描画したりすることも可能です。

image "#{Rails.root}/app/assets/images/file_icon_text_pdf.png", at: [100,600], width: 200
stroke_color 'ff0000'
stroke do
  move_down 600
  horizontal_rule
  line [200,200], [300,150]
end
Exported PDF

(画像は「いらすとや」より)

その他の詳しい使い方は、Prawnの公式マニュアルなどを参照してみてください。

表を書くことに特化した拡張機能のPrawn Tableというgemもあります。複雑な表組みが必要な場合、検討すると良いでしょう。

変更履歴の管理(paper_trail)

gem 'paper_trail', '~> 12.0'

paper_trailは、モデルの変更履歴(生成,更新,削除)を記録してくれます。papertrail(アンダースコアなし)という似たgemが存在するので注意してください。

バージョン履歴を保存するテーブルを作ったりなどするために、以下のコマンドを実行します。

rails generate paper_trail:install
rails db:migrate

バージョン管理を行いたいモデルにhas_paper_trailと記載します。

app/models/page.rb

class Page < ApplicationRecord
  has_paper_trail
end

これだけで、モデルの各操作に対する履歴が保存されるようになります。例えば、最初に「1」という内容で作成されたpageモデルが、続けて「2」「3」と2回更新された場合、以下のように3つのバージョン履歴が残っていて、それぞれの時点で保存されていた値を参照することが可能です。

irb> p = Page.last
irb> p.body
=> "3"
irb> p.versions[2].reify.body
=> "2"
irb> p.versions[1].reify.body
=> "1"
irb> p.versions[0].reify
=> nil

IDをベースに記録されているため、モデルが削除された場合でも、同一IDのモデルを新しく作ってから、versionsを参照すると過去の状態を参照することができます。以下の例では一度削除したモデルを削除直前の状態に戻しています。

irb> p = Page.new(id:3)
irb> p = p.versions.last.reify
irb> p.save
=> true

devise などのgemと組み合わせると「誰が操作したか」という記録も合わせて保管できます。

markdownの導入(redcarpet)

近年、多くのウェブサービスでテキストのマークアップ(修飾)に使われているMarkdown記法のテキストをhtmlにパースしてくれるのが redcarpet です。Gemfileに以下のバージョンを登録しました(Gemfileを更新したらbundleを実行してください、これ以降も同様です)。

Gemfile

gem 'redcarpet', '~> 3.5'

直接Markdownとは関係ないですが、複数行のテキスト入力をするために、bodyフィールドの編集エリアを text_field から text_area に変更します。

app/views/pages/_form.html.erb
<div class="field">
  <%= form.label :body %>
  <%= form.text_area :body %>
</div>

Redcarpetのパーザを使うためのヘルパメソッドを定義します。

app/helpers/pages_helper.rb

  def markdown(text)
    unless @renderer
      @renderer = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
    end
    @renderer.render(text).html_safe
  end

このメソッドでbodyの出力を囲うと、Markdownで保存されたテキストがHTMLとして整形されて出力されます。

app/views/pages/show.html.erb

<p>
  <strong>Body:</strong>
  <%= markdown(@page.body) %>
</p>
redcarpet

画像のリサイズ(image_processing)

ユーザからの画像アップロードを受け付けていると、例えばスマホで撮影したままの画像データそのままだと非常にサイズが大きくなってしまいページの表示が遅くなってしまうなどの弊害があります。また、個別のページでは大きな画像では構わないけれど、一覧表示をする場合に大きなままだとやはり表示速度に悪影響が出てしまう場合もあります。

このような時に、予めリサイズしておいた画像を返す機能がActiveStorageには備わっています。まず、必要なgemをGemfileに追加します。

gem 'image_processing', '~> 1.12'

dockerのビルドが必要です。

その後、以下のようにvariantというメソッドを使ってリサイズされた画像を参照することができます。この指定の場合、100x100pxのサイズに収まる(縦横比は保持したまま)サイズに縮小されて表示されます。

<%= image_tag @page.image.variant(resize_to_limit: [100, 100]) %>

Rails5系では、上記のパラメタが使えない場合があるようです。その場合は、以下の記載で代替することができるようです。

<%= image_tag @page.image.variant(resize: "100x100") %>

アイコンを使ってみる(font-awesome-sass)

メニューの文字やボタンにアイコンがあると、視認性が良くなります。Font Awesomeというアイコンのセットが手軽に利用可能です。有料ですが、無料で利用可能なものも多くあります。

gem 'font-awesome-sass', '~> 5.12'

任意のscssファイルにimport文を追記します。

@import "font-awesome-sprockets";
@import "font-awesome";

iconというメソッドを使ってアイコンをリンクやボタンに配置することが出来るようになります。

<%= link_to icon("fas","calendar")+' Calendar', '#' %>

配備・運用編

ここまでに作成したRailsのプロジェクトをインターネットに公開するための手順を確認します。プロジェクトの規模や目的によって無数の手段があり、どれが正解と言い切るのがとても難しい部分です。筆者自身は、VPSサーバ(仮想のLinuxが動くマシン)の中に、Apacheとpassengerを組み込んで使う方法をよく使いますが、ここではHerokuというサービスを利用してもっとお手軽に(Linuxの基礎知識をあまり必要としない方法で)配備を実現してみます。

Heroku CLIのインストール

公式サイトの手順を参考に手元の環境に一致する方法でインストールを行ってください。macOSの場合、Homebrewを使う方法がおすすめです。

https://devcenter.heroku.com/articles/heroku-cli

ログインと最初の設定

以下のコマンドを打つと、ブラウザ経由でログインをすることができます(事前に登録したユーザIDとパスワード、2段階認証を設定している場合は、それが可能なデバイス等が必要です)。

heroku login

Herokuサーバへのデプロイはgitのリポジトリを介して行います。本書の最初のサンプルでは「–skip-git」を指定してプロジェクトを作成しているため、ここで初期化が必要です。

git init

.gitignoreというファイルをプロジェクト直下に作成し、以下の内容を書き込みます。これも(最初からgitを入れていれば)本来は元から設定済みのものなのですが、本書の構成に合わせて念のため解説しています。

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep

# Ignore uploaded files in development
/storage/*
!/storage/.keep

# Ignore master key for decrypting credentials and more.
/config/master.key

その後、以下の手順でリポジトリにコミットします。

git add .
git commit -am "Initial commit."

Heroku側にアプリケーションを作成します。rails-practice1がアプリの名前になりますが、適宜ご自身のプロジェクトの名前に置き換えて実行してください。

heroku apps:create rails-practice1

以下の手順でアプリケーションを配備します。

heroku git:remote -a rails-practice1
git push heroku main
heroku run rake db:migrate db:seed

開発環境(develop)や、その他のサーバに配備する場合と異なり、Herokuではdb:createは不要です。多くのログが出力されますが、最終的に以下のようなメッセージまで辿り着けば配備成功です。

remote: -----> Launching...
remote:        Released v6
remote:        https://rails-practice1.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.

サーバが立ち上がった後、正常に動作しているかどうか、まずは上記のURL(..herokuapp.com)にアクセスして確認しましょう。開発環境とは異なりエラーが出ていてもセキュリティの観点から、詳細が表示されません。代わりに以下のHerokuコマンドを使って詳細を確認します。

heroku logs

データベースに接続エラーが出る場合、config/database.ymlのusernameとpasswordを削除、または適切な値に更新して対応してください。

開発環境(docker)で見ていたものと同じ画面が..herokuapp.comというHerokuプラットフォームのURLで閲覧できたら配備完了です!

master.keyの設定

開発環境で使っていた秘密鍵は、Herokuのサーバには直接アップロードされません(しないことが推奨されています)。代わりに以下の手順で環境変数として設定します。この設定を行っておくことで、config/credentials.yml.encに保存したその他のAPIキー(ここではAWSの秘密鍵など)をアプリ側から参照することが出来るようになります。

heroku config:set RAILS_MASTER_KEY=`cat config/master.key`

active_storageの設定(AWS S3)

Amazon Web Service(AWS)のアカウントが必要です。

ログイン -> サービス一覧からS3を選択 -> Create Bucket 名前とリージョンと決定し、その他は全てそのままにします。

サービス一覧からIAM(Identity and Access Management)を選択 -> Create User

Access TypeをProgrammatic access AmazonS3FullAccessを持ったグループを作成し、ロールを割り当てます。

作成ウィザードの最後またはSecurity credentialsページから、access_keyとsecret_access_keyを取得してRailsのcredentialsに保管します。

docker compose exec web rails credentials:edit
aws:
   access_key_id: アクセスキー
   secret_access_key: 秘密キー

config/storage.ymlに設定を追加します。regionとbucketは適宜、上記で設定したものに書き換えてください。

amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: ap-northeast-1
  bucket: rails-practice1

config/environments/production.rb を編集して、production環境ではawsを利用する設定にします。

config.active_storage.service = :amazon

s3を操作するためのgemも追加が必要です。

gem "aws-sdk-s3", require: false

mailerの設定(SendGrid)

action_mailerが有効化されていない場合は、config/application.rbを編集して有効化してください(本書のサンプルでは有効化済みのはずです)。

require "action_mailer/railtie"

SendGridのアカウントが必要です。Dashboard -> Settings -> API Keyと進みAPIキーを取得(新規作成)しておいてください。

AWSのキーを追加した際と同じ要領で、credentialsに保存します。

docker compose exec web rails credentials:edit
sendgrid:
   sendgrid_username: apikey
   sendgrid_password: sendgrid1
   sendgrid_api_key: APIキー

config/environments/production.rbに以下のように設定を追加します。

ActionMailer::Base.smtp_settings = {
  :user_name => 'apikey',
  :password => Rails.application.credentials.sendgrid_secret_key,
  :domain => 'rails-practice1.herokuapp.com',
  :address => 'smtp.sendgrid.net',
  :port => 2525,
  :authentication => :plain,
  :enable_starttls_auto => true
}

mailerの設定(AWS SES)

(執筆予定)

トラブルシュート(heroku編)

ここからは個別のエラー対応の記録です。先の手順でも書いた通り、Heroku上でなにかエラーが怒っても”We’re sorry, but something went wrong.”と言われるだけで、その詳細をすぐに確認することができません。一番の基本はサーバ側で何が起こっているかを確認することです。以下のようなコマンドを必要な時にすぐ使えるようになっておくと良いでしょう。

heroku logs
heroku console

起動しない

deviseを入れている時に起こります。

rails zeitwerk:check:
expected file /usr/local/bundle/gems/devise-4.7.1/app/mailers/devise/mailer.rb to define constant Devise::Mailer,

deviseのissueによるとconfig/application.rbでActionMailerを有効化するのが簡単な回避方法のようです。

require "action_mailer/railtie"

プラットフォームが対応していない

RaspberryPiなど、Intel以外のCPUで開発をしていると、IntelのCPUで動作しているHerokuにデプロイができません。エラーメッセージの指示に従ってGemfile.lockにプラットフォームを追記することで回避できます。

docker compose run web bundle lock --add-platform x86_64-linux

SSL(https)経由のアクセスのみ受け付けたい

最近のブラウザは(sのつかない)http経由のアクセスに際して常に警告マークを表示するようになっています。普通のウェブページで特に通信を秘匿する必要がなければ、問題のないことですが、警告が出ているのは見た目が良くありませんし、SSL通信に特別なコストがかかるわけでもないので、常にhttpsでアクセスするように設定してしまうのも一つの方法です。config/environments/production.rb でその指定をすることが出来ます。

config.force_ssl = true

データベースをリセットしたい

運用中のデータが入っているサーバでは絶対に実行しないでください! 開発中に仕様変更や不具合対応を繰り返して、どうしても動作が不安定になってしまうことがあります。その場合、以下のコマンドでHeroku上のPostgreSQLデータベースを完全に初期化してしまうことが可能です。

heroku pg:reset  

一時的にサイトを停止したい

メンテナンスモードに設定することができます(再開する場合は off を指定)。

heroku maintenance:on --remote staging

また、on/offをつけずに実行すると、現在の状態を確認できます。

heroku maintenance --remote staging

補遺

参考資料

更新履歴

本書について

本書は、宮崎県ソフトウェアセンター様主催の集合研修向けリソースとして、6年前に書いた「Ruby on Railsで作る『イベント告知サイト』」を再構成する形で編成しました。

「CC BY 4.0」に則った再利用・再頒布が可能です。

著者紹介:伊藤陽生 株式会社ランバーミルの代表取締役。中小規模のシステム開発を20年近く続けています。 Cから始まりJava、JavaScript、Objective-C、PHP、 Python、SQLなどを主に業務で扱っています。Linuxサーバの構築だったり、ウェブサイトのデザインをしたり、これらの分野の研修講師として働いていたこともあります。技術の移り変わりの 速いこの領域においては、それぞれの経験値そのものよりも、いかに新しい技術にうまくキャッチアップし、チームで共有し、身近な問題の解決に結びつけられるか、という、もう一段抽象的な技能こそが重要なのではないかと考えています。

本書に関するお問い合わせ、誤字・脱字等のご指摘は、下記URLにお願い致します(SNSのメッセージでも大歓迎です)。

https://lumber-mill.co.jp/contact.php