- 投稿日:2020-06-04T23:58:09+09:00
deviseでログインする際の項目を追加する
【開発環境】
macOS Catalina
バージョン 10.15.3
ruby 2.5.1
Rails 5.2.4.2
visual studio codedeviseでログインする際の項目を追加する
deviseでログイン機能を作ると本来はメールアドレス、パスワードだけですが、他の項目のカラムを追加する方法です。
作成したいカラムを追加
今回は例としてcompanyというカラムを追加します。
$ rails g migration AddColumnToUsers company作成されたマイグレーションファイルを見てみます。
デフォルト値の設定をしないとエラーになるそうなので記載します。作成日時_add_company_to_users.rbclass 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:viewsregistrations/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.rbbefore_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:company]) endこれでDBに反映することができます。
間違いなどがありましたらご指摘いただければ幸いです。
最後までご覧いただきありがとうございました。
- 投稿日:2020-06-04T22:16:33+09:00
配列からpathを作成する
配列からpathを作成する
ルーティングヘルパーを使う方法の他に、パラメータの配列からパスやURLを作成することもできます。
次のようなルーティングを考えてみるruby.rbresources :magazines do resources :ads endresources :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ガイド
- 投稿日:2020-06-04T21:45:13+09:00
最速 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 mailgemfile追加
app内.gemfilegem '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 usersDB,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 reconfirmablemodelを編集
※「:confirmable」のアクセスも追加
user.rbclass User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable ←追加 end.envファイルをルートディレクトリに追加し、下記を追記
※一応パスワードがあるので「.env」を導入、先のgemfile導入はこの為
※環境変数に埋め込passwordは事前に申請してください/.envmail = 'あなたのgamilアドレス' password = 'パスワード申請が必要'config/initializers/mail_config.rb作成、編集
※メールの形式を指定
※「user_name」「password」は上記の.envファイルから引っ張ってきていますconfig/initializers/mail_config.rbActionMailer::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時間くらい時間取られましたが
思ったより簡単に実装できました。
- 投稿日:2020-06-04T21:45:13+09:00
最速 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 mailgemfile追加
app内.gemfilegem '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 usersDB,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 reconfirmablemodelを編集
※「:confirmable」のアクセスも追加
user.rbclass User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable ←追加 end.envファイルをルートディレクトリに追加し、下記を追記
※一応パスワードがあるので「.env」を導入、先のgemfile導入はこの為
※環境変数に埋め込passwordは事前に申請してください/.envmail = 'あなたのgamilアドレス' password = 'パスワード申請が必要'config/initializers/mail_config.rb作成、編集
※メールの形式を指定
※「user_name」「password」は上記の.envファイルから引っ張ってきていますconfig/initializers/mail_config.rbActionMailer::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時間くらい時間取られましたが
思ったより簡単に実装できました。
- 投稿日:2020-06-04T21:00:40+09:00
はじめてのポートフォリオ(技術<制作過程)
プログラミングを始めて半年を迎えます、上野栞音です。
スクールでは主にRailsアプリの作り方を教わり、現在は株式会社Wilicoでインターンとしてお世話になりながら就活中です。
プログラミングも字書きも不慣れなもんで、至らぬ点がありましたら教えてください。今回はスクール3ヶ月目のフェーズで作成したポートフォリオについて、雑多にはなりますが色々と書き留めていこうと思います。
ポートフォリオ概要
ToT
github
※かんたんログイン("Signin as a trial user")実装済み。開発コミュニティ向けのQAサイトを想定して制作しました。
詳細はgithubのREADMEに。開発経緯と目的
今回学習したかったことは大きく分けると2つ。
ユーザーのアクションを経て、どのようなデータが集められるのか。
そのデータを基にどのような分析結果を返せるのか。もともと、ユーザーに評価を付与→チャートで表示の機能は絶対に実装しようと決めていました。
きっかけとしてはAI(データの統計や分析)に興味が湧いたからなので、最初はPythonを使って何かしようと考えていたのですが却下。
理由としては、
- 初学者が1ヶ月という期限付きで新しい技術を得てアプリを作っても、強みのあるポートフォリオにはならない気がしたから。
- 当時教わっていたRailsについての理解が明らかに浅く、その理解度のまま別の技術に手を出すのが腑に落ちなかったから。
そんなこんなで いくつかサイトをチラ見しつつアプリの企画を練りました。
QAサイトの制作に至った理由としては、評価基準の設け方がパッとイメージできたからです。DB設計
静的(不変的)なデータのみを保管するようにするのがベスト!
というのが今回の学びです。開発はSQLite3、本番はMySQLで実装しています。はじめに考えていたもの
ユーザーの評価はユーザーテーブルに、それ以外の評価もコメントやスレッドのアクションに紐づけてアップデートする予定でしたが、途中でこんがらがってスクールのメンターさんに相談。
諸々差し置いて問題点として大きかったのが、この設計だとユーザーが何か操作するたびにアップデートが掛かってしまうため、正常にアップデートされない可能性があることです。(回避方も色々あるみたいですが、もっと掘り下げたいので省略)後々算出できるデータやユーザーの動作に依存して頻繁にデータが更新されるものは、理由がない限りDBで管理しないようにするため、分析するための材料だけ保管してチャートを描画するタイミングで算出する設計にしました。
最終的な設計
赤い部分をごっそり消しましたが、時間系の評価については他の評価に比べて算出するステップが一つ多い(2つのテーブルに登録されている登録日を基に差分を算出 → その差分を基に平均値算出)こともあり、集計のスピードを上げるために用途を変えてテーブルを残しています。
基本的な機能の実装
チャート実装を除いた部分です。チャートに時間が割きたかったこともあり、検索機能以外を1週間くらいでスケジュールを組んで実装しました。
さして難しい機能は実装してないのでアピールポイントを挙げると、
タグ付け機能と検索機能はgemを使わずに実装しました
改めて調べてみると「gemで出来たのでは…?」なんて思いますが、いい運動(?)になったので結果オーライ
以下備考
- タグ付け機能
- 同じ意味なのに違う表記(rails, Railsみたいな)のデータが入ると分析の精度が下がってしまうため、新規タグ作成の動作を重くしたかった。
- 基本1つのtext_fieldにカンマ区切りで書き込むようなやり方しか見つからず軽いなぁと悩んだあげく、普通に中間テーブルで結んでフォーム作った方が慣れたやり方だし早そうだと判断。
- 実装後、Qiitaのタグ付け機能を見て目ぇひん剥きました。なるほど。この手があったか。なるほど…
- 検索機能
チャート実装
開発経緯の通り、
今できる事→これやりたい!軸で企画を立ててここまで来たので、為せば成る精神で詳しい実装の目論見はほぼありませんでした。
調べてみるものの、当時jsに馴染みが無さ過ぎて悶絶寸前。<何をどうすれば これが出来るんですか…?
自分で考えたアプリのくせに、ここに来て教室の隅で静かに絶望してました。この時の学びとして大きかったのは、
分からない、初めて触るものは一度触ってみる大切さ何も分からず嘆いていた最中、スクールの同期生に相談したらchart.jsのcodepenを教えてくれました。
ここで少し触ってみた瞬間、chart.jsが面白いほど読める。要因としては、すでに完成しているコードを触れることが大きかったと思います。
どの値がどの軸のデータに影響しているかや、どの値がどのデザインに影響しているかなどが直観的に分かり、ここに配列渡せば勝ちじゃん!とゴールを定める事が出来ました。ちょっと無謀にも思いますが、試行錯誤も含めて工数を割くために基本的な機能 頑張ったので潔く実装に移って良かったなと思います(結果論)。
いざ、尋常にチャート実装
jsファイルとのデータの受け渡しは、gem 'gon'で行っています。
json形式に変換するのが基本ですが学習目的に含まれていないのと、これから実装するチャートの工数が読みきれずスピードを重視したかったので採用しました。大体こう。
- UserModelのロジックでチャートに渡す配列を計算するメソッド作成、Controllerで呼び出す
- UsersControllerでgonに渡す
- Viewでgonのタグ→jsファイルに渡す
- jsファイル→canvasタグに渡してチャート描画
※自分が流れを掴むために搔い摘んだものです。gon周りは特にもう少し検証しながら理解を深めたい。
UserModelのロジックは、大体こう。
コードがぼちぼち長いため、流れだけ伝わりますようにといった感じで書きます
代わりと言っては何ですが行単位でGithubのリンクを貼るので、気になる方はご覧ください。
評価基準ごとにまとめます。Questioner/Tags, Answer/Tags
ドーナツチャートの2項目です。少しデータを引っ張るロジックが違うだけなので、Questioner/Tagsを例にします。
Time to response, Time to solved, Total
バーチャートとレーダーチャートの項目です。
データを引っ張ってくるテーブルが違うだけなので、Time to responseを例にします。
ドーナツチャート以外は基本この流れです。細かい処理は省きます。
- 全ユーザーの平均値を算出
・ユーザーの動作に依存してグラフの階級を変えるため、このデータを基に基準になる値を算出します。- チャートの諸々を決めるのに使う値を算出
・返すのは各ユーザー平均値の [ 最小値, 最大値, それを基にした階級幅 ]。
・投稿されたIssueが1つも無い場合はfalseで返して例外処理。- 2を基に境界値を算出
・〇秒~〇秒のユーザーはスコア1、〇秒~〇秒のユーザーはスコア2… の〇だけ入ったような配列です。- 3の境界値を基に
- 各階級に何人ユーザーが含まれるかを算出して配列を作りながら
- 各階級に対象のユーザーが含まれるか否かを0,1で算出して配列を作る
- 6の配列を基に、ユーザーのスコアを算出
・3点の場合、[0,0,1,0,0,0,0,0,0,0] → each_with_indexで回すと2番目の値が1になる → 2+1でスコア算出- 5,7をControllerに返す
- ※レーダーチャートの配列だけControllerで作ります。
・ ControllerからModelのメソッドを呼び出す際、Model上に配列を作ろうとすると呼び出すたびに配列がリセットされるためです。
・[ 5(チャートに渡す値), 7(ユーザーのスコア) ] の配列が返り値なので、この配列の[1]を拾って配列を作ります。ほか
Time to response とほぼ同じなので、差分だけまとめます。
1.平均値
平均値が割り出せるほど絶対値が大きくなかったため合計値を算出しています。チャートの値が0ばかりになって変わり映えしなかった3. 階級の算出
階級の誤差をスコア1に寄せるため、呼び出すメソッドを変えています。
評価基準によってスコアが高くなる条件が分岐するのが肝で、
Time to ~ → 1.平均値算出の結果が低いと高スコア → calculate_evaluation_datas_sort_by_max
それ以外 → 1.合計値算出の結果が高いと高スコア → calculate_evaluation_datas_sort_by_min
大きく違う点としては、階級を決める基準が 最大値 → 最小値 であること(こちら基準で命名してます)と、配列をreverseするタイミングです。リファクタリング
制作期間が 2/15~3/15 くらいだったのですが、3月頭にβ版をデプロイして色んな方にレビューを頂きました。ありがとうございました
UserModelのメソッド
1. 最初は全てControllerに記述していたメソッドを
2. Modelに移行して
3. 共通するロジックをメソッド化(最新版)Viewの描画まわり
1. HTMLの部分テンプレートでscriptタグをrenderしていたのを
2. jsファイルに移行して(リンクはcomment_tags)
3. 一つのファイルにまとめて共通するロジックをメソッド化(最新版)チャートまとめ
このポートフォリオにおいて最大の学びでもあるのですが、
手を動かせば必ず答えは見つかる!
と確信を得られた制作物でした。
今扱えるパラメータを読み、それを基にロジックを組み、足りなければパラメータを送る。これを自分自身の経験をもって得られたのはとても貴重な学びだったなと思います。全てのチャートを最低限実装するまでの1週間くらいはコンソール画面にかじりついて模索する日々でしたが、ロジックができた瞬間の達成感が最高すぎて何だかんだ楽しかったです
さいごに
ポートフォリオについて調べていると「こういうのが有利!」ばかりで自分と同じレベル感の記事が上手いこと見つからず、望んでいた判断材料では無かったので書いてみました。
もちろん まだ改善の余地があるアプリとは思いますが、キリがないので一旦区切りにしようかなと思います。
これからポートフォリオを制作する方の目に留まり、少しでもインスピレーションの助けになれば幸いです
あと色んな方のこんな感じの記事見たいので是非書いてくださいありがとうございました!
参照
Ransackで簡単に検索フォームを作る73のレシピ
railsで複数ワードでの検索機能(or)とマイナス検索機能(-)を実装してみる
chart.jsのcodepen
gem 'gon'
- 投稿日:2020-06-04T20:51:46+09:00
データベースの制約について
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の記述をしなくて良いのでコードの可読性が上がる点を忘れる事があるので要注意!!
- 投稿日:2020-06-04T20:11:03+09:00
follow_redirect!って何をfollowしてるの?: Railsチュートリアル躓きポイント - 7章
ユーザー登録作成時のテストを書いております
/sample_app/test/integration/users_signup_test.rbrequire '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
> RedFAIL["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リクエストを送信した結果を見て"、
つまり対応するコントローラ内で明示されたリダイレクトの挙動に従っているようだ
- 投稿日:2020-06-04T18:16:48+09:00
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.rbrequire '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 endassert_selectで検証している構造を、
Google Chromeのデベロッパーツールで可視的に示すと
以上です
わかってないこと(自己学習用)
" "と' 'の使い分け
文字列であることを明示的に示す場合に""が好まれるような気がするが、
セレクタの指定は' '
- 投稿日:2020-06-04T18:16:48+09:00
エラーメッセージに対するテスト: 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.rbrequire '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 endassert_selectで検証している構造を、
Google Chromeのデベロッパーツールで可視的に示すと
以上です
わかってないこと(自己学習用)
" "と' 'の使い分け
文字列であることを明示的に示す場合に""が好まれるような気がするが、
セレクタの指定は' '
- 投稿日:2020-06-04T16:49:54+09:00
【Error】本番環境でアプリが表示されない
概要
手順通り、デプロイ作業を行なったのに、本番環境で表示されない
という事象に陥って1時間近く試行錯誤した結果、単純な回答だったため自分の戒めのため備忘録として残します事象
ブラウザで http://<サーバに紐付けたElastic IP>:3000/を入力しても表示されない
何かの間違い??と思い、リロードしてもロードが続いた結果、変わらず。。
検証
①ターミナル(ECサーバ)でエラーを確認
リポジトリ名(ECサーバ)$ less log/unicorn.stderr.log確認すべきは、3行目の(Mysql2::Error::ConnectionError)です。
Mysqlは起動してるってこと??②Mysqlの状態を確認
リポジトリ名(ECサーバ)$ sudo service mysqld status #コマンド入力後、下記の表示されたら起動中 mysqld (pid 8621) is running...よし!問題なし。。ではなぜ??
③AWSでポートの確認
ポート範囲の入力漏れ
原因
AWSにあるセキュリティグループのポート設定時に入力漏れがありました。
インバウンドルール追加時に下記の内容を入力する必要があります。【タイプ】カスタムTCPルール
【プロトコル】TCP
【ポート範囲】3000 ←私、忘れました
【ソース】カスタム 0.0.0.0/0無事、ブラウザに画面が表示されました
※最初はCSSが反映されていないため、ビュー崩れが起こっていると思います!参考
本番環境デプロイ時のコマンド集
https://qiita.com/15grmr/items/7ad36caa82a0fa27c4bdデプロイ時に対峙したエラーとその対応
https://qiita.com/sho012b/items/54fcd932ff8c76cdcd05
- 投稿日:2020-06-04T13:48:58+09:00
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.rbit 'should route articles create' do expect(post '/articles').to route_to('articles#create') endcreateアクションははhttpリクエストがpostなので、getではなくpostで書いていきます。
$ bundle exec rspec spec/routing/articles_spec.rb
No route matches "/articles"
というふうに出るので、routingを追加していきます
エンドポイント実装
config/routes.rbresources :articles, only: [:index, :show, :create]$ bundle exec rspec spec/routing/articles_spec.rbテストを実行して通ることを確認します。
そして、次はcontrollerのテストを書いていきます。
createアクション実装
spec/controllers/articles_controller_spec.rbdescribe '#create' do subject { post :create } end endこの記述を末尾に追加します。
そして、part2で定義したforbidden_requestsを使って認証がうまくいかないときのテストも書いていきます
spec/controllers/articles_controller_spec.rbdescribe '#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.rbdef create endこれでもう一度テストを実行して全て通ることを確認します。
テストが通ったということはきちんと認証が効いていることを表しています。では、createアクションを実装するためにテストを書いていきます。
spec/controllers/articles_controller_spec.rbcontext '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.rbdef 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 endActionController::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.rbcontext '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.rbdef 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.rbrescue 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.rbclass ErrorSerializer < ActiveModel::Serializer::ErrorSerializer; endこのように継承させます。
app/controllers/articles_controller.rbrescue render json: article, adapter: :json_api, serializer: ErrorSerializer, status: :unprocessable_entity endそして、先ほどの長い記述をすっきりさせる事ができます。
一応テストを実行して、失敗していないかを確認します。これでarticleをcreateするアクションの実装は完了です。
updateアクション
updateエンドポイント追加
ではエンドポイントの追加から再びしていきます。まずはテストを書いていきます。
spec/routing/articles_spec.rbit 'should route articles show' do expect(patch '/articles/1').to route_to('articles#update', id: '1') end毎回のようにエンドポイントのテストを書いていきます。showアクションはhttpリクエストが、patchもしくはputなので、そのどちらかを使います。
テストを実行して、正しくエラーが出ることを確認します。
config/routes.rbresources :articles, only: [:index, :show, :create, :update]updateを追加して、テストが通ることを確認します。
updateアクション追加
では次にcontroller#updateアクションのテストを書いていきます。
spec/controllers/articles_controller_spec.rbdescribe '#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 endupdateアクションがcreateアクションと違う部分は、リクエストの種類と既にデータベースにupdateの
対象となるarticleがある、という状況のみなので、最初にarticleを作成しているところと、リクエストを定義している部分以外はcreateのテストをコピーして来ているだけです。これでテストを実行します。
The action 'update' could not be found for ArticlesController
このようなエラーが出ると思います。なので、updateを実際に定義していきます。
app/controllers/articles_controller.rbdef 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.rbdescribe '#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.rbbelongs_to :userapp/models/user.rbhas_many :articles, dependent: :destroyそして、二つのモデルをつなげるためにはarticleモデルにuser_idを持たせる必要があるので、追加します。
$ rails g migration AddUserToArticles user:references $ rails db:migrateこれでアソシエーション自体は実装する事ができました。なので、それを使って、controllerの記述を変更していきます。
app/controllers/articles_controller.rbdef 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.rbdef create - article = Article.new(article_params) + article = current_user.articles.build(article_params)そして、factorybotにもアソシエーションの記述を足していきます。
spec/factories/articles.rbFactoryBot.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.rbit 'should route articles destroy' do expect(delete '/articles/1').to route_to('articles#destroy', id: '1') endテストを実行すると以下のメッセージが出ます
No route matches "/articles/1"
なので、ルーティングを編集していきます。
config/routes.rbresources :articlesonlyオプションで指定せずに全てを設定します。
これでルーティングのテストは通ります。次にcontrollerのテストを追加します。
spec/controllers/articles_controller_spec.rbdescribe '#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.rbdef destroy article = current_user.articles.find(params[:id]) article.destroy head :no_content rescue authorization_error end単純にcurrent_userの中の指定されたarticleをdestroyをしています。
これで、テストを実行します。
これで通ったら、全てが終了です。長い間お付き合いいただきありがとうございました!
- 投稿日:2020-06-04T13:48:58+09:00
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.rbit 'should route articles create' do expect(post '/articles').to route_to('articles#create') endcreateアクションははhttpリクエストがpostなので、getではなくpostで書いていきます。
$ bundle exec rspec spec/routing/articles_spec.rb
No route matches "/articles"
というふうに出るので、routingを追加していきます
エンドポイント実装
config/routes.rbresources :articles, only: [:index, :show, :create]$ bundle exec rspec spec/routing/articles_spec.rbテストを実行して通ることを確認します。
そして、次はcontrollerのテストを書いていきます。
createアクション実装
spec/controllers/articles_controller_spec.rbdescribe '#create' do subject { post :create } end endこの記述を末尾に追加します。
そして、part2で定義したforbidden_requestsを使って認証がうまくいかないときのテストも書いていきます
spec/controllers/articles_controller_spec.rbdescribe '#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.rbdef create endこれでもう一度テストを実行して全て通ることを確認します。
テストが通ったということはきちんと認証が効いていることを表しています。では、createアクションを実装するためにテストを書いていきます。
spec/controllers/articles_controller_spec.rbcontext '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.rbdef 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 endActionController::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.rbcontext '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.rbdef 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.rbrescue 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.rbclass ErrorSerializer < ActiveModel::Serializer::ErrorSerializer; endこのように継承させます。
app/controllers/articles_controller.rbrescue render json: article, adapter: :json_api, serializer: ErrorSerializer, status: :unprocessable_entity endそして、先ほどの長い記述をすっきりさせる事ができます。
一応テストを実行して、失敗していないかを確認します。これでarticleをcreateするアクションの実装は完了です。
updateアクション
updateエンドポイント追加
ではエンドポイントの追加から再びしていきます。まずはテストを書いていきます。
spec/routing/articles_spec.rbit 'should route articles show' do expect(patch '/articles/1').to route_to('articles#update', id: '1') end毎回のようにエンドポイントのテストを書いていきます。showアクションはhttpリクエストが、patchもしくはputなので、そのどちらかを使います。
テストを実行して、正しくエラーが出ることを確認します。
config/routes.rbresources :articles, only: [:index, :show, :create, :update]updateを追加して、テストが通ることを確認します。
updateアクション追加
では次にcontroller#updateアクションのテストを書いていきます。
spec/controllers/articles_controller_spec.rbdescribe '#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 endupdateアクションがcreateアクションと違う部分は、リクエストの種類と既にデータベースにupdateの
対象となるarticleがある、という状況のみなので、最初にarticleを作成しているところと、リクエストを定義している部分以外はcreateのテストをコピーして来ているだけです。これでテストを実行します。
The action 'update' could not be found for ArticlesController
このようなエラーが出ると思います。なので、updateを実際に定義していきます。
app/controllers/articles_controller.rbdef 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.rbdescribe '#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.rbbelongs_to :userapp/models/user.rbhas_many :articles, dependent: :destroyそして、二つのモデルをつなげるためにはarticleモデルにuser_idを持たせる必要があるので、追加します。
$ rails g migration AddUserToArticles user:references $ rails db:migrateこれでアソシエーション自体は実装する事ができました。なので、それを使って、controllerの記述を変更していきます。
app/controllers/articles_controller.rbdef 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.rbdef create - article = Article.new(article_params) + article = current_user.articles.build(article_params)そして、factorybotにもアソシエーションの記述を足していきます。
spec/factories/articles.rbFactoryBot.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.rbit 'should route articles destroy' do expect(delete '/articles/1').to route_to('articles#destroy', id: '1') endテストを実行すると以下のメッセージが出ます
No route matches "/articles/1"
なので、ルーティングを編集していきます。
config/routes.rbresources :articlesonlyオプションで指定せずに全てを設定します。
これでルーティングのテストは通ります。次にcontrollerのテストを追加します。
spec/controllers/articles_controller_spec.rbdescribe '#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.rbdef destroy article = current_user.articles.find(params[:id]) article.destroy head :no_content rescue authorization_error end単純にcurrent_userの中の指定されたarticleをdestroyをしています。
これで、テストを実行します。
これで通ったら、全てが終了です。長い間お付き合いいただきありがとうございました!
- 投稿日:2020-06-04T11:54:28+09:00
【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 update2.
application.js
を編集
=
を削除する。application.js// 変更前 //= require turbolinks // 変更後 // require turbolinks3.
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.slimdiv data-turbolinks='false' = link_to '', root_path
- 投稿日:2020-06-04T11:23:19+09:00
【Rails】Google Mapに複数マーカーを表示し、クリックしたら吹き出しを出す方法
目標
開発環境
・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.rbdef index @users = User.all gon.users = User.all end2.ビューを編集
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
を無効化しないと地図が切り替わらないので、必ず無効化しておきましょう。
- 投稿日:2020-06-04T10:02:25+09:00
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
(開発環境で使用)を追加Gemfilegem 'graphql' #追加 group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'graphiql-rails' #開発環境に追加 endコメントアウトを外す
config/application.rbrequire "sprockets/railtie" #コメントアウトを外すターミナル で下記を実行
ターミナル$ bundle install $ rails generate graphql:install #GraphQLに関するファイルが作成されます
routes.rb
に下記を追加routes.rbRails.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で変更しています。)
下記の画面になれば接続成功!
試しにQueryを送ると設定をしていないのでエラーが返ってきます。
GraphiQLに接続できない場合(Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show)
どの記事を見ても上記の設定しか紹介されておらず、そのまま実装してもエラーが出たので対策方法を紹介します。
APIモードで作成するとassetsが作成されないのでそれが原因のようです。
[Rails6][sprockets4.0.0]Sprocketsを無効にするためにapp/assetsフォルダ以下を削除すると、Sprockets::Railtie::ManifestNeededErrorが出たどちらかをやれば大丈夫なので開発環境に合わせて選択してください。
対策① 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ファイルがないとエラーが出るようです。
なのでバージョンを下げると解消できます。
Gemfilegem 'sprockets', '~> 3.7.2' #追加するターミナル$ bundle update終わりに
今回はrails6でGraphQLを使えるようにすることにフォーカスを当てました。
実際にQueryを作成してデータを受け取るところは他の記事を参考にしてください。
- 投稿日:2020-06-04T09:04:59+09:00
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
- 投稿日:2020-06-04T01:13:17+09:00
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)
- 投稿日:2020-06-04T00:52:29+09:00
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.51つ目の
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
を使いましょう。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=cache
〜target=/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の導入
主に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_modulesyarn 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を利用してアセットをプリコンパイルします。
開発環境と本番環境でさらにステージを分けて、本番環境でのみプリコンパイルを行うなどの設定も考えられます。
- 投稿日:2020-06-04T00:52:29+09:00
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.51つ目の
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
を使いましょう。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=cache
〜target=/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の導入
主に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_modulesyarn 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を利用してアセットをプリコンパイルします。
開発環境と本番環境でさらにステージを分けて、本番環境でのみプリコンパイルを行うなどの設定も考えられます。