20200604のRailsに関する記事は19件です。

deviseでログインする際の項目を追加する

【開発環境】
macOS Catalina
バージョン 10.15.3
ruby 2.5.1
Rails 5.2.4.2
visual studio code

deviseでログインする際の項目を追加する

deviseでログイン機能を作ると本来はメールアドレス、パスワードだけですが、他の項目のカラムを追加する方法です。

作成したいカラムを追加

今回は例としてcompanyというカラムを追加します。

$ rails g migration AddColumnToUsers company

作成されたマイグレーションファイルを見てみます。
デフォルト値の設定をしないとエラーになるそうなので記載します。

作成日時_add_company_to_users.rb
class AddCompanyToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :company, :string, null: false, default: ''
  end
end

記述が終わったらDBに反映させます。

$ rails db:migrate

ビューにフォームを追加する

作成したカラムに値を入力できるようにフォームを追加します。
deviseのビューを作っていない場合は作ります。

$ rails g devise:views
registrations/new.html.erb
ここを追加 ---------------------------------
  <div class="field">
    <%= f.label :病院名、会社名を入力してください %><br />
    <%= f.text_field :company, autofocus: true, class: "new_inform"%>
  </div>
---------------------------------
  <div class="field">
    <%= f.label :メールアドレスを入力してください %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "new_inform" %>
  </div>

  <div class="field">
    <%= f.label :パスワードを入力してください %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> 文字)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password", class: "new_inform" %>
  </div>

  <div class="field">
    <%= f.label :パスワードをもう一度入力してください %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "new_inform" %>
  </div>

  <div class="actions">
    <%= f.submit "登録する", class: "new_submit" %>
  </div>
<% end %>

追加したカラムをDBへ反映

ストロングパラメーターの設定でconfigure_permitted_parametersメソッドを使用します。

devise\app\controllers\application_controller.rb
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:company])
    end

これでDBに反映することができます。

間違いなどがありましたらご指摘いただければ幸いです。
最後までご覧いただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

配列からpathを作成する

配列からpathを作成する

ルーティングヘルパーを使う方法の他に、パラメータの配列からパスやURLを作成することもできます。
次のようなルーティングを考えてみる

ruby.rb
resources :magazines do
  resources :ads
end

resources :magazinesの中に resources :ads(広告)があります。
magazine_ad_pathを使えば、idを数字で渡す代りに
MagazineとAdのインスタンスをそれぞれ引数として渡すことができます。
これで@magazine@adそれぞれのインスタンス情報を持ったパスを精製できます。

<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>

複数のオブジェクトが集まったセットに対してurl_forを使うことも可能です。
複数のオブジェクトを渡しても、適切なルーティングが自動的に決定されます。

<!--@magazineと@adを引数に渡す -->
<%= link_to 'Ad details', url_for([@magazine, @ad]) %>

上の場合、Railsは@magazineがMagazineであり、@adがAdであることを認識し、それに基づいてmagazine_ad_pathヘルパーを呼び出します。これ自分で認識してくれるのすごいよね。。

link_toなどのヘルパーでも同様にして、完全なurl_for呼び出しの代わりに単にオブジェクトを渡すことが可能です。

<%= link_to 'Ad details', [@magazine, @ad] %>

もし1冊の雑誌にだけリンクしたいのであれば、以下のように書きます。

<%= link_to 'Magazine details', @magazine %>

それ以外のアクションであれば、配列の最初の要素にアクション名を挿入するだけで済みます。

<%= link_to 'Edit Ad', [:edit, @magazine, @ad] %>

これにより、モデルのインスタンスをURLとして扱うことができます。これはリソースフルなスタイルを採用する大きなメリットの1つです。

pathに対してインスタンスを渡すことでRailsが自動的にpathを生成してくれるのはすごいよね

参考 Railsガイド

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最速 rails mail devise ウェルカムメール送信機能実装 一番簡単

【ゴール】

deviseユーザ登録時にmailerを使用してmailの送信(ウェルカムメール)
参考:https://web-camp.io/magazine/archives/19143
参考:https://freesworder.net/rails-mail/

【メリット】

■UXの向上
■アプリケーション完成度向上

【開発環境】

■ Mac OS catalina
■ Ruby on Rails (5.2.4.2)
■ Virtual Box:6.1
■ Vagrant: 2.2.7

【実装】

アプリケーションを作成

mac.terminal
$ rails new mail

$ cd mail

gemfile追加

app内.gemfile
gem 'devise' #deviseの会員登録時にメールを飛ばします
gem 'dotenv-rails' #環境変数に使用します。詳細は後述

ターミナルへ戻り諸々作成

mac.terminal
$ bundle install #gemfileをインストール

$ rials g devise:install #deviseを初期化

$ rails g devise User 

$ rails g devise:views

$ rails g devise:controllers users

DB,migrationfileを編集、下記のコメントアウ外す。

※メールの情報を追いかける為。

db/migrate/xxxxxxxxxx_devise_create_users.rb
# Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

modelを編集

※「:confirmable」のアクセスも追加

user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable ←追加

end

.envファイルをルートディレクトリに追加し、下記を追記

※一応パスワードがあるので「.env」を導入、先のgemfile導入はこの為
※環境変数に埋め込passwordは事前に申請してください

/.env
mail = 'あなたのgamilアドレス'
password = 'パスワード申請が必要'

config/initializers/mail_config.rb作成、編集

※メールの形式を指定
※「user_name」「password」は上記の.envファイルから引っ張ってきています

config/initializers/mail_config.rb
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
  address: 'smtp.gmail.com',
  domain: 'gmail.com',
  port: 587,
  user_name: ENV['mail'],
  password: ENV['password'],
  authentication: 'plain',
  enable_starttls_auto: true
}

view/users/mailer/confirmation_instructions.html編集

confirmation_instructions.html
##任意に変更、メールの内容になります。

<p>Welcome <%= @email %>!</p>

<p>You can confirm your account email through the link below:</p>

##ここまで


<!-- <p><%#= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p> -->
#コメントアウト,今回不要の為

再度ターミナルへ

mac.terminal
$ rails db:migrate

$ rails s -b 0.0.0.0

以上でユーザー登録時にメール送信されているはずです。

action mailer等経由して3時間くらい時間取られましたが
思ったより簡単に実装できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最速 rails mail devise ウェルカムメール送信機能実装 action mailer不要 一番簡単

【ゴール】

deviseユーザ登録時にmailerを使用してmailの送信(ウェルカムメール)
参考:https://web-camp.io/magazine/archives/19143
参考:https://freesworder.net/rails-mail/

【メリット】

■UXの向上
■アプリケーション完成度向上

【開発環境】

■ Mac OS catalina
■ Ruby on Rails (5.2.4.2)
■ Virtual Box:6.1
■ Vagrant: 2.2.7

【実装】

アプリケーションを作成

mac.terminal
$ rails new mail

$ cd mail

gemfile追加

app内.gemfile
gem 'devise' #deviseの会員登録時にメールを飛ばします
gem 'dotenv-rails' #環境変数に使用します。詳細は後述

ターミナルへ戻り諸々作成

mac.terminal
$ bundle install #gemfileをインストール

$ rials g devise:install #deviseを初期化

$ rails g devise User 

$ rails g devise:views

$ rails g devise:controllers users

DB,migrationfileを編集、下記のコメントアウ外す。

※メールの情報を追いかける為。

db/migrate/xxxxxxxxxx_devise_create_users.rb
# Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

modelを編集

※「:confirmable」のアクセスも追加

user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable ←追加

end

.envファイルをルートディレクトリに追加し、下記を追記

※一応パスワードがあるので「.env」を導入、先のgemfile導入はこの為
※環境変数に埋め込passwordは事前に申請してください

/.env
mail = 'あなたのgamilアドレス'
password = 'パスワード申請が必要'

config/initializers/mail_config.rb作成、編集

※メールの形式を指定
※「user_name」「password」は上記の.envファイルから引っ張ってきています

config/initializers/mail_config.rb
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
  address: 'smtp.gmail.com',
  domain: 'gmail.com',
  port: 587,
  user_name: ENV['mail'],
  password: ENV['password'],
  authentication: 'plain',
  enable_starttls_auto: true
}

view/users/mailer/confirmation_instructions.html編集

confirmation_instructions.html
##任意に変更、メールの内容になります。

<p>Welcome <%= @email %>!</p>

<p>You can confirm your account email through the link below:</p>

##ここまで


<!-- <p><%#= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p> -->
#コメントアウト,今回不要の為

再度ターミナルへ

mac.terminal
$ rails db:migrate

$ rails s -b 0.0.0.0

以上でユーザー登録時にメール送信されているはずです。

action mailer等経由して3時間くらい時間取られましたが
思ったより簡単に実装できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はじめてのポートフォリオ(技術<制作過程)

プログラミングを始めて半年を迎えます、上野栞音です。
スクールでは主にRailsアプリの作り方を教わり、現在は株式会社Wilicoでインターンとしてお世話になりながら就活中です。
プログラミングも字書きも不慣れなもんで、至らぬ点がありましたら教えてください。

今回はスクール3ヶ月目のフェーズで作成したポートフォリオについて、雑多にはなりますが色々と書き留めていこうと思います。

ポートフォリオ概要

ToT
github
※かんたんログイン("Signin as a trial user")実装済み。

開発コミュニティ向けのQAサイトを想定して制作しました。
詳細はgithubのREADMEに。

開発経緯と目的

今回学習したかったことは大きく分けると2つ。

:thinking: ユーザーのアクションを経て、どのようなデータが集められるのか。
:thinking: そのデータを基にどのような分析結果を返せるのか。

もともと、ユーザーに評価を付与→チャートで表示の機能は絶対に実装しようと決めていました。
きっかけとしてはAI(データの統計や分析)に興味が湧いたからなので、最初はPythonを使って何かしようと考えていたのですが却下。
理由としては、

  1. 初学者が1ヶ月という期限付きで新しい技術を得てアプリを作っても、強みのあるポートフォリオにはならない気がしたから。
  2. 当時教わっていたRailsについての理解が明らかに浅く、その理解度のまま別の技術に手を出すのが腑に落ちなかったから。

そんなこんなで いくつかサイトをチラ見しつつアプリの企画を練りました。
QAサイトの制作に至った理由としては、評価基準の設け方がパッとイメージできたからです。

DB設計

:bulb:静的(不変的)なデータのみを保管するようにするのがベスト!
というのが今回の学びです。開発はSQLite3、本番はMySQLで実装しています。

はじめに考えていたもの

旧UR図.jpg

ユーザーの評価はユーザーテーブルに、それ以外の評価もコメントやスレッドのアクションに紐づけてアップデートする予定でしたが、途中でこんがらがってスクールのメンターさんに相談。
諸々差し置いて問題点として大きかったのが、この設計だとユーザーが何か操作するたびにアップデートが掛かってしまうため、正常にアップデートされない可能性があることです。(回避方も色々あるみたいですが、もっと掘り下げたいので省略)

後々算出できるデータやユーザーの動作に依存して頻繁にデータが更新されるものは、理由がない限りDBで管理しないようにするため、分析するための材料だけ保管してチャートを描画するタイミングで算出する設計にしました。

最終的な設計

最新UR図.jpg

赤い部分をごっそり消しましたが、時間系の評価については他の評価に比べて算出するステップが一つ多い(2つのテーブルに登録されている登録日を基に差分を算出 → その差分を基に平均値算出)こともあり、集計のスピードを上げるために用途を変えてテーブルを残しています。

基本的な機能の実装

チャート実装を除いた部分です。チャートに時間が割きたかったこともあり、検索機能以外を1週間くらいでスケジュールを組んで実装しました。
さして難しい機能は実装してないのでアピールポイントを挙げると、
:information_desk_person:タグ付け機能と検索機能はgemを使わずに実装しました
改めて調べてみると「gemで出来たのでは…?」なんて思いますが、いい運動(?)になったので結果オーライ:confetti_ball:

以下備考
  • タグ付け機能
    • 同じ意味なのに違う表記(rails, Railsみたいな)のデータが入ると分析の精度が下がってしまうため、新規タグ作成の動作を重くしたかった
    • 基本1つのtext_fieldにカンマ区切りで書き込むようなやり方しか見つからず軽いなぁと悩んだあげく、普通に中間テーブルで結んでフォーム作った方が慣れたやり方だし早そうだと判断。
    • 実装後、Qiitaのタグ付け機能を見て目ぇひん剥きました。なるほど。この手があったか。なるほど…
  • 検索機能
    • 検索対象や解決/未解決フラグごとなど少し細かく条件を指定して検索できるようにしたかった。
    • 改めて調べてみるとアドバンストモードでいけそう。当時触ってみたけど、このモードを理解するより自力で実装した方が早いなと判断。(条件分岐はスクールの応用課題で実装済み、or検索はこのサイトを参考に実装。)

チャート実装

開発経緯の通り、今できる事→これやりたい!軸で企画を立ててここまで来たので、為せば成る精神で詳しい実装の目論見はほぼありませんでした。
調べてみるものの、当時jsに馴染みが無さ過ぎて悶絶寸前。

:fearful:何をどうすれば これが出来るんですか…?
自分で考えたアプリのくせに、ここに来て教室の隅で静かに絶望してました。

この時の学びとして大きかったのは、
:raised_hands:分からない、初めて触るものは一度触ってみる大切さ

何も分からず嘆いていた最中、スクールの同期生に相談したらchart.jsのcodepenを教えてくれました。
ここで少し触ってみた瞬間、chart.jsが面白いほど読める。要因としては、すでに完成しているコードを触れることが大きかったと思います。
どの値がどの軸のデータに影響しているかや、どの値がどのデザインに影響しているかなどが直観的に分かり、ここに配列渡せば勝ちじゃん!とゴールを定める事が出来ました。

ちょっと無謀にも思いますが、試行錯誤も含めて工数を割くために基本的な機能 頑張ったので潔く実装に移って良かったなと思います(結果論)。

いざ、尋常にチャート実装

jsファイルとのデータの受け渡しは、gem 'gon'で行っています。
json形式に変換するのが基本ですが学習目的に含まれていないのと、これから実装するチャートの工数が読みきれずスピードを重視したかったので採用しました。

大体こう。

  1. UserModelのロジックでチャートに渡す配列を計算するメソッド作成、Controllerで呼び出す
  2. UsersControllerでgonに渡す
  3. Viewでgonのタグ→jsファイルに渡す
  4. jsファイル→canvasタグに渡してチャート描画

※自分が流れを掴むために搔い摘んだものです。gon周りは特にもう少し検証しながら理解を深めたい。

UserModelのロジックは、大体こう。

コードがぼちぼち長いため、流れだけ伝わりますようにといった感じで書きます:pray:
代わりと言っては何ですが行単位でGithubのリンクを貼るので、気になる方はご覧ください。
評価基準ごとにまとめます。

:cactus: Questioner/Tags, Answer/Tags

ドーナツチャートの2項目です。少しデータを引っ張るロジックが違うだけなので、Questioner/Tagsを例にします。

  1. 対象のユーザーが投稿したIssueに紐づくタグを算出
  2. タグの名前だけを格納した配列を作る
  3. 対象のユーザーが投稿したIssueに紐づくタグを算出
  4. タグの割合だけ格納した配列を作る

:cactus: Time to response, Time to solved, Total

バーチャートとレーダーチャートの項目です。
データを引っ張ってくるテーブルが違うだけなので、Time to responseを例にします。
ドーナツチャート以外は基本この流れです。細かい処理は省きます。

  1. 全ユーザーの平均値を算出
    ・ユーザーの動作に依存してグラフの階級を変えるため、このデータを基に基準になる値を算出します。
  2. チャートの諸々を決めるのに使う値を算出
    ・返すのは各ユーザー平均値の [ 最小値, 最大値, それを基にした階級幅 ]。
    ・投稿されたIssueが1つも無い場合はfalseで返して例外処理。
  3. 2を基に境界値を算出
    ・〇秒~〇秒のユーザーはスコア1、〇秒~〇秒のユーザーはスコア2… の〇だけ入ったような配列です。
  4. 3の境界値を基に
  5. 各階級に何人ユーザーが含まれるかを算出して配列を作りながら
  6. 各階級に対象のユーザーが含まれるか否かを0,1で算出して配列を作る
  7. 6の配列を基に、ユーザーのスコアを算出
    ・3点の場合、[0,0,1,0,0,0,0,0,0,0] → each_with_indexで回すと2番目の値が1になる → 2+1でスコア算出
  8. 5,7をControllerに返す
  9. レーダーチャートの配列だけControllerで作ります。
    ・ ControllerからModelのメソッドを呼び出す際、Model上に配列を作ろうとすると呼び出すたびに配列がリセットされるためです。
    ・[ 5(チャートに渡す値), 7(ユーザーのスコア) ] の配列が返り値なので、この配列の[1]を拾って配列を作ります。

:cactus: ほか

Time to response とほぼ同じなので、差分だけまとめます。

1.平均値
平均値が割り出せるほど絶対値が大きくなかったため合計値を算出しています。チャートの値が0ばかりになって変わり映えしなかった:frowning2:

3. 階級の算出
階級の誤差をスコア1に寄せるため、呼び出すメソッドを変えています。
評価基準によってスコアが高くなる条件が分岐するのが肝で、
Time to ~ → 1.平均値算出の結果が低いと高スコアcalculate_evaluation_datas_sort_by_max
それ以外 → 1.合計値算出の結果が高いと高スコアcalculate_evaluation_datas_sort_by_min
大きく違う点としては、階級を決める基準が 最大値 → 最小値 であること(こちら基準で命名してます)と、配列をreverseするタイミングです。

リファクタリング

制作期間が 2/15~3/15 くらいだったのですが、3月頭にβ版をデプロイして色んな方にレビューを頂きました。ありがとうございました:sob:

UserModelのメソッド
1. 最初は全てControllerに記述していたメソッドを
2. Modelに移行して
3. 共通するロジックをメソッド化(最新版)

Viewの描画まわり
1. HTMLの部分テンプレートでscriptタグをrenderしていたのを
2. jsファイルに移行して(リンクはcomment_tags)
3. 一つのファイルにまとめて共通するロジックをメソッド化(最新版)

チャートまとめ

このポートフォリオにおいて最大の学びでもあるのですが、
:wave:手を動かせば必ず答えは見つかる!
と確信を得られた制作物でした。
今扱えるパラメータを読み、それを基にロジックを組み、足りなければパラメータを送る。これを自分自身の経験をもって得られたのはとても貴重な学びだったなと思います。

全てのチャートを最低限実装するまでの1週間くらいはコンソール画面にかじりついて模索する日々でしたが、ロジックができた瞬間の達成感が最高すぎて何だかんだ楽しかったです:v:

さいごに

ポートフォリオについて調べていると「こういうのが有利!」ばかりで自分と同じレベル感の記事が上手いこと見つからず、望んでいた判断材料では無かったので書いてみました。

もちろん まだ改善の余地があるアプリとは思いますが、キリがないので一旦区切りにしようかなと思います。

これからポートフォリオを制作する方の目に留まり、少しでもインスピレーションの助けになれば幸いです:ramen:
あと色んな方のこんな感じの記事見たいので是非書いてください:eyes:

ありがとうございました!

参照
Ransackで簡単に検索フォームを作る73のレシピ
railsで複数ワードでの検索機能(or)とマイナス検索機能(-)を実装してみる
chart.jsのcodepen
gem 'gon'

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データベースの制約について

NOT NULL制約▼

テーブルの属性値にNULL(空の値)が入ることを許さない制約のこと。

Railsではマイグレーションファイルでカラムを追加するときにnull: falseと記述することで
NOT NULL制約を設定することができます。

一意性制約▼

ユニークで他とは異なるという意味。
制約を設定した場合、同じ値を設定できなくなる。(例 ▶︎ メールアドレスやパスワードなど)

Railsでは、add_indexメソッドの中でunique: trueという引数を指定することで、一意性制約をかけるためのマイグレーションファイルを作成できます。

主キー制約▼

主キー制約は、主キーである属性値が必ず存在してかつ重複していないことを保証する制約です。主キーに対してNOT NULL制約と一意性制約を両方設定するのと同義になります。

Railsでテーブルを作成する際、主キー制約は元々実装されています。Railsでは主キーはidカラムとして自動で作成されます。つまりidカラムの値は重複しないようにできています。

外部キー制約▼

外部キーに対応するレコードが必ずないといけないという制約。

Railsでは、マイグレーションファイルで外部キーとなるカラムを追加するときにforeign_key: trueと記述することで外部キー制約を設定することができます。

なお、外部キーを設定する場合のカラムの型は、references型を採用すること。

references型を採用しなければ外部キーは設定されないので注意が必要な点と、references型を設定することでカラム名の_idの記述をしなくて良いのでコードの可読性が上がる点を忘れる事があるので要注意!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

follow_redirect!って何をfollowしてるの?: Railsチュートリアル躓きポイント - 7章

ユーザー登録作成時のテストを書いております

/sample_app/test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.

  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
  end

follow_redirect!とはなんぞ!

このメソッドは、POSTリクエストを送信した結果を見て、指定されたリダイレクト先に移動するメソッドです。(Railsチュートリアル)

なんとなく挙動は想像できる

わからないこと

指定されたリダイレクト先ってなんぞ?

検証

このままテストを実行してみると
rails test > Green

post users_pathに対応するコントローラーの挙動を参照すると

/sample_app/app/controllers/users_controller.rb
.
.
.
  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

この中のredirect_to @user > redirect_to root_pathに変更してみると

rails test > Red

 FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x000055e3f2c61b10 @name="UsersSignupTest">, 1.4677946789997804]
 test_valid_signup_information#UsersSignupTest (1.47s)
        expecting <"users/show"> but rendering with <["static_pages/home", 
...

リダイレクト先が"static_pages/home"になってるよと

結論

follow_redirect!は、
"POSTリクエストを送信した結果を見て"、
つまり対応するコントローラ内で明示されたリダイレクトの挙動に従っているようだ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル躓きポイント - 7章:エラーメッセージに対するテスト

Railsチュートリアル 6.0の7章
7.3.4 失敗時のテスト
最後の演習で躓いた記録

演習内容

"リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.25にテンプレートを用意しておいたので、参考にしてください。"

解答

assert_selectを使用して、
HTML構造に不適切なログイン時固有の要素が存在するかどうかを調べるとよい。

直前に作成したパーシャルの内容
(この内容がnew.html.erbに挿入されている)

/sample_app/app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

@user.errorsが存在する場合に、
特定のclass("alert" & "alert-danger"), id("error_explanation")を含むdivタグが展開される

ブラウザに渡される最終的なHTMLにこれが含まれていれば良いわけなので、
assert_select 'セレクタ(class: div.class, id: div#id)'
でそれを確認する

完成形は以下

/sample_app/test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  test "invalid signup information" do
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name: "",
                                         email: "user@invalid",
                                         password: "foo",
                                         password_confirmation: "bar"} }
    end
    assert_template 'users/new'
    assert_select 'div.alert'
    assert_select 'div.alert-danger'
    assert_select 'div#error_explanation'
  end

end

assert_selectで検証している構造を、
Google Chromeのデベロッパーツールで可視的に示すと
スクリーンショット 2020-06-04 17.50.58.png

以上です

わかってないこと(自己学習用)

" "と' 'の使い分け
文字列であることを明示的に示す場合に""が好まれるような気がするが、
セレクタの指定は' '

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エラーメッセージに対するテスト: Railsチュートリアル躓きポイント - 7章

Railsチュートリアル 6.0の7章
7.3.4 失敗時のテスト
最後の演習で躓いた記録

演習内容

"リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.25にテンプレートを用意しておいたので、参考にしてください。"

解答

assert_selectを使用して、
HTML構造に不適切なログイン時固有の要素が存在するかどうかを調べるとよい。

直前に作成したパーシャルの内容
(この内容がnew.html.erbに挿入されている)

/sample_app/app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

@user.errorsが存在する場合に、
特定のclass("alert" & "alert-danger"), id("error_explanation")を含むdivタグが展開される

ブラウザに渡される最終的なHTMLにこれが含まれていれば良いわけなので、
assert_select 'セレクタ(class: div.class, id: div#id)'
でそれを確認する

完成形は以下

/sample_app/test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  test "invalid signup information" do
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name: "",
                                         email: "user@invalid",
                                         password: "foo",
                                         password_confirmation: "bar"} }
    end
    assert_template 'users/new'
    assert_select 'div.alert'
    assert_select 'div.alert-danger'
    assert_select 'div#error_explanation'
  end

end

assert_selectで検証している構造を、
Google Chromeのデベロッパーツールで可視的に示すと
スクリーンショット 2020-06-04 17.50.58.png

以上です

わかってないこと(自己学習用)

" "と' 'の使い分け
文字列であることを明示的に示す場合に""が好まれるような気がするが、
セレクタの指定は' '

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Error】本番環境でアプリが表示されない

概要

手順通り、デプロイ作業を行なったのに、本番環境で表示されない:sob:
という事象に陥って1時間近く試行錯誤した結果、単純な回答だったため自分の戒めのため備忘録として残します:bow_tone1:

事象

ブラウザで http://<サーバに紐付けたElastic IP>:3000/を入力しても表示されない:sob:
スクリーンショット 2020-06-04 16.08.27.png

何かの間違い??と思い、リロードしてもロードが続いた結果、変わらず。。

検証

①ターミナル(ECサーバ)でエラーを確認

リポジトリ名(ECサーバ)
$ less log/unicorn.stderr.log

上記コマンドを入力した結果表示されたのが以下のエラーです
スクリーンショット 2020-06-04 16.14.18.png

確認すべきは、3行目の(Mysql2::Error::ConnectionError)です。
Mysqlは起動してるってこと??

②Mysqlの状態を確認

リポジトリ名(ECサーバ)
$ sudo service mysqld status

#コマンド入力後、下記の表示されたら起動中
mysqld (pid  8621) is running...

よし!問題なし。。ではなぜ??

③AWSでポートの確認

スクリーンショット 2020-06-04 16.28.29.png

ポート範囲の入力漏れ:tired_face:

原因

AWSにあるセキュリティグループのポート設定時に入力漏れがありました。
インバウンドルール追加時に下記の内容を入力する必要があります。

【タイプ】カスタムTCPルール
【プロトコル】TCP
【ポート範囲】3000  ←私、忘れました:expressionless:
【ソース】カスタム 0.0.0.0/0

無事、ブラウザに画面が表示されました:sparkles:
※最初はCSSが反映されていないため、ビュー崩れが起こっていると思います!

参考

本番環境デプロイ時のコマンド集
https://qiita.com/15grmr/items/7ad36caa82a0fa27c4bd

デプロイ時に対峙したエラーとその対応
https://qiita.com/sho012b/items/54fcd932ff8c76cdcd05

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecによるTDDでRailsAPIを実装してみた。part3

はじめに

この記事はpart3となります。もしも、part1, part2を見られていない方はそちらからご覧ください。(すごく長いです)

↓part1
https://qiita.com/yoshi_4/items/6c9f3ced0eb20131903d
↓part2
https://qiita.com/yoshi_4/items/963bd1f5397caf8d7d67

このpart3ではpart2で実装したuser認証を使って、createアクションなどの認証をしている場合のみに使えるアクションを実装していきます。今回のゴールはcreate, update, destroyアクションを実装する事です。では初めていきます。

createアクション

createエンドポイント追加

まずは、エンドポイントを追加していきます。そしてその前に一旦テストを書きます。

spec/routing/articles_spec.rb
  it 'should route articles create' do
    expect(post '/articles').to route_to('articles#create')
  end

createアクションははhttpリクエストがpostなので、getではなくpostで書いていきます。

$ bundle exec rspec spec/routing/articles_spec.rb

No route matches "/articles"

というふうに出るので、routingを追加していきます

エンドポイント実装

config/routes.rb
  resources :articles, only: [:index, :show, :create]
$ bundle exec rspec spec/routing/articles_spec.rb

テストを実行して通ることを確認します。

そして、次はcontrollerのテストを書いていきます。

createアクション実装

spec/controllers/articles_controller_spec.rb
  describe '#create' do
    subject { post :create }
  end
end

この記述を末尾に追加します。

そして、part2で定義したforbidden_requestsを使って認証がうまくいかないときのテストも書いていきます

spec/controllers/articles_controller_spec.rb
  describe '#create' do
    subject { post :create }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid parameters provided' do

    end
  end

このforbidden_rquestsでは403が返ってくることを期待しているテストを実行します。

$ rspec spec/controllers/articles_controller_spec.rb

すると以下のようなメッセージが返って来ます
The action 'create' could not be found for ArticlesController
createアクションが見つからないというふうに言われているので、定義していきます。

app/controllers/articles_controller.rb
  def create

  end

これでもう一度テストを実行して全て通ることを確認します。
テストが通ったということはきちんと認証が効いていることを表しています。

では、createアクションを実装するためにテストを書いていきます。

spec/controllers/articles_controller_spec.rb
    context 'when authorized' do
      let(:access_token) { create :access_token }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
      context 'when invalid parameters provided' do
        let(:invalid_attributes) do
          {
            data: {
              attributes: {
                title: '',
                content: '',
              }
            }
          }
        end

        subject { post :create, params: invalid_attributes }

        it 'should return 422 status code' do
          subject
          expect(response).to have_http_status(:unprocessable_entity)
        end

        it 'should return proper error json' do
          subject
          expect(json['errors']).to include(
            {
              "source" => { "pointer" => "/data/attributes/title" },
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/content"},
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/slug"},
              "detail" => "can't be blank"
            }
          )
        end
      end

      context 'when success request sent' do

      end
    end

テストを追加しました。いっぺんにたくさん追加しましたが、一つ一つは既にやって来たことと被っている部分も多いです。

追加したテストは、when authorizedなので、認証は成功した場合、をテストして来ます。テストしていく項目はそれぞれ、
when invalid parameters provided
should return 422 status code
should return proper error json

を追加しています。parameterが正しい場合は後で書きます。

parameterがからの場合、can't be blankが返ってくることを期待しています。
sourceのpointerはどこでエラーが出ているのかを示しています。今回は全てをからの文字列にしているので、全てからcan't be blankが返ってくることを期待しています。

テストを実行します。二つテストが失敗します。
expected the response to have status code :unprocessable_entity (422) but it was :no_content (204)

一つ目は、unprocessable(処理ができない)というレスポンスが返ってくることを期待していますが、no_contentが帰って来ています。no_contentはcreateaが正常に実行された時に返したいので、後で修正します。

unexpected token at ''

二つ目はJSON.parseはからの文字列ではエラーが出てしまうので、そのエラーです。

では、controllerに実装をしていき、エラーを解消していきます。

app/controllers/articles_controller.rb
  def create
    article = Article.new(article_params)
    if article.valid?
      #we will figure that out
    else
      render json: article, adapter: :json_api,
        serializer: ActiveModel::Serializer::ErrorSerializer,
        status: :unprocessable_entity
    end
  end

  private

  def article_params
    ActionController::Parameters.new
  end

ActionController::Parametersのインスタンスを作成しているのは、これによって、StrongParameterが使えるからです。ActionController::Parametersのインスタンスメソッドである、permitや、requireが使えるようになります。permitやrequireを使えば、もしも形式的に期待しているものと違ったり、違うkeyで何かparameterが送られて来た時に、その不要な部分を切り捨てる事ができます。

renderにadapterを指定していますが、これは形式を指定しています。このadapterを指定しなかった場合は、defaultでattributesというものが指定されています。今回は、json_apiという方を使っています。以下はその違いを例で表示しています。Railsのactive_model_serializerについて学ぶ100DaysOfCodeチャレンジ10日目(Day10:#100DaysOfCode)からコピーさせてもらいました。

attributes

[
    {
        "id": 1,
        "name": "中島 光",
        "email": "rhianna_walsh@maggio.net",
        "birthdate": "2016-05-02",
        "birthday": "2016/05/02"
    }
  ]
}

json_api

{
    "data": [
        {
            "id": "1",
            "type": "contacts",
            "attributes": {
                "name": "中島 光",
                "email": "rhianna_walsh@maggio.net",
                "birthdate": "2016-05-02",
                "birthday": "2016/05/02"
            }
        }
   ]
}

今回はapiに適しているjson_apiを使います。

テストを実行し、通る事を確認します。

次にparameterが正しい場合のテストを書いていきます。

spec/controllers/articles_controller_spec.rb
      context 'when success request sent' do
        let(:access_token) { create :access_token }
        before { request.headers['authorization'] = "Bearer #{access_token.token}" }
        let(:valid_attributes) do
          {
            'data' => {
              'attributes' => {
                'title' => 'Awesome article',
                'content' => 'Super content',
                'slug' => 'awesome-article',
              }
            }
          }
        end

        subject { post :create, params: valid_attributes }

        it 'should have 201 status code' do
          subject
          expect(response).to have_http_status(:created)
        end

        it 'should have proper json body' do
          subject
          expect(json_data['attributes']).to include(
            valid_attributes['data']['attributes']
          )
        end

        it 'should create article' do
          expect { subject }.to change{ Article.count }.by(1)
        end
      end

正しいtokenと、正しいparameterを入れています。これでテストを実行します。

expected the response to have status code :created (201) but it was :unprocessable_entity (422)

undefined method `[]' for nil:NilClass

`Article.count` to have changed by 1, but was changed by 0

三つのテストがそれぞれこのように失敗すると思います。
これらは正しい失敗をしているので、実際にただしいparameterの場合のcontrollerの実装をしていきます。

app/controllers/articles_controller.rb
  def create
    article = Article.new(article_params)
    article.save!
    render json: article, status: :created
  rescue
    render json: article, adapter: :json_api,
      serializer: ActiveModel::Serializer::ErrorSerializer,
      status: :unprocessable_entity
  end

  private

  def article_params
    params.requrie(:data).require(:attributes).
      permit(:title, :content, :slug) ||
    ActionController::Parameters.new
  end

次にcreateをこのように編集していきます。
rescueを用いて、エラーが出た時に、renderでエラーを飛ばすようにしています。

article_paramsでは、:dataの中の:attributesの中の:title,:content,:slugしか取得しないというような条件を設けることで、この指定された形式以外では全て弾くようにしています。

これでテストを実行すると全て通ります。

さらに一つリファクタリングをします。

app/controllers/articles_controller.rb
  rescue
    render json: article, adapter: :json_api,
      serializer: ActiveModel::Serializer::ErrorSerializer,
      status: :unprocessable_entity
  end

このActiveModel::Serializer::ErrorSerializer,が長いので、これを他の場所で違うクラスに継承して、短く記述できるようにします。

app/serializers/error_serializer.rbを作成します

app/serializers/error_serializer.rb
class ErrorSerializer < ActiveModel::Serializer::ErrorSerializer; end

このように継承させます。

app/controllers/articles_controller.rb
  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

そして、先ほどの長い記述をすっきりさせる事ができます。
一応テストを実行して、失敗していないかを確認します。

これでarticleをcreateするアクションの実装は完了です。

updateアクション

updateエンドポイント追加

ではエンドポイントの追加から再びしていきます。まずはテストを書いていきます。

spec/routing/articles_spec.rb
  it 'should route articles show' do
    expect(patch '/articles/1').to route_to('articles#update', id: '1')
  end

毎回のようにエンドポイントのテストを書いていきます。showアクションはhttpリクエストが、patchもしくはputなので、そのどちらかを使います。

テストを実行して、正しくエラーが出ることを確認します。

config/routes.rb
  resources :articles, only: [:index, :show, :create, :update]

updateを追加して、テストが通ることを確認します。

updateアクション追加

では次にcontroller#updateアクションのテストを書いていきます。

spec/controllers/articles_controller_spec.rb
  describe '#update' do
    let(:article) { create :article }

    subject { patch :update, params: { id: article.id } }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when authorized' do
      let(:access_token) { create :access_token }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
      context 'when invalid parameters provided' do
        let(:invalid_attributes) do
          {
            data: {
              attributes: {
                title: '',
                content: '',
              }
            }
          }
        end

        it 'should return 422 status code' do
          subject
          expect(response).to have_http_status(:unprocessable_entity)
        end

        it 'should return proper error json' do
          subject
          expect(json['errors']).to include(
            {
              "source" => { "pointer" => "/data/attributes/title" },
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/content"},
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/slug"},
              "detail" => "can't be blank"
            }
          )
        end
      end

      context 'when success request sent' do
        let(:access_token) { create :access_token }
        before { request.headers['authorization'] = "Bearer #{access_token.token}" }
        let(:valid_attributes) do
          {
            'data' => {
              'attributes' => {
                'title' => 'Awesome article',
                'content' => 'Super content',
                'slug' => 'awesome-article',
              }
            }
          }
        end

        subject { post :create, params: valid_attributes }

        it 'should have 201 status code' do
          subject
          expect(response).to have_http_status(:created)
        end

        it 'should have proper json body' do
          subject
          expect(json_data['attributes']).to include(
            valid_attributes['data']['attributes']
          )
        end

        it 'should create article' do
          expect { subject }.to change{ Article.count }.by(1)
        end
      end
    end
  end

updateアクションがcreateアクションと違う部分は、リクエストの種類と既にデータベースにupdateの
対象となるarticleがある、という状況のみなので、最初にarticleを作成しているところと、リクエストを定義している部分以外はcreateのテストをコピーして来ているだけです。

これでテストを実行します。

The action 'update' could not be found for ArticlesController

このようなエラーが出ると思います。なので、updateを実際に定義していきます。

app/controllers/articles_controller.rb
  def update
    article = Article.find(params[:id])
    article.update_attributes!(article_params)
    render json: article, status: :ok
  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

もはや目新しいことはないので、説明は割愛します。

これでテストを実行して全て通ることを確認します。
createとupdateの違いさえわかっていればほとんど違いがないという事がわかると思います。そして、テストもほとんど同じものを使い回す事ができます。

しかし、ここで少しだけ問題があります。それは、リクエスト次第で、誰のarticleでもupdateできてしまいます。勝手にupdateされては困ります。なのでそこを修正していきます。

どのように修正していくかというと、現時点、userとarticleが関連性を持っていないために、起きている問題なので、userとarticleにassociationを追加していきます。

その前にassociationを設定して、期待する値が返ってくることをテストしていきます。

spec/controllers/articles_controller_spec.rb
   describe '#update' do
+    let(:user) { create :user }
     let(:article) { create :article }
+    let(:access_token) { user.create_access_token }

     subject { patch :update, params: { id: article.id } }

@ -140,8 +142,17 @@ describe ArticlesController do
       it_behaves_like 'forbidden_requests'
     end

+    context 'when trying to update not owned article' do
+      let(:other_user) { create :user }
+      let(:other_article) { create :article, user: other_user }
+
+      subject { patch :update, params: { id: other_article.id } }
+      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
+
+      it_behaves_like 'forbidden_requests'
+    end

     context 'when authorized' do
-      let(:access_token) { create :access_token }
       before { request.headers['authorization'] = "Bearer #{access_token.token}" }

       context 'when invalid parameters provided' do
         let(:invalid_attributes) do

このようにテストを追加しました。userと繋がったarticleを作り、認証までしています。

新しく追加したテスト項目で何をしているかというと、他のuserのarticleをupdateしようとした時にちゃんとforbidden_requestsが返ってくるかどうかを確認しています。

これでテストを実行すると

undefined method user=

というようなメッセージで失敗します。これはアソシエーションができていない証拠なので、次にアソシエーションを設定していきます。

app/models/article.rb
  belongs_to :user
app/models/user.rb
  has_many :articles, dependent: :destroy

そして、二つのモデルをつなげるためにはarticleモデルにuser_idを持たせる必要があるので、追加します。

$ rails g migration AddUserToArticles user:references

$ rails db:migrate

これでアソシエーション自体は実装する事ができました。なので、それを使って、controllerの記述を変更していきます。

app/controllers/articles_controller.rb
  def update
    article = current_user.articles.find(params[:id])
    article.update_attributes!(article_params)
    render json: article, status: :ok
  rescue ActiveRecord::RecordNotFound
    authorization_error
  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

記述で変わったところはfindするuserをcurrent_userで呼び出しているところです。これにより、ログインしているユーザーのみからfindする事ができます。
そして、指定されたidがcurrent_userのarticleになかった場合ActiveRecord::RecordNotFoundがraiseされるので、その時ようにrescueして、認証専用のauthorization_errorを出すようにしています。

また、createでも、誰のarticleをcreateするというふうに記述し、user_idをarticle
に持たせたいので、少し変更を加えます。

app/controllers/articles_controller.rb
   def create
-    article = Article.new(article_params)
+    article = current_user.articles.build(article_params)

そして、factorybotにもアソシエーションの記述を足していきます。

spec/factories/articles.rb
FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "My article #{n}"}
    sequence(:content) { |n| "The content of article #{n}"}
    sequence(:slug) { |n| "article-#{n}"}
    association :user
  end
end

association :model_name
とすると、自動的にモデルのidを定義してくれます。

これでテストを実行すると通ると思います。
次はdestroyアクションに移っていきます。

destroyアクション

destroyエンドポイント追加

まずはエンドポイントを追加するためにテストを書いていきます。

spec/routing/articles_spec.rb
  it 'should route articles destroy' do
    expect(delete '/articles/1').to route_to('articles#destroy', id: '1')
  end

テストを実行すると以下のメッセージが出ます
No route matches "/articles/1"

なので、ルーティングを編集していきます。

config/routes.rb
  resources :articles

onlyオプションで指定せずに全てを設定します。
これでルーティングのテストは通ります。

次にcontrollerのテストを追加します。

spec/controllers/articles_controller_spec.rb
  describe '#delete' do
    let(:user) { create :user }
    let(:article) { create :article, user_id: user.id }
    let(:access_token) { user.create_access_token }

    subject { delete :destroy, params: { id: article.id } }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when trying to remove not owned article' do
      let(:other_user) { create :user }
      let(:other_article) { create :article, user: other_user }

      subject { delete :destroy, params: { id: other_article.id } }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }

      it_behaves_like 'forbidden_requests'
    end

    context 'when authorized' do
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }

      it 'should have 204 status code' do
        subject
        expect(response).to have_http_status(:no_content)
      end

      it 'should have empty json body' do
        subject
        expect(response.body).to be_blank
      end

      it 'should destroy the article' do
        article
        expect{ subject }.to change{ user.articles.count }.by(-1)
      end
    end
  end

このテストのコードはほとんどがupdateのテストをコピーして使いまわしています。
内容は特に新しいことはありません。テストを実行します。

The action 'destroy' could not be found for ArticlesController

destroyアクションはまだ定義していないので、このエラーが正しいです。ではcontroller
を実装していきます。

destroyアクション追加

app/controllers/articles_controller.rb
  def destroy
    article = current_user.articles.find(params[:id])
    article.destroy
    head :no_content
  rescue
    authorization_error
  end

単純にcurrent_userの中の指定されたarticleをdestroyをしています。

これで、テストを実行します。

これで通ったら、全てが終了です。長い間お付き合いいただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecによるTDDでRailsAPIを実装してみた。part3 -認証ありのアクション実装-

はじめに

この記事はpart3となります。もしも、part1, part2を見られていない方はそちらからご覧ください。(すごく長いです)

↓part1
https://qiita.com/yoshi_4/items/6c9f3ced0eb20131903d
↓part2
https://qiita.com/yoshi_4/items/963bd1f5397caf8d7d67

このpart3ではpart2で実装したuser認証を使って、createアクションなどの認証をしている場合のみに使えるアクションを実装していきます。今回のゴールはcreate, update, destroyアクションを実装する事です。では初めていきます。

createアクション

createエンドポイント追加

まずは、エンドポイントを追加していきます。そしてその前に一旦テストを書きます。

spec/routing/articles_spec.rb
  it 'should route articles create' do
    expect(post '/articles').to route_to('articles#create')
  end

createアクションははhttpリクエストがpostなので、getではなくpostで書いていきます。

$ bundle exec rspec spec/routing/articles_spec.rb

No route matches "/articles"

というふうに出るので、routingを追加していきます

エンドポイント実装

config/routes.rb
  resources :articles, only: [:index, :show, :create]
$ bundle exec rspec spec/routing/articles_spec.rb

テストを実行して通ることを確認します。

そして、次はcontrollerのテストを書いていきます。

createアクション実装

spec/controllers/articles_controller_spec.rb
  describe '#create' do
    subject { post :create }
  end
end

この記述を末尾に追加します。

そして、part2で定義したforbidden_requestsを使って認証がうまくいかないときのテストも書いていきます

spec/controllers/articles_controller_spec.rb
  describe '#create' do
    subject { post :create }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid parameters provided' do

    end
  end

このforbidden_rquestsでは403が返ってくることを期待しているテストを実行します。

$ rspec spec/controllers/articles_controller_spec.rb

すると以下のようなメッセージが返って来ます
The action 'create' could not be found for ArticlesController
createアクションが見つからないというふうに言われているので、定義していきます。

app/controllers/articles_controller.rb
  def create

  end

これでもう一度テストを実行して全て通ることを確認します。
テストが通ったということはきちんと認証が効いていることを表しています。

では、createアクションを実装するためにテストを書いていきます。

spec/controllers/articles_controller_spec.rb
    context 'when authorized' do
      let(:access_token) { create :access_token }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
      context 'when invalid parameters provided' do
        let(:invalid_attributes) do
          {
            data: {
              attributes: {
                title: '',
                content: '',
              }
            }
          }
        end

        subject { post :create, params: invalid_attributes }

        it 'should return 422 status code' do
          subject
          expect(response).to have_http_status(:unprocessable_entity)
        end

        it 'should return proper error json' do
          subject
          expect(json['errors']).to include(
            {
              "source" => { "pointer" => "/data/attributes/title" },
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/content"},
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/slug"},
              "detail" => "can't be blank"
            }
          )
        end
      end

      context 'when success request sent' do

      end
    end

テストを追加しました。いっぺんにたくさん追加しましたが、一つ一つは既にやって来たことと被っている部分も多いです。

追加したテストは、when authorizedなので、認証は成功した場合、をテストして来ます。テストしていく項目はそれぞれ、
when invalid parameters provided
should return 422 status code
should return proper error json

を追加しています。parameterが正しい場合は後で書きます。

parameterがからの場合、can't be blankが返ってくることを期待しています。
sourceのpointerはどこでエラーが出ているのかを示しています。今回は全てをからの文字列にしているので、全てからcan't be blankが返ってくることを期待しています。

テストを実行します。二つテストが失敗します。
expected the response to have status code :unprocessable_entity (422) but it was :no_content (204)

一つ目は、unprocessable(処理ができない)というレスポンスが返ってくることを期待していますが、no_contentが帰って来ています。no_contentはcreateaが正常に実行された時に返したいので、後で修正します。

unexpected token at ''

二つ目はJSON.parseはからの文字列ではエラーが出てしまうので、そのエラーです。

では、controllerに実装をしていき、エラーを解消していきます。

app/controllers/articles_controller.rb
  def create
    article = Article.new(article_params)
    if article.valid?
      #we will figure that out
    else
      render json: article, adapter: :json_api,
        serializer: ActiveModel::Serializer::ErrorSerializer,
        status: :unprocessable_entity
    end
  end

  private

  def article_params
    ActionController::Parameters.new
  end

ActionController::Parametersのインスタンスを作成しているのは、これによって、StrongParameterが使えるからです。ActionController::Parametersのインスタンスメソッドである、permitや、requireが使えるようになります。permitやrequireを使えば、もしも形式的に期待しているものと違ったり、違うkeyで何かparameterが送られて来た時に、その不要な部分を切り捨てる事ができます。

renderにadapterを指定していますが、これは形式を指定しています。このadapterを指定しなかった場合は、defaultでattributesというものが指定されています。今回は、json_apiという方を使っています。以下はその違いを例で表示しています。Railsのactive_model_serializerについて学ぶ100DaysOfCodeチャレンジ10日目(Day10:#100DaysOfCode)からコピーさせてもらいました。

attributes

[
    {
        "id": 1,
        "name": "中島 光",
        "email": "rhianna_walsh@maggio.net",
        "birthdate": "2016-05-02",
        "birthday": "2016/05/02"
    }
  ]
}

json_api

{
    "data": [
        {
            "id": "1",
            "type": "contacts",
            "attributes": {
                "name": "中島 光",
                "email": "rhianna_walsh@maggio.net",
                "birthdate": "2016-05-02",
                "birthday": "2016/05/02"
            }
        }
   ]
}

今回はapiに適しているjson_apiを使います。

テストを実行し、通る事を確認します。

次にparameterが正しい場合のテストを書いていきます。

spec/controllers/articles_controller_spec.rb
      context 'when success request sent' do
        let(:access_token) { create :access_token }
        before { request.headers['authorization'] = "Bearer #{access_token.token}" }
        let(:valid_attributes) do
          {
            'data' => {
              'attributes' => {
                'title' => 'Awesome article',
                'content' => 'Super content',
                'slug' => 'awesome-article',
              }
            }
          }
        end

        subject { post :create, params: valid_attributes }

        it 'should have 201 status code' do
          subject
          expect(response).to have_http_status(:created)
        end

        it 'should have proper json body' do
          subject
          expect(json_data['attributes']).to include(
            valid_attributes['data']['attributes']
          )
        end

        it 'should create article' do
          expect { subject }.to change{ Article.count }.by(1)
        end
      end

正しいtokenと、正しいparameterを入れています。これでテストを実行します。

expected the response to have status code :created (201) but it was :unprocessable_entity (422)

undefined method `[]' for nil:NilClass

`Article.count` to have changed by 1, but was changed by 0

三つのテストがそれぞれこのように失敗すると思います。
これらは正しい失敗をしているので、実際にただしいparameterの場合のcontrollerの実装をしていきます。

app/controllers/articles_controller.rb
  def create
    article = Article.new(article_params)
    article.save!
    render json: article, status: :created
  rescue
    render json: article, adapter: :json_api,
      serializer: ActiveModel::Serializer::ErrorSerializer,
      status: :unprocessable_entity
  end

  private

  def article_params
    params.requrie(:data).require(:attributes).
      permit(:title, :content, :slug) ||
    ActionController::Parameters.new
  end

次にcreateをこのように編集していきます。
rescueを用いて、エラーが出た時に、renderでエラーを飛ばすようにしています。

article_paramsでは、:dataの中の:attributesの中の:title,:content,:slugしか取得しないというような条件を設けることで、この指定された形式以外では全て弾くようにしています。

これでテストを実行すると全て通ります。

さらに一つリファクタリングをします。

app/controllers/articles_controller.rb
  rescue
    render json: article, adapter: :json_api,
      serializer: ActiveModel::Serializer::ErrorSerializer,
      status: :unprocessable_entity
  end

このActiveModel::Serializer::ErrorSerializer,が長いので、これを他の場所で違うクラスに継承して、短く記述できるようにします。

app/serializers/error_serializer.rbを作成します

app/serializers/error_serializer.rb
class ErrorSerializer < ActiveModel::Serializer::ErrorSerializer; end

このように継承させます。

app/controllers/articles_controller.rb
  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

そして、先ほどの長い記述をすっきりさせる事ができます。
一応テストを実行して、失敗していないかを確認します。

これでarticleをcreateするアクションの実装は完了です。

updateアクション

updateエンドポイント追加

ではエンドポイントの追加から再びしていきます。まずはテストを書いていきます。

spec/routing/articles_spec.rb
  it 'should route articles show' do
    expect(patch '/articles/1').to route_to('articles#update', id: '1')
  end

毎回のようにエンドポイントのテストを書いていきます。showアクションはhttpリクエストが、patchもしくはputなので、そのどちらかを使います。

テストを実行して、正しくエラーが出ることを確認します。

config/routes.rb
  resources :articles, only: [:index, :show, :create, :update]

updateを追加して、テストが通ることを確認します。

updateアクション追加

では次にcontroller#updateアクションのテストを書いていきます。

spec/controllers/articles_controller_spec.rb
  describe '#update' do
    let(:article) { create :article }

    subject { patch :update, params: { id: article.id } }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when authorized' do
      let(:access_token) { create :access_token }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
      context 'when invalid parameters provided' do
        let(:invalid_attributes) do
          {
            data: {
              attributes: {
                title: '',
                content: '',
              }
            }
          }
        end

        it 'should return 422 status code' do
          subject
          expect(response).to have_http_status(:unprocessable_entity)
        end

        it 'should return proper error json' do
          subject
          expect(json['errors']).to include(
            {
              "source" => { "pointer" => "/data/attributes/title" },
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/content"},
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/slug"},
              "detail" => "can't be blank"
            }
          )
        end
      end

      context 'when success request sent' do
        let(:access_token) { create :access_token }
        before { request.headers['authorization'] = "Bearer #{access_token.token}" }
        let(:valid_attributes) do
          {
            'data' => {
              'attributes' => {
                'title' => 'Awesome article',
                'content' => 'Super content',
                'slug' => 'awesome-article',
              }
            }
          }
        end

        subject { post :create, params: valid_attributes }

        it 'should have 201 status code' do
          subject
          expect(response).to have_http_status(:created)
        end

        it 'should have proper json body' do
          subject
          expect(json_data['attributes']).to include(
            valid_attributes['data']['attributes']
          )
        end

        it 'should create article' do
          expect { subject }.to change{ Article.count }.by(1)
        end
      end
    end
  end

updateアクションがcreateアクションと違う部分は、リクエストの種類と既にデータベースにupdateの
対象となるarticleがある、という状況のみなので、最初にarticleを作成しているところと、リクエストを定義している部分以外はcreateのテストをコピーして来ているだけです。

これでテストを実行します。

The action 'update' could not be found for ArticlesController

このようなエラーが出ると思います。なので、updateを実際に定義していきます。

app/controllers/articles_controller.rb
  def update
    article = Article.find(params[:id])
    article.update_attributes!(article_params)
    render json: article, status: :ok
  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

もはや目新しいことはないので、説明は割愛します。

これでテストを実行して全て通ることを確認します。
createとupdateの違いさえわかっていればほとんど違いがないという事がわかると思います。そして、テストもほとんど同じものを使い回す事ができます。

しかし、ここで少しだけ問題があります。それは、リクエスト次第で、誰のarticleでもupdateできてしまいます。勝手にupdateされては困ります。なのでそこを修正していきます。

どのように修正していくかというと、現時点、userとarticleが関連性を持っていないために、起きている問題なので、userとarticleにassociationを追加していきます。

その前にassociationを設定して、期待する値が返ってくることをテストしていきます。

spec/controllers/articles_controller_spec.rb
   describe '#update' do
+    let(:user) { create :user }
     let(:article) { create :article }
+    let(:access_token) { user.create_access_token }

     subject { patch :update, params: { id: article.id } }

@ -140,8 +142,17 @@ describe ArticlesController do
       it_behaves_like 'forbidden_requests'
     end

+    context 'when trying to update not owned article' do
+      let(:other_user) { create :user }
+      let(:other_article) { create :article, user: other_user }
+
+      subject { patch :update, params: { id: other_article.id } }
+      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
+
+      it_behaves_like 'forbidden_requests'
+    end

     context 'when authorized' do
-      let(:access_token) { create :access_token }
       before { request.headers['authorization'] = "Bearer #{access_token.token}" }

       context 'when invalid parameters provided' do
         let(:invalid_attributes) do

このようにテストを追加しました。userと繋がったarticleを作り、認証までしています。

新しく追加したテスト項目で何をしているかというと、他のuserのarticleをupdateしようとした時にちゃんとforbidden_requestsが返ってくるかどうかを確認しています。

これでテストを実行すると

undefined method user=

というようなメッセージで失敗します。これはアソシエーションができていない証拠なので、次にアソシエーションを設定していきます。

app/models/article.rb
  belongs_to :user
app/models/user.rb
  has_many :articles, dependent: :destroy

そして、二つのモデルをつなげるためにはarticleモデルにuser_idを持たせる必要があるので、追加します。

$ rails g migration AddUserToArticles user:references

$ rails db:migrate

これでアソシエーション自体は実装する事ができました。なので、それを使って、controllerの記述を変更していきます。

app/controllers/articles_controller.rb
  def update
    article = current_user.articles.find(params[:id])
    article.update_attributes!(article_params)
    render json: article, status: :ok
  rescue ActiveRecord::RecordNotFound
    authorization_error
  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

記述で変わったところはfindするuserをcurrent_userで呼び出しているところです。これにより、ログインしているユーザーのみからfindする事ができます。
そして、指定されたidがcurrent_userのarticleになかった場合ActiveRecord::RecordNotFoundがraiseされるので、その時ようにrescueして、認証専用のauthorization_errorを出すようにしています。

また、createでも、誰のarticleをcreateするというふうに記述し、user_idをarticle
に持たせたいので、少し変更を加えます。

app/controllers/articles_controller.rb
   def create
-    article = Article.new(article_params)
+    article = current_user.articles.build(article_params)

そして、factorybotにもアソシエーションの記述を足していきます。

spec/factories/articles.rb
FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "My article #{n}"}
    sequence(:content) { |n| "The content of article #{n}"}
    sequence(:slug) { |n| "article-#{n}"}
    association :user
  end
end

association :model_name
とすると、自動的にモデルのidを定義してくれます。

これでテストを実行すると通ると思います。
次はdestroyアクションに移っていきます。

destroyアクション

destroyエンドポイント追加

まずはエンドポイントを追加するためにテストを書いていきます。

spec/routing/articles_spec.rb
  it 'should route articles destroy' do
    expect(delete '/articles/1').to route_to('articles#destroy', id: '1')
  end

テストを実行すると以下のメッセージが出ます
No route matches "/articles/1"

なので、ルーティングを編集していきます。

config/routes.rb
  resources :articles

onlyオプションで指定せずに全てを設定します。
これでルーティングのテストは通ります。

次にcontrollerのテストを追加します。

spec/controllers/articles_controller_spec.rb
  describe '#delete' do
    let(:user) { create :user }
    let(:article) { create :article, user_id: user.id }
    let(:access_token) { user.create_access_token }

    subject { delete :destroy, params: { id: article.id } }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when trying to remove not owned article' do
      let(:other_user) { create :user }
      let(:other_article) { create :article, user: other_user }

      subject { delete :destroy, params: { id: other_article.id } }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }

      it_behaves_like 'forbidden_requests'
    end

    context 'when authorized' do
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }

      it 'should have 204 status code' do
        subject
        expect(response).to have_http_status(:no_content)
      end

      it 'should have empty json body' do
        subject
        expect(response.body).to be_blank
      end

      it 'should destroy the article' do
        article
        expect{ subject }.to change{ user.articles.count }.by(-1)
      end
    end
  end

このテストのコードはほとんどがupdateのテストをコピーして使いまわしています。
内容は特に新しいことはありません。テストを実行します。

The action 'destroy' could not be found for ArticlesController

destroyアクションはまだ定義していないので、このエラーが正しいです。ではcontroller
を実装していきます。

destroyアクション追加

app/controllers/articles_controller.rb
  def destroy
    article = current_user.articles.find(params[:id])
    article.destroy
    head :no_content
  rescue
    authorization_error
  end

単純にcurrent_userの中の指定されたarticleをdestroyをしています。

これで、テストを実行します。

これで通ったら、全てが終了です。長い間お付き合いいただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】turbolinksを無効化する方法

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

完全に無効化する方法

1.Gemを無効化

Gemfile
# コメントアウトする
# gem 'turbolinks', '~> 5'
ターミナル
$ bundle update

2.application.jsを編集

=を削除する。

application.js
// 変更前
//= require turbolinks 

// 変更後
// require turbolinks 

3. application.html.slimを編集

'data-turbolinks-track': 'reload'を削除する。

application.html.slim
/ 変更前
= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'

/ 変更後
= stylesheet_link_tag    'application', media: 'all'
= javascript_include_tag 'application'

部分的に無効化する方法

1.JavaScriptを編集する方法

~.jsファイルの場合

~.js
$(document).on('turbolinks:load', function() {
  // turbolinksを無効化したい処理
});

~.coffeeファイルの場合

~.coffee
$(document).on 'turbolinks:load', -> 
  # turbolinksを無効化したい処理

2.リンクを編集する方法

①link_toに属性を追加する場合

~html.slim
= link_to '', root_path, 'data-turbolinks': false

②divで囲う場合

~html.slim
div data-turbolinks='false'
  = link_to '', root_path
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Google Mapに複数マーカーを表示し、クリックしたら吹き出しを出す方法

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
ログイン機能実装
Google Map表示
Gocoding APIで緯度経度を算出

実装

1.コントローラーを編集

users_controller.rb
def index
  @users = User.all
  gon.users = User.all
end

2.ビューを編集

users/index.html.slim
#map style='height: 500px; width: 500px;'

- google_api = "https://maps.googleapis.com/maps/api/js?key=#{ ENV['GOOGLE_MAP_API'] }&callback=initMap".html_safe
script{ async src=google_api }

javascript:

  let map;
  let marker = []; // マーカーを複数表示させたいので、配列化
  let infoWindow = []; // 吹き出しを複数表示させたいので、配列化
  let markerData = gon.users; // コントローラーで定義したインスタンス変数を変数に代入

  function initMap() {
    geocoder = new google.maps.Geocoder()

    map = new google.maps.Map(document.getElementById('map'), {
      center: { lat: 35.6585, lng: 139.7486 }, // 東京タワーを中心に表示させている
      zoom: 12,
    });

    // 繰り返し処理でマーカーと吹き出しを複数表示させる
    for (var i = 0; i < markerData.length; i++) {
      let id = markerData[i]['id']

      // 各地点の緯度経度を算出
      markerLatLng = new google.maps.LatLng({
        lat: markerData[i]['latitude'],
        lng: markerData[i]['longitude']
      });

      // 各地点のマーカーを作成
      marker[i] = new google.maps.Marker({
        position: markerLatLng,
        map: map
      });

      // 各地点の吹き出しを作成
      infoWindow[i] = new google.maps.InfoWindow({
        // 吹き出しの内容
        content: markerData[i]['address']
      });

      // マーカーにクリックイベントを追加
      markerEvent(i);
    }
  }

  // マーカーをクリックしたら吹き出しを表示
  function markerEvent(i) {
    marker[i].addListener('click', function () {
      infoWindow[i].open(map, marker[i]);
    });
  }

吹き出しの内容ををリンクにしたい場合は下記の様に記述する。

// 各ユーザーのIDを変数化
let id = markerData[i]['id']

infoWindow[i] = new google.maps.InfoWindow({
  // <a>タグでリンクを作成
  content: `<a href='/users/${ id }'>${ markerData[i]['address'] }</a>`
});

注意

turbolinksを無効化しないと地図が切り替わらないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6のAPIモードでGraphQLを使う方法(エラー対策も含む)

はじめに

railsでGraphQLを使うための方法をまとめてみました。

必要最低限の実装にとどめ、GraphQLの動作確認ができることをメインに紹介します。(+αでエラーの対策を記載しました。)
そのため、Queryの設定はしていません。

【参考】
Rails APIモードで始めるGraphQL
【Rails】graphql-rubyでAPIを作成

アプリ作成

$ rails new demo-graphql --api       #オプションで--apiをつけるとAPIモードでappを作成

Gemfileにgraphql, graphiql-rails(開発環境で使用)を追加

Gemfile
gem 'graphql'    #追加

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'graphiql-rails'   #開発環境に追加
end

コメントアウトを外す

config/application.rb
require "sprockets/railtie"    #コメントアウトを外す

ターミナル で下記を実行

ターミナル
$ bundle install
$ rails generate graphql:install   #GraphQLに関するファイルが作成されます

routes.rbに下記を追加

routes.rb
Rails.application.routes.draw do
  if Rails.env.development?
    # add the url of your end-point to graphql_path.
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" 
  end
  post '/graphql', to: 'graphql#execute'  #ここはrails generate graphql:installで自動生成される
end

動作確認

rails sした後にhttp://localhost:3000/graphiqlに接続。(graphqlではなくgraphiqlと"i"が入っているので注意してください。上記のroutesで変更しています。)
下記の画面になれば接続成功!
スクリーンショット 2020-06-03 19.59.07.png

試しにQueryを送ると設定をしていないのでエラーが返ってきます。
スクリーンショット 2020-06-03 19.59.38.png

GraphiQLに接続できない場合(Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show)

どの記事を見ても上記の設定しか紹介されておらず、そのまま実装してもエラーが出たので対策方法を紹介します。
APIモードで作成するとassetsが作成されないのでそれが原因のようです。
[Rails6][sprockets4.0.0]Sprocketsを無効にするためにapp/assetsフォルダ以下を削除すると、Sprockets::Railtie::ManifestNeededErrorが出た

どちらかをやれば大丈夫なので開発環境に合わせて選択してください。

エラー内容↓
スクリーンショット 2020-06-04 10.03.49.png

対策① assets/config/manifest.jsを作成する

assets/config/manifest.jsファイルを作成し、下記を記載する。

assets/config/manifest.js
//= link graphiql/rails/application.css
//= link graphiql/rails/application.js

対策② gem 'sprocket'のバージョン3.7.2に下げる

sprockets4.0.0になってからassetsファイルがないとエラーが出るようです。

なのでバージョンを下げると解消できます。

Gemfile
gem 'sprockets', '~> 3.7.2'  #追加する
ターミナル
$ bundle update

終わりに

今回はrails6でGraphQLを使えるようにすることにフォーカスを当てました。
実際にQueryを作成してデータを受け取るところは他の記事を参考にしてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSに手動でデプロイする

AWSに入って手動でデプロイしたときのコマンドです。

前提
- EC2インスタンス作成済
- RDS DB(mysql)作成済

# updateして必要なものを入れる
sudo yum -y update
sudo yum install -y git curl unzip gcc openssl-devel readline-devel mysql-devel

# git config
git config --global user.name "xxxxxx"
git config --global user.email "xxxxxxxx@email.com"

# timezone
sudo timedatectl set-timezone Asia/Tokyo

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
~/.rbenv/bin/rbenv init

mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

rbenv install 2.6.6
rbenv global 2.6.6

gem install bundler

sudo curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install -y nodejs

git clone https://github.com/branch_name/xxx.git
bundle install --without test development

bin/rails db:migrate RAILS_ENV=production
bundle exec rake assets:precompile RAILS_ENV=production

bin/rails s -e production
bin/rails db -e production

vi Gemfile # uniron gem 追加
bundle install

vi config/unicorn.conf.rb
# ==========================================
  # set lets
  $worker  = 2
  $timeout = 30
  $app_dir = "/home/ec2-user/branch_name" <= edit
  $listen  = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dir
  $pid     = File.expand_path 'tmp/pids/unicorn.pid', $app_dir
  $std_log = File.expand_path 'log/unicorn.log', $app_dir
  # set config
  worker_processes  $worker
  working_directory $app_dir
  stderr_path $std_log
  stdout_path $std_log
  timeout $timeout
  listen  $listen
  pid $pid
  # loading booster
  preload_app true
  # before starting processes
  before_fork do |server, worker|
    defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
    old_pid = "#{server.config[:pid]}.oldbin"
    if old_pid != server.pid
      begin
        Process.kill "QUIT", File.read(old_pid).to_i
      rescue Errno::ENOENT, Errno::ESRCH
      end
    end
  end
  # after finishing processes
  after_fork do |server, worker|
    defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
  end
# ==========================================

sudo amazon-linux-extras install nginx1.12
nginx -v
cd /etc/nginx/conf.d/
cd /var/lib
sudo chmod -R 775 nginx
sudo service nginx start
cd /etc/nginx/conf.d/
sudo vi default.conf

# ==========================================
error_log  /home/ec2-user/branch_name/log/nginx.error.log; <= edit
access_log /home/ec2-user/branch_name/log/nginx.access.log; <= edit
# max body size
client_max_body_size 2G;
upstream app_server {
  # for UNIX domain socket setups
  server unix:/home/ec2-user/branch_name/tmp/sockets/.unicorn.sock fail_timeout=0; <= edit
}
server {
  listen 80;
  server_name IPアドレス; <= edit
  # nginx so increasing this is generally safe...
  keepalive_timeout 5;
  # path for static files
  root /home/ec2-user/branch_name/public;
  # page cache loading
  try_files $uri/index.html $uri.html $uri @app;
  location @app {
    # HTTP headers
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }
  # Rails error pages
  error_page 500 502 503 504 /500.html;
  location = /500.html {
    root /home/ec2-user/branch_name/public; <= edit
  }
}
# ==========================================

# 構文検査
nginx -t
sudo service nginx start
sudo nginx -s reload
sudo chmod -R +r /home/ec2-user/branch_name/public
sudo chmod -R +rwx /home/ec2-user/branch_name/tmp
sudo nginx -s reload
bundle exec unicorn_rails -c /home/ec2-user/circleci_study/config/unicorn.conf.rb -D -E production
ps -ef | grep unicorn | grep -v grep
chmod 701 /home/ec2-user

最後の方でずっと「forbidden 403」権限エラーが出ていて、すぐにはわかりませんでした。
権限つけたら見ることができるようになりました。

参考文献
個人開発のための Webサービス公開マニュアル - 秀和システム あなたの学びをサポート
難波 聖一 著
秀和システム (2019/12/25)
415ページ

参考ページ
(デプロイ編②)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまで - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

findとfind_byの違い

両者の違い

find
-検索条件として指定できるのは『id』のみ
-データを複数取得できる
-検索結果が無い場合は例外を発生させて処理を中断
find_by
-検索時に複数の条件を指定できる。また、『id』以外ののカラムでも検索できる
-取得できるデータは最初にマッチした1件のみ
-検索結果が無い場合nilを返すため処理を中断されない

使い方(find)

基本形はモデル名にfindを繋げて引数を指定。
それによって該当するモデルのテーブルからデータを検索できる。

モデル名.find(引数)

指定できる引数は
『数値型』、『文字列の数字』、『数字のリスト』、『数字の配列』
該当する『id』のデータが見つからない場合は例外(RecordNotFound)が発生する

【例】Userモデルからidが1のデータを返す

User.find(1)

【例】Userモデルからidが1,3,5のデータを配列で返す

User.find(1, 3, 5)

使い方(find_by)

モデル名に引数を繋げて条件を1つ以上指定する。
検索できる条件は『カラム名』、『検索条件』
データが見つからない場合nilを返す。

【例】Userモデルからnameカラムが"佐藤"とマッチした最初のデータを取得

User.find_by(name: "佐藤")

【例】Userモデルからnameカラムが"佐藤"で且つadminカラム(管理者)がtrueの最初のデータを取得

User.find_by(name: "佐藤", admin: true)

【例】Userテーブルから作成日が1週間以内のデータを1件取得

User.find_by("created_at > ?", 1.weeks.ago)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerfile for Rails6のベストプラクティスを解説

本記事の趣旨

令和時代のRails運用
こちらのスライドに掲載されている以下のDockerfileが、キャッシュやマルチステージビルドを利用したベストプラクティスとして参考になりました。

Dockerfile
# syntax = docker/dockerfile:experimental

# Node.jsダウンロード用ビルドステージ
FROM ruby:2.6.5 AS nodejs

WORKDIR /tmp

# Node.jsのダウンロード
RUN curl -LO https://nodejs.org/dist/v12.14.1/node-v12.14.1-linux-x64.tar.xz
RUN tar xvf node-v12.14.1-linux-x64.tar.xz
RUN mv node-v12.14.1-linux-x64 node

FROM ruby:2.6.5

# nodejsをインストールしたイメージからnode.jsをコピーする
COPY --from=nodejs /tmp/node /opt/node
ENV PATH /opt/node/bin:$PATH

# アプリケーション起動用のユーザーを追加
RUN useradd -m -u 1000 rails
RUN mkdir /app && chown rails /app
USER rails

# yarnのインストール
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /home/rails/.yarn/bin:/home/rails/.config/yarn/global/node_modules/.bin:$PATH

# ruby-2.7.0でnewした場合を考慮
RUN gem install bundler

WORKDIR /app

# Dockerのビルドステップキャッシュを利用するため
# 先にGemfileを転送し、bundle installする
COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/

RUN bundle config set app_config .bundle
RUN bundle config set path .cache/bundle
# mount cacheを利用する
RUN --mount=type=cache,uid=1000,target=/app/.cache/bundle \
    bundle install && \
    mkdir -p vendor && \
    cp -ar .cache/bundle vendor/bundle
RUN bundle config set path vendor/bundle

RUN --mount=type=cache,uid=1000,target=/app/.cache/node_modules \
    bin/yarn install --modules-folder .cache/node_modules && \
    cp -ar .cache/node_modules node_modules

COPY --chown=rails . /app

RUN --mount=type=cache,uid=1000,target=/app/tmp/cache bin/rails assets:precompile

# 実行時にコマンド指定が無い場合に実行されるコマンド
CMD ["bin/rails", "s", "-b", "0.0.0.0"]

ここで使用されているBuildkitなどの要素について、勉強した内容をまとめたいと思います。

これらの方法により、開発効率の向上を実感しましたので、RailsとDockerを学習中の方のご参考になればと思います。

(投稿者はDockerを勉強中で、実務は未経験ですので、気になる点がありましたらコメントでご指摘をお願いします。)

参考記事

Dockerfileを改善するためのBest Practice 2019年版

Docker Buildにおけるリードタイム短縮のための3つの改善ポイント

開発環境

  • Mac OS X 10.15.4
  • Docker 19.03.8
  • Docker Desktop for Mac 2.3.0.3
  • Ruby 2.6.5 Rails 6.0.2

Dockerfileの解説

上記のDockerfileの要点を見ていきます。
1行目の# syntax =という部分は後述するBuildkitに関する記述です。

その次の

# Node.jsダウンロード用ビルドステージ
FROM ruby:2.6.5 AS nodejs

WORKDIR /tmp

# Node.jsのダウンロード
RUN curl -LO https://nodejs.org/dist/v12.14.1/node-v12.14.1-linux-x64.tar.xz
RUN tar xvf node-v12.14.1-linux-x64.tar.xz
RUN mv node-v12.14.1-linux-x64 node

ここでは、Railsに必要なNode.jsをインストールしています。

  • tmpに移動
  • node-v12.14.1-linux-x64.tar.xzをダウンロード、展開
  • node-v12.14.1-linux-x64をnodeにリネーム

の結果、/tmp/node(本体), /tmp/node-v12.14.1-linux-x64.tar.xz(不要)
が生成されます。

最終的なDockerイメージを軽量にするために、必要なnode本体だけを残す必要があります。Dockerはレイヤー構造で履歴が残っているため、ただ単にRUN rm node-v12.14.1-linux-x64.tar.xzとしても意味がないようです。

そこで、マルチステージビルドを利用しています。

マルチステージビルド

マルチステージビルドは、1つのDockerfileに複数のステージを分けて記述します。
例のDockerfileでは、FROM行が2箇所、つまり2つのステージがあります。

FROM ruby:2.6.5 AS nodejs
...
FROM ruby:2.6.5

1つ目のAS nodejsの記載で、ステージにnodejsと名付けています。これによって、2つ目のステージで、

# nodejsをインストールしたイメージからnode.jsをコピーする
COPY --from=nodejs /tmp/node /opt/node
ENV PATH /opt/node/bin:$PATH

上でインストールしたtmp/nodeだけをコピーする事ができます。

今回の場合は、これによって節約できるのは15MBほどですが、Goのようなコンパイル言語で、ビルド結果のファイルだけを次のステージに渡すと、かなりの軽量化ができるようです。

ちなみに、2つ目のステージのyarnインストールは、ファイルそのものではなく、install.shをダウンロードして実行しているだけで、不要なファイルが残らないため、このままで問題ないのだと思います。

# yarnのインストール
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /home/rails/.yarn/bin:/home/rails/.config/yarn/global/node_modules/.bin:$PATH

ユーザーの追加

# アプリケーション起動用のユーザーを追加
RUN useradd -m -u 1000 rails
RUN mkdir /app && chown rails /app
USER rails

ここでは、コンテナ内にユーザーを追加しています。デフォルトのrootユーザーのままでは、ホストとファイルを共有する際に権限の問題が発生するようです。

ただ、Docker for Macの場合、その問題は起こらないので、この設定は省略しても良いかもしれません。
(非rootユーザーにすると、vimを使いたい時にapt-getができないなど、色々と困る場面もありましたので..)

bundle install

WORKDIR /app

# Dockerのビルドステップキャッシュを利用するため
# 先にGemfileを転送し、bundle installする
COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/

作業ディレクトリに必要なパッケージ管理ファイルを置いています。その後、

RUN bundle config set app_config .bundle
RUN bundle config set path .cache/bundle

まずbundle configコマンドを使用して、installするpathを設定しています。

bundle config

例として、bundle config set path vendor/bundleと設定しておくと、bundle installの際に、bundle install --path vendor/bundleとパスを指定した事と同じになります。

installの際に、--pathを指定する方法は非推奨となったようなので、今後はbundle configを使いましょう。

Bundlerでビルドオプションを指定する

Cache Mount

RUN bundle config set path .cache/bundle
# mount cacheを利用する
RUN --mount=type=cache,uid=1000,target=/app/.cache/bundle \
    bundle install && \
    mkdir -p vendor && \
    cp -ar .cache/bundle vendor/bundle
RUN bundle config set path vendor/bundle

まずbundleのinstall先を.cache/bundleに設定します。

続く--mount=type=cachetarget=/app/.cache/bundleという記載が、Cache Mountを利用している部分です。
この記載を含むRUN命令の中では、targetに指定したpath(ここでは/app/.cache/bundle)の中身が、ホスト内に保存され、次回以降のbuildでcacheとして利用されるようになります。

したがって、直後のbundle install.cache/bundleにインストールされたgemは、ホスト内部に保存されています。
このままだと、コンテナ内にgemがない状態になってしまうため、続けてvendorディレクトリを作り、そこに.cacheディレクトリから中身をコピーしています。

最後にbundle config set path vendor/bundleでpathを指定することで、bundlerがvendor/bundleを読みに行ってくれるようになります。

やや回りくどい気もしますが、これによってbuild時間が劇的に改善しました。こちらによると、

RUN --mount=type=cache 命令をうまく活用すると,従来のdocker buildより33倍以上速いビルドも可能です.

私の環境ではbuildのたびにbundle installで300秒以上かかっていました。cacheがあれば、build時のbundle installは変更差分のみですぐ終わるので、気軽にbuildできます。

Buildkit

上述のCache Mountを使うためには、Buildkitでbuildをする必要があります。

Buildkitとは、dockerのイメージビルドを便利にしてくれるビルドツールキットです。こちらにあるように、ビルドのそれぞれの過程ごとにかかった時間を表示してくれたりします。
他にもビルドの並列実行など、たくさんの機能があるようです。

BuildKit によるイメージ構築

Buildkitの導入

主に2つの方法があります。

  • 環境変数DOCKER_BUILDKIT=1を設定する。
  • 試験機能モードを有効にすることでdocker buildxコマンドを使う(Docker 19.03以上)。

1つ目は、DOCKER_BUILDKIT=1 docker build .のように環境変数を指定する簡単な方法です。

2つ目は、buildxというプラグインを利用する方法で、buildkitの全ての機能が有効になるとのことです。config.json (デフォルトでは~/.docker/config.json) に次のように指定します。

~/.docker/config.json
{
    "experimental": "enabled"
}

これにより、環境変数なしでdocker buildx build .のようにビルドを実行する事ができます。
BuildKitによりDockerとDocker Composeで外部キャッシュを使った効率的なビルドをする方法

Buildkit Cache Mountの利用

--mountは新しい構文のため、Dockerfileの1行目に次の記述をする必要があります。

# syntax = docker/dockerfile:experimental

Cacheの削除

docker builder prune

以上がbuildkitの使い方です。
簡単な設定をするだけで良いので、cacheを使わない場合も、取り入れてみるといいかもしれません。

yarn install

RUN --mount=type=cache,uid=1000,target=/app/.cache/node_modules \
    bin/yarn install --modules-folder .cache/node_modules && \
    cp -ar .cache/node_modules node_modules

yarn installも同じくCache Mountを使います。

asset precompile

COPY --chown=rails . /app

RUN --mount=type=cache,uid=1000,target=/app/tmp/cache bin/rails assets:precompile

最後にホストのファイルを全てコピーして、Cache Mountを利用してアセットをプリコンパイルします。

開発環境と本番環境でさらにステージを分けて、本番環境でのみプリコンパイルを行うなどの設定も考えられます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerイメージを快適にビルド for Rails6

本記事の趣旨

令和時代のRails運用
こちらのスライドに掲載されている以下のDockerfileが、キャッシュやマルチステージビルドを利用したベストプラクティスとして参考になりました。

Dockerfile
# syntax = docker/dockerfile:experimental

# Node.jsダウンロード用ビルドステージ
FROM ruby:2.6.5 AS nodejs

WORKDIR /tmp

# Node.jsのダウンロード
RUN curl -LO https://nodejs.org/dist/v12.14.1/node-v12.14.1-linux-x64.tar.xz
RUN tar xvf node-v12.14.1-linux-x64.tar.xz
RUN mv node-v12.14.1-linux-x64 node

FROM ruby:2.6.5

# nodejsをインストールしたイメージからnode.jsをコピーする
COPY --from=nodejs /tmp/node /opt/node
ENV PATH /opt/node/bin:$PATH

# アプリケーション起動用のユーザーを追加
RUN useradd -m -u 1000 rails
RUN mkdir /app && chown rails /app
USER rails

# yarnのインストール
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /home/rails/.yarn/bin:/home/rails/.config/yarn/global/node_modules/.bin:$PATH

# ruby-2.7.0でnewした場合を考慮
RUN gem install bundler

WORKDIR /app

# Dockerのビルドステップキャッシュを利用するため
# 先にGemfileを転送し、bundle installする
COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/

RUN bundle config set app_config .bundle
RUN bundle config set path .cache/bundle
# mount cacheを利用する
RUN --mount=type=cache,uid=1000,target=/app/.cache/bundle \
    bundle install && \
    mkdir -p vendor && \
    cp -ar .cache/bundle vendor/bundle
RUN bundle config set path vendor/bundle

RUN --mount=type=cache,uid=1000,target=/app/.cache/node_modules \
    bin/yarn install --modules-folder .cache/node_modules && \
    cp -ar .cache/node_modules node_modules

COPY --chown=rails . /app

RUN --mount=type=cache,uid=1000,target=/app/tmp/cache bin/rails assets:precompile

# 実行時にコマンド指定が無い場合に実行されるコマンド
CMD ["bin/rails", "s", "-b", "0.0.0.0"]

ここで使用されているBuildkitなどの要素について、勉強した内容をまとめたいと思います。

これらの方法により、開発効率の向上を実感しましたので、RailsとDockerを学習中の方のご参考になればと思います。

(投稿者はDockerを勉強中で、実務は未経験ですので、気になる点がありましたらコメントでご指摘をお願いします。)

参考記事

Dockerfileを改善するためのBest Practice 2019年版

Docker Buildにおけるリードタイム短縮のための3つの改善ポイント

開発環境

  • Mac OS X 10.15.4
  • Docker 19.03.8
  • Docker Desktop for Mac 2.3.0.3
  • Ruby 2.6.5 Rails 6.0.2

Dockerfileの解説

上記のDockerfileの要点を見ていきます。
1行目の# syntax =という部分は後述するBuildkitに関する記述です。

その次の

# Node.jsダウンロード用ビルドステージ
FROM ruby:2.6.5 AS nodejs

WORKDIR /tmp

# Node.jsのダウンロード
RUN curl -LO https://nodejs.org/dist/v12.14.1/node-v12.14.1-linux-x64.tar.xz
RUN tar xvf node-v12.14.1-linux-x64.tar.xz
RUN mv node-v12.14.1-linux-x64 node

ここでは、Railsに必要なNode.jsをインストールしています。

  • tmpに移動
  • node-v12.14.1-linux-x64.tar.xzをダウンロード、展開
  • node-v12.14.1-linux-x64をnodeにリネーム

の結果、/tmp/node(本体), /tmp/node-v12.14.1-linux-x64.tar.xz(不要)
が生成されます。

最終的なDockerイメージを軽量にするために、必要なnode本体だけを残す必要があります。Dockerはレイヤー構造で履歴が残っているため、ただ単にRUN rm node-v12.14.1-linux-x64.tar.xzとしても意味がないようです。

そこで、マルチステージビルドを利用しています。

マルチステージビルド

マルチステージビルドは、1つのDockerfileに複数のステージを分けて記述します。
例のDockerfileでは、FROM行が2箇所、つまり2つのステージがあります。

FROM ruby:2.6.5 AS nodejs
...
FROM ruby:2.6.5

1つ目のAS nodejsの記載で、ステージにnodejsと名付けています。これによって、2つ目のステージで、

# nodejsをインストールしたイメージからnode.jsをコピーする
COPY --from=nodejs /tmp/node /opt/node
ENV PATH /opt/node/bin:$PATH

上でインストールしたtmp/nodeだけをコピーする事ができます。

今回の場合は、これによって節約できるのは15MBほどですが、Goのようなコンパイル言語で、ビルド結果のファイルだけを次のステージに渡すと、かなりの軽量化ができるようです。

ちなみに、2つ目のステージのyarnインストールは、ファイルそのものではなく、install.shをダウンロードして実行しているだけで、不要なファイルが残らないため、このままで問題ないのだと思います。

# yarnのインストール
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /home/rails/.yarn/bin:/home/rails/.config/yarn/global/node_modules/.bin:$PATH

ユーザーの追加

# アプリケーション起動用のユーザーを追加
RUN useradd -m -u 1000 rails
RUN mkdir /app && chown rails /app
USER rails

ここでは、コンテナ内にユーザーを追加しています。デフォルトのrootユーザーのままでは、ホストとファイルを共有する際に権限の問題が発生するようです。

ただ、Docker for Macの場合、その問題は起こらないので、この設定は省略しても良いかもしれません。
(非rootユーザーにすると、vimを使いたい時にapt-getができないなど、色々と困る場面もありましたので..)

bundle install

WORKDIR /app

# Dockerのビルドステップキャッシュを利用するため
# 先にGemfileを転送し、bundle installする
COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/

作業ディレクトリに必要なパッケージ管理ファイルを置いています。その後、

RUN bundle config set app_config .bundle
RUN bundle config set path .cache/bundle

まずbundle configコマンドを使用して、installするpathを設定しています。

bundle config

例として、bundle config set path vendor/bundleと設定しておくと、bundle installの際に、bundle install --path vendor/bundleとパスを指定した事と同じになります。

installの際に、--pathを指定する方法は非推奨となったようなので、今後はbundle configを使いましょう。

Bundlerでビルドオプションを指定する

Cache Mount

RUN bundle config set path .cache/bundle
# mount cacheを利用する
RUN --mount=type=cache,uid=1000,target=/app/.cache/bundle \
    bundle install && \
    mkdir -p vendor && \
    cp -ar .cache/bundle vendor/bundle
RUN bundle config set path vendor/bundle

まずbundleのinstall先を.cache/bundleに設定します。

続く--mount=type=cachetarget=/app/.cache/bundleという記載が、Cache Mountを利用している部分です。
この記載を含むRUN命令の中では、targetに指定したpath(ここでは/app/.cache/bundle)の中身が、ホスト内に保存され、次回以降のbuildでcacheとして利用されるようになります。

したがって、直後のbundle install.cache/bundleにインストールされたgemは、ホスト内部に保存されています。
このままだと、コンテナ内にgemがない状態になってしまうため、続けてvendorディレクトリを作り、そこに.cacheディレクトリから中身をコピーしています。

最後にbundle config set path vendor/bundleでpathを指定することで、bundlerがvendor/bundleを読みに行ってくれるようになります。

やや回りくどい気もしますが、これによってbuild時間が劇的に改善しました。こちらによると、

RUN --mount=type=cache 命令をうまく活用すると,従来のdocker buildより33倍以上速いビルドも可能です.

私の環境ではbuildのたびにbundle installで300秒以上かかっていました。cacheがあれば、build時のbundle installは変更差分のみですぐ終わるので、気軽にbuildできます。

Buildkit

上述のCache Mountを使うためには、Buildkitでbuildをする必要があります。

Buildkitとは、dockerのイメージビルドを便利にしてくれるビルドツールキットです。こちらにあるように、ビルドのそれぞれの過程ごとにかかった時間を表示してくれたりします。
他にもビルドの並列実行など、たくさんの機能があるようです。

BuildKit によるイメージ構築

Buildkitの導入

主に2つの方法があります。

  • 環境変数DOCKER_BUILDKIT=1を設定する。
  • 試験機能モードを有効にすることでdocker buildxコマンドを使う(Docker 19.03以上)。

1つ目は、DOCKER_BUILDKIT=1 docker build .のように環境変数を指定する簡単な方法です。

2つ目は、buildxというプラグインを利用する方法で、buildkitの全ての機能が有効になるとのことです。config.json (デフォルトでは~/.docker/config.json) に次のように指定します。

~/.docker/config.json
{
    "experimental": "enabled"
}

これにより、環境変数なしでdocker buildx build .のようにビルドを実行する事ができます。
BuildKitによりDockerとDocker Composeで外部キャッシュを使った効率的なビルドをする方法

Buildkit Cache Mountの利用

--mountは新しい構文のため、Dockerfileの1行目に次の記述をする必要があります。

# syntax = docker/dockerfile:experimental

Cacheの削除

docker builder prune

以上がbuildkitの使い方です。
簡単な設定をするだけで良いので、cacheを使わない場合も、取り入れてみるといいかもしれません。

yarn install

RUN --mount=type=cache,uid=1000,target=/app/.cache/node_modules \
    bin/yarn install --modules-folder .cache/node_modules && \
    cp -ar .cache/node_modules node_modules

yarn installも同じくCache Mountを使います。

asset precompile

COPY --chown=rails . /app

RUN --mount=type=cache,uid=1000,target=/app/tmp/cache bin/rails assets:precompile

最後にホストのファイルを全てコピーして、Cache Mountを利用してアセットをプリコンパイルします。

開発環境と本番環境でさらにステージを分けて、本番環境でのみプリコンパイルを行うなどの設定も考えられます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む