- 投稿日:2019-12-02T23:35:46+09:00
Capybaraで壊れにくいテストを書くために気を付けていること
この記事はSmartHR Advent Calendar 2019 2日目の記事です。
SmartHRではRuby on Railsを広く採用しています。アプリケーションを長期的にメンテナンスしていくためにテストは欠かせません。特にReact.jsなどを用いた複雑なUIにおいては、単なるAPIのテストやモデルのテストだけではなく「実際にブラウザを操作して、ユーザーが期待する結果を得られるかどうか」をテストすることが重要です。
Rubyではこのようなブラウザを操作するテストを書くために、Capybaraという便利なフレームワークがあり、比較的簡単にテストを書き始めることができます。ただ、この手のテストは保守が大変であったり、手間の大きさからテストが追加されなくなったり、ということがよくあります。本記事では、私がこれまでの経験から学んだ、壊れにくいテストを書くためのTipsを紹介します。
なお、特に説明のない限り、ここではCapybara + RSpec + Selenium + Chrome (Headless)の環境を想定しています。
sleep
しない出オチっぽいですが、非常に重要です。非同期なリクエストの結果を待っているときや、時間差でレンダリングされる画面など「いい感じに少し待って」と言いたくなる状況は確かにあります。
そういった場合に、単に
sleep 5
などとしてしまうと
- テストを実行する環境によって適切な待ち時間が異なるため、落ちたり落ちなかったりするテストが生まれやすい
- テストが落ちたときに、適当に待ち時間を伸ばされることが多く、テストの実行時間が伸びがち
などの問題があります。このような場合では、何を待っているかを短いスパンで定期的にタイムアウトまで待つようなヘルパーメソッドを定義して、それを利用するようにしましょう。
it "何かのアクションの結果、Successメッセージが帰ってくる" do click_button "何かのアクション" finally do expect(page).to have_content "Success!" end end def finally(timeout: Capybara.default_max_wait_time) start = Time.now begin yield return rescue RSpec::Expectations::ExpectationNotMetError, Capybara::ElementNotFound raise if Time.now > start + timeout sleep 0.1 retry end endXPathやclassに依存しない
Capybaraでは
have_button
やfill_in
など基本的なHTMLの要素に対するヘルパーが定義されているため、素直な画面に対しては比較的読みやすいテストを書くことができます。しかし、現実には画面が複雑な構造になっていることが多く、これらのヘルパーだけでは力不足であることはよくあります。そういったときに、XPathやclassに依存したテストを書いてしまうこともきっとよくあるでしょう。
find('form active-form button').click expect(page).to have_xpath '//*[@id="form"]/div[2]'しかし、これでは後からテストだけ見たときに、何をテストしているのかわからなくなってしまいます。例えば、あなたが画面を大幅に弄った後に、「よーし、テスト直すかー」とこのテストを見たら... きっとこのテストごと消えてしまうことになるでしょう。
このような悲劇を産まないためにも、テストは後から読めるように、XPathやclassに依存しないことをおすすめしています。とはいえ、素直にヘルパーが利用できない画面というのは当然ありますから、話はそんな簡単ではありません。こういった場合にはデータ属性を利用し、さらにそれを指定するヘルパーメソッドを生やすと良い感じになります。
click_form_button expect_to_have_error_messagedef click_form_button find(spec_selector('active-form-button')).click end def expect_to_have_error_message expect(page).to have spec_selector('active-form-error-message') end def spec_selector(name) "[data-spec='#{name}']" endデータ属性は他の用途に利用されることがなく、自由に目印をつけられるので、テストを見たときに何を指しているかわかりやすく、HTML側の編集時にも目を引く良い方法です。適切な単位でデータ属性を割り当てていれば「なぜかわからないけどdivをひとつズラしたらテストが通らなくなった」といった問題も起きにくくなるでしょう。
within
を活用するこの記事をテストすると仮定して、「本文の書き出しに"Capybara"というリンクが含まれていること」をテストするとします。
expect(page).to have_link "Capybara", href: "https://github.com/teamcapybara/capybara"これでもテストは通りますが、これでは「本文の書き出しの中に」という重要な条件が抜けてしまっています。例えば、末尾の参考文献に"Capybara"を含むリンクを追加した途端、本当にテストしたかった書き出しのリンクが消えても、テストが通る状態になってしまいます。
他にも「保存」ボタンをクリックしたい状況があるとして、同じ画面中にいくつも「保存」ボタンがあると、単に
click_button "保存"
では、Ambiguous matchを引き起こしてしまいます。match: :first
やall
してアクセスする方法もありますが、あまり良い方法ではありませんよね。click_button "保存", match: :first # firstって何? all('button', text: "保存")[1].click # うーん...こういった場合では、
within
によるスコープの絞り込みが役に立ちます。it "書き出しにリンクが含まれる" do within_introduction do expect(page).to have_link "Capybara", href: "https://github.com/teamcapybara/capybara" end end it "ヘッダーの保存ボタンをクリック" do within_header do click_button "保存" end end def within_introduction within(spec_selector("introduction")) { yield } end def within_header within(spec_selector("header")) { yield } endデータ属性を使ったヘルパーメソッドの定義と合わせると、随分と読みやすく感じるはずです。
ユーザーの目に見えないもの(気にしないもの)をテストしない
これは書き方というか、心構えの問題だと思うのですが、基本的にデータベースの中身だったり、DOMの構造など「ユーザーが意識しないもの」はテストするべきではない、と考えています。例えば、こんなテストです。
visit edit_user_path(user) fill_in "名前", with: "新しい名前" click_button "保存" expect(user).to have_attribute(name: "新しい名前")もちろん、こういったテストを書かざるを得ない状況というのもあると思うのですが、可能な限りユーザーの体験をテストしたいので、ユーザーが知ることができないデータベースの値をテストするのは望ましくないでしょう。実際にユーザーが更新済みの値を見ることができる画面でテストするべきです。
visit edit_user_path(user) fill_in "名前", with: "新しい名前" click_button "保存" expect(page).to have_current_path user_path(user) expect(page).to have_content "新しい名前"ボタンクリックなどの操作も同様です。ユーザーは「divタグの3番目の中のボタンをクリックするぞ!」とクリックすることはありませんよね。
within
などと組み合わせて「新着メニューの中にあるボタンをクリックする」というように表現すると、後から見た時に読みやすくなります。# Bad all('button', text: "詳細")[2].click # Good within_new_menu do click_button "詳細" endブラウザのサイズを大きくする
当たり前のことだからしれませんが、あまり言及されている印象がないので書いておきます。ブラウザのサイズは大きければ大きいほどいいです。
Capybara.register_driver(:chrome_headless) do |app| options = Selenium::WebDriver::Chrome::Options.new(args: [ "window-size=3000,3000", "headless", "disable-gpu", ]) Capybara::Selenium::Driver.new(app, browser: chrome, options: options) endブラウザのサイズが小さい場合、別の要素が被ってくることによって、テストが落ちるなどの問題が起きることがあります。もちろん、ブラウザサイズが小さい画面でテストをしたい状況もあるので、必ずしもこの手が使えるわけではないのですが、特に理由がないならば、ある程度大きく設定しておくことをおすすめします。
簡単にブラウザを起動できる環境を用意する
CIでテストを回すことを考えると、ヘッドレスモードでChromeを動かすことになると思いますが、開発中やテストを書いている段階では、実際にブラウザが立ち上がって操作しているところを見れる方がテンションもあがりますし、問題を特定しやすくなります。
個人的には、環境変数でdriverを簡単に切り替えられるようにしておくと、さっとブラウザを起動してテストが落ちた原因を探ることができるので便利です。
Capybara.configure do config.default_driver = ENV['FOREGROUND'] ? :chrome : :chrome_headless endおわりに
ブラウザを操作するテストはコストが高く、メンテが難しい、という意見をよく聞きますが、個人的にはコツを抑えて書けば、もっとうまくできるのではないかと思っています。
SmartHRもまだ十分と言える状況ではありませんが、QAチームとも協力しながら、うまくテストを増やしていきたいところです。
- 投稿日:2019-12-02T23:27:17+09:00
【Rails】ユーザーの削除と権限【Rails Tutorial 10章まとめ】
管理ユーザー
admin属性
ユーザーを削除する機能を追加するが、この機能は特別な権限を持ったユーザーのみが行えるようにする。
まずはUserモデルにadmin属性を追加する。$ rails generate migration add_admin_to_users admin:boolean
マイグレーションファイルのadd_columnにdefault: falseオプションを与えて、admin属性のデフォルト値をfalseにする。
db/migrate/[timestamp]_add_admin_to_users.rbclass AddAdminToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :admin, :boolean, default: false end endサンプルユーザーの一人のadmin属性値をtrueにする。
db/seeds.rbUser.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) endadmin属性のテスト
admin属性を変更されるとセキュリティ上問題になるので、ユーザーの新規登録と編集ではユーザー情報を送信する場合にStrong Parametersという形式を用いていた。
def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) endこれが機能しているかどうかのテストを書く。
test/controllers/users_controller_test.rbtest "should not allow the admin attribute to be edited via the web" do log_in_as(@other_user) assert_not @other_user.admin? patch user_path(@other_user), params: { user: { password: '', password_confirmation: '', admin: true } } assert_not @other_user.admin? endadmin?メソッドはadmin属性を追加したことで生成されるメソッドで、Userオブジェクトに使うとadminの値に応じて論理値を返す。
ユーザーの削除
削除用リンク
ユーザー一覧ページに、管理者にのみ表示される削除用リンクを表示する。
app/views/users/_user.html.erb<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %> | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %> <% end %> </li>リンク表示の条件式に!current_user?(user)を付けることで、管理ユーザー自身は削除できないようになっている。
link_toにmethod: :deleteを追加することで、DELETEリクエストを送信できるようになっている。
また、data: { confirm: "You sure?" }を付けると削除前に確認用のポップアップが表示される。destroyアクション
ユーザーを削除するdestroyアクションを書く。
app/controllers/users_controller.rbbefore_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] . . . def destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url enddestroyメソッドでUserオブジェクトを削除する。
その後、フラッシュメッセージを表示してユーザー一覧ページにリダイレクトする。
また、logged_in_userを使ってログインユーザー以外はアクセスできないようにしておく。admin_userフィルター
管理者以外に削除用リンクを表示しないようにするだけでは、セキュリティ上の問題がある。
そこで、destroyアクションには管理者のみがリクエストを送れるようにするadmin_userメソッドを用意する。app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] before_action :admin_user, only: :destroy . . . private . . . # 管理者かどうか確認 def admin_user redirect_to(root_url) unless current_user.admin? end enddestroyアクションにDELETEリクエストを送っても、現在のユーザーが管理者でなければリダイレクトする。
ユーザー削除のテスト
管理者権限によるユーザー削除の制限のテスト
テスト用ユーザーの一人を管理者にしておく。
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: trueユーザー削除に関するテストは次の2点である。
ユーザーを削除しようとした時、
①ログインしていないユーザーの場合はログイン画面にリダイレクトする。
②ログインしているが管理者でないユーザーの場合はホーム画面にリダイレクトする。test/controllers/users_controller_test.rbdef setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect destroy when not logged in" do assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to login_url end test "should redirect destroy when logged in as a non-admin" do log_in_as(@other_user) assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to root_url endユーザーが削除されていないことをassert_no_differenceで確認している。
ユーザー削除のテスト
ユーザー一覧画面のテストに管理者によるユーザー削除を含める。
test/integration/users_index_test.rbrequire 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @admin = users(:michael) @non_admin = users(:archer) end test "index as admin including pagination and delete links" do log_in_as(@admin) get users_path assert_template 'users/index' assert_select 'div.pagination' first_page_of_users = User.paginate(page: 1) first_page_of_users.each do |user| assert_select 'a[href=?]', user_path(user), text: user.name unless user == @admin assert_select 'a[href=?]', user_path(user), text: 'delete' end end assert_difference 'User.count', -1 do delete user_path(@non_admin) end end test "index as non-admin" do log_in_as(@non_admin) get users_path assert_select 'a', text: 'delete', count: 0 end end管理者であれば、ユーザー一覧画面にはユーザー削除用リンクが表示されていることをassert_selectで確認している。
二つ目のテストではこれの逆である。
また、unless user == @adminとすることで、管理者以外の削除リンクだけが表示されていることを確認している。
- 投稿日:2019-12-02T23:05:50+09:00
rails独学でポートフォリオを作成中
こんにちは!現在転職活動中のバスケンです。
初めての投稿です!railsの独学を初めて3か月が経ちました。
この3か月間の私の学習経過は下記になります。1、progateでhtml,css,ruby,ruby on rails,githubを1周
2、ruby on rails チュートリアル2週
3、railsでオリジナルのポートフォリオを作成途中今日は3にて私が作成したポートフォリオの概要を説明します。
ポートフォリオ「KuiShare」の概要
私は人の後悔を聞いたとき似たような後悔が自分に起きないように気をつけるよにします。
このことから、みんなとたくさんの後悔が共有できればおのずと注意深くなり先々で自分に起きうる後悔を減らすことができるのではないかと考えました。
みんなと気軽に後悔を共有することができるのが今回作成した「KuiShare」です。
「KuiShare」のURL「https://kuishare.herokuapp.com/」
「KuiShare」でできること
・後悔したことを投稿/編集/削除
・後悔を共有したいユーザーをフォローする機能
★人の後悔にコメントをする機能
★人の後悔に『ドンマイ』(instagramでいういいね!)をつける機能
・プロフィールの編集機能利用している技術
・rails
・heroku
・S3(画像置き場)★印をつけている機能はこれから追加しようとしている機能です。
今回は初投稿でしたのでただ自分の学習経過を記しただけになってしまい申し訳ございません!!
明日からはちゃんとした技術ブログを投稿していきたいと思います!
- 投稿日:2019-12-02T22:32:35+09:00
【Rails】ユーザー一覧の表示とページネーション【Rails Tutorial 10章まとめ】
ユーザー一覧ページ
indexアクションの認可
ユーザー一覧を表示するindexアクションとビューはログインユーザーにのみ表示したいので、logged_in_userを設定する。
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] before_action :correct_user, only: [:edit, :update] def index @users = User.all end . . . endindexアクションには@users変数に全てのUserオブジェクトを入れておく。
テストを書く。
test/controllers/users_controller_test.rbtest "should redirect index when not logged in" do get users_path assert_redirected_to login_url endindexビュー
ユーザー一覧ページ用のindexビューを作成する。
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul>ヘッダーにユーザー一覧ページへのリンクを貼っておく。
app/views/layouts/_header.html.erb<% if logged_in? %> <li><%= link_to "Users", users_path %></li> <li class="dropdown"> . . .サンプルユーザーの生成
ユーザー一覧に表示するサンプルユーザーを手作業で作っていると手間なので、fakerジェムを使ってユーザーを大量生成する。
Gemfileにfakerジェムを追加する。
Gemfilegem 'faker', '1.7.3'seedファイルにユーザーを生成するRailsスクリプト(Railsタスク)を書く。
db/seeds.rbUser.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar") 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) endcreate!メソッドはcreateメソッドと違い、ユーザーが無効な場合でも例外を返すため、余計なエラーの現にならずに済む。
データベースのリセットと、Railsタスクの実行を行う。
rails db:migrate:reset rails db:seed
エラーを吐く場合はローカルサーバーを止めたりしてみる。
(自分の場合はseedファイルにデフォルトで入っていたコメントを残していたらエラーを吐いた。)ページネーション
will_paginateメソッド
一つのページに表示するユーザーを30人までとして、それ以降はページを切り替えて表示するページネーションを実装する。
まずGemfileにwill_paginateジェムとbootstrap-will_paginateジェムを追加する。Gemfilegem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0'ページネーションを実装するには、まずindexビューのユーザー表示部分をwill_paginateメソッドで挟む。
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul> <%= will_paginate %>次に、indexアクションの@users = User.allをUser.paginate(page: params[:page])に変える。
app/controllers/users_controller.rbdef index @users = User.paginate(page: params[:page]) endpaginateメソッドは、:pageパラメータが1であれば1−30のユーザーを、2であれば31−60のユーザーを取り出す(30人ずつ取り出す設定にしている場合)。
ここでエラーを吐く場合は、will_paginateジェムのバージョンを上げてみるとよい。
ユーザー一覧ページのテスト
テスト用ユーザーの生成
fixtureファイルに、テスト用ユーザーを大量生成するRailsタスクを書く。
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>テスト
ユーザー一覧ページ用の統合テストを作成する。
$ rails generate integration_test users_index
paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認する。
test/integration/users_index_test.rbrequire 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "index including pagination" do log_in_as(@user) get users_path assert_template 'users/index' assert_select 'div.pagination' User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name end end endテストでは1ページ目に表示されるユーザーの名前が、各ユーザーのプロフィールページにリンクしていることを確認している。
ユーザー一覧ページのパーシャル
割愛する。
- 投稿日:2019-12-02T22:02:17+09:00
railsの`ActiveSupport::Callbacks`の各コールバックの実行順序
railsの
ActiveSupport::Callbacks
の各コールバックの実行順序結論
before_*
,around_*
はキューに積まれた新しい順番に、
after_*
は積まれた順番の古い順に処理されるCalls the before and around callbacks in the order they were set, yields
the block (if given one), and then runs the after callbacks in reverse
order.See: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb#L76-L78
実装
キューを処理する処理
まずコールバックの実行自体は
ActiveSupport::Callbacks#run_callbacks
(https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb#L96-L141) で処理されます。その中の以下の処理で処理順を並び替えています。
callbacks.compile # See: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb#L103def compile @callbacks || @mutex.synchronize do final_sequence = CallbackSequence.new @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback| callback.apply callback_sequence end end end # See: https://github.com/rails/rails/blob/d94263f3e76527f196cab2026cfa119ff26b6d9e/activesupport/lib/active_support/callbacks.rb#L562-L569ポイントは
reverse
している所です。
処理するときは、積まれたキューをreverse
して頭から処理していきます。キューを積む処理
キューを積む時は
set_callback
はが呼ばれます。
その中で、prepend
とappend
どちらかが呼び出されます。def set_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) self_chain = get_callbacks name mapped = filters.map do |filter| Callback.build(self_chain, filter, type, options) end __update_callbacks(name) do |target, chain| options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped) target.set_callbacks name, chain end end # See: https://github.com/rails/rails/blob/d94263f3e76527f196cab2026cfa119ff26b6d9e/activesupport/lib/active_support/callbacks.rb#L673-L685def append(*callbacks) callbacks.each { |c| append_one(c) } end def prepend(*callbacks) callbacks.each { |c| prepend_one(c) } end # See: https://github.com/rails/rails/blob/d94263f3e76527f196cab2026cfa119ff26b6d9e/activesupport/lib/active_support/callbacks.rb#L571-L577この
skip_callback(name, *filter_list, &block)
を呼び出す時の第二引数*filter_list
の値によって配列の順序を変更しています。詳しく結論
キューを積む時
before_*
,around_*
はprepend
-> 先頭に追加先頭から古い順
after_*
はappend
-> 末尾に追加先頭から新しい順
キューを処理する時
reverse
して先頭から処理していくので、
before_*
,around_*
はキューに積まれた新しい順番に、
after_*
は積まれた順番の古い順に処理されます。
- 投稿日:2019-12-02T21:55:18+09:00
Rails × CircleCI × ECSのインフラ構築
簡単なDocker RailsアプリをECSを利用して本番環境に上げるまでのまとめ
* あくまで参考に(実務でそのまま利用できるほどしっかり構築しておりません)
前提知識
ECSとは?クラスターとは?サービスとは?タスクとは?って人は
ECSの概念を理解しよう
などを読んでください。Railsアプリ作成
まずはローカルでRailsアプリを作成しましょう。
機能は簡単なものでいいので、scaffoldなどを利用してサクッと作成してしまいましょう。
脳死で作成したい人は下記をご覧下さい。
Docker Rails Sampleアプリ構築 - QiitaAWS上で利用するリソースの作成
コンソール上(or Terraformなど)からあらかじめ作成しておくべきものになります。
IAMロール・ポリシーの作成
ECSで運用するための必要なIAMロール・ポリシーを作成していきます。
ちなみにポリシーとは、ロールに付与される権限情報です。なのでポリシーのないロールは何も権限がない状態なのでまずはポリシーを作成してロールを作成していきましょう。ポリシーの作成
作成手順
- IAMページに行って、サイドバーの「ポリシー」選択
- 「ポリシーの作成」ボタン押下
- JSONタブを開いて下記に記載したJSON内容をコピペして、「ポリシーの確認」押下
- それぞれのポリシー名を入力する
下記の4つのポリシーを作成する。
- AmazonSSMReadAccess
- AmazonECSTaskExecutionRolePolicy
- AmazonEC2ContainerServiceforEC2Role
- AmazonECSServiceRolePolicy
AmazonSSMReadAccess
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:GetParameters", "secretsmanager:GetSecretValue", "kms:Decrypt" ], "Resource": "*" } ] }AmazonECSTaskExecutionRolePolicy
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" } ] }AmazonEC2ContainerServiceforEC2Role
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:DescribeTags", "ecs:CreateCluster", "ecs:DeregisterContainerInstance", "ecs:DiscoverPollEndpoint", "ecs:Poll", "ecs:RegisterContainerInstance", "ecs:StartTelemetrySession", "ecs:UpdateContainerInstancesState", "ecs:Submit*", "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" } ] }AmazonECSServiceRolePolicy
{ "Version": "2012-10-17", "Statement": [ { "Sid": "ECSTaskManagement", "Effect": "Allow", "Action": [ "ec2:AttachNetworkInterface", "ec2:CreateNetworkInterface", "ec2:CreateNetworkInterfacePermission", "ec2:DeleteNetworkInterface", "ec2:DeleteNetworkInterfacePermission", "ec2:Describe*", "ec2:DetachNetworkInterface", "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", "elasticloadbalancing:DeregisterTargets", "elasticloadbalancing:Describe*", "elasticloadbalancing:RegisterInstancesWithLoadBalancer", "elasticloadbalancing:RegisterTargets", "route53:ChangeResourceRecordSets", "route53:CreateHealthCheck", "route53:DeleteHealthCheck", "route53:Get*", "route53:List*", "route53:UpdateHealthCheck", "servicediscovery:DeregisterInstance", "servicediscovery:Get*", "servicediscovery:List*", "servicediscovery:RegisterInstance", "servicediscovery:UpdateInstanceCustomHealthStatus" ], "Resource": "*" }, { "Sid": "ECSTagging", "Effect": "Allow", "Action": [ "ec2:CreateTags" ], "Resource": "arn:aws:ec2:*:*:network-interface/*" }, { "Sid": "CWLogGroupManagement", "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:DescribeLogGroups", "logs:PutRetentionPolicy" ], "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/*" }, { "Sid": "CWLogStreamManagement", "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:DescribeLogStreams", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/*:log-stream:*" } ] }ロールの作成
IAMページに行って、サイドバーの「ロール」→「ロールの作成」より下記のロールを作成する。
作成後、各ロールのページにて「ポリシーをアタッチする」を押下して上記で作成したポリシーを紐づける。
- ecsInstanceRole(→AmazonEC2ContainerServiceforEC2Roleに紐づける)
- AWSServiceRoleForECS(→AmazonECSServiceRolePolicyに紐づける)
- ecsTaskExecutionRole(→AmazonECSTaskExecutionRolePolicy,AmazonSSMReadAccessを紐づける)
ALBの作成
ECSのサービス作成時にALBを登録しておけば、コンテナに動的にポートマッピングをしてくれるようになるので楽になります。
- Application Load Balancerを選択
- 名前を入力。サブネットを二つ選択。(ない場合は、適宜作成)
- セキュリティグループを選択。(ない場合は、適宜作成)
- ターゲットグループを選択or作成
- ターゲットグループにインスタンスを登録
クラスターの作成
ECSのサイドバーにある「クラスター」から「クラスターの作成」ボタンを押下
「クラスターテンプレートの選択」は「EC2 Linux + ネットワーキング」を選択
1. クラスター名記載
2. EC2インスタンスタイプの選択(お好み)
3. キーペア(お好み。ただし、デバッグ時にSSHできた方がいいので設定しておくことをおすすめ)
4. コンテナインスタンスの IAM ロールに「ecsInstanceRole」を選択RDSの作成
aws-cliでのRDS作成。
コンソール上からでもOKです。aws rds create-db-instance \ --db-instance-identifier rails-sample-db-production \ --db-instance-class db.t2.micro \ --db-subnet-group-name rails-sample-db-subnet-group \ --engine mysql \ --engine-version 5.7.26 \ --allocated-storage 20 \ --master-username [username] \ --master-user-password [password] \ --backup-retention-period 3 \参考
AWS CLI を使って RDS を作成する (自分用メモ) - Qiita
AWS-CLI Amazon Aurora インスタンス作成 - QiitaAWS Systems Managerの設定
AWS Systems Managerは、タスク実行時にコンテナに注入する秘匿情報(環境変数)の管理に使えるAWSサービスです。
初めての人は設定の仕方を含め、
ECSでごっつ簡単に機密情報を環境変数に展開できるようになりました!
を見れば大体分かると思います。AWS Systems Managerの左側メニューから「パラメータストア」→「パラメータの作成」をクリック。パラメータの詳細画面が表示されるので、パラメータのキー名と値を入力します。タイプには「安全な文字列」を選択します。
パラメータのキー名と値一覧
キー名 値 /production/database_username [RDSに設定したusername] /production/database_password [RDSに設定したpassword] /production/database_host [RDSインスタンスのエンドポイント] RDSインスタンスのエンドポイント(RDS→データベース→[インスタンス名])
CircleCIの設定
circleci/config.ymlversion: 2.1 orbs: aws-cli: circleci/aws-cli@0.1.13 executors: builder: docker: - image: circleci/buildpack-deps commands: init: steps: - checkout - aws-cli/install - install_ecs-cli - setup_remote_docker install_ecs-cli: steps: - run: name: Install ECS-CLI command: | sudo curl -o /usr/local/bin/ecs-cli https://amazon-ecs-cli.s3.amazonaws.com/ecs-cli-linux-amd64-latest sudo chmod +x /usr/local/bin/ecs-cli jobs: build: executor: builder steps: - init - run: name: Build application Docker image command: | docker build -f build.Dockerfile --rm=false -t rails-sample-app-build:latest . - run: name: Save image command: | mkdir -p /tmp/docker docker save rails-sample-app-build:latest -o /tmp/docker/image - persist_to_workspace: root: /tmp/docker paths: - image deploy: executor: builder steps: - init - attach_workspace: at: /tmp/docker - run: docker load -i /tmp/docker/image - run: name: Assets precompile and Push Docker image command: | docker build -f assets.Dockerfile --build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY} --rm=false -t rails-sample-app-build:latest . - run: name: Push Docker image command: | ecs-cli push rails-sample-app-build:latest - run: name: ECS Config command: | ecs-cli configure \ --cluster rails-sample-${CIRCLE_BRANCH} \ --region ${AWS_DEFAULT_REGION} \ --config-name rails-sample-${CIRCLE_BRANCH} - run: name: migrate deploy command: | ecs-cli compose \ --file ecs/${CIRCLE_BRANCH}/migrate/docker-compose.yml \ --ecs-params ecs/${CIRCLE_BRANCH}/migrate/ecs-params.yml \ --project-name rails-sample-${CIRCLE_BRANCH}-migrate \ up \ --launch-type EC2 \ --create-log-groups \ --cluster-config rails-sample-${CIRCLE_BRANCH} - run: name: Unicorn + Nginx deploy command: | ecs-cli compose \ --file ecs/${CIRCLE_BRANCH}/app/docker-compose.yml \ --ecs-params ecs/${CIRCLE_BRANCH}/app/ecs-params.yml \ --project-name rails-sample-${CIRCLE_BRANCH}-app \ service up \ --container-name nginx \ --container-port 80 \ --target-group-arn ${TARGET_GROUP_ARN} \ --timeout 0 \ --launch-type EC2 \ --create-log-groups \ --cluster-config rails-sample-${CIRCLE_BRANCH} workflows: version: 2 build-deploy: jobs: - build - deploy: requires: - build filters: branches: only: - masterCircleCIに設定する環境変数
CircleCIのプロジェクトの設定ページ(Settings→[アカウント名or組織名]→[プロジェクト名])に行き、下記の画像の箇所から設定する
https://circleci.com/gh/[アカウント名or組織名]/[プロジェクト名]/edit#env-vars
環境変数名 値 AWS_ACCESS_KEY_ID [AWSのアクセスキーID] AWS_ACCOUNT_ID [AWSのアカウントID] AWS_DEFAULT_REGION [AWSのデフォルトリージョン] AWS_ECR_REPOSITORY_URL [AWSのECRリポジトリURL] AWS_SECRET_ACCESS_KEY [AWSのシークレットアクセスキー] RAILS_MASTER_KEY [config/master.keyの値] TARGET_GROUP_ARN [ターゲットグループのarn] Task definitionの作成
docker-compose.yml
rails-sample/ecs/production/app/docker-compose.ymlversion: "3" services: app: image: [ECRのリポジトリURI] entrypoint: bundle exec unicorn -c config/unicorn.rb env_file: - ../env working_dir: /projects/rails-sample logging: driver: "awslogs" options: awslogs-region: "ap-northeast-1" awslogs-group: "rails-sample-production/app" awslogs-stream-prefix: "rails-sample-app" nginx: image: [ECRのリポジトリURI] ports: - 0:80 links: - "app:app" env_file: - ../env working_dir: /projects/rails-sample logging: driver: "awslogs" options: awslogs-region: "ap-northeast-1" awslogs-group: "rails-sample-production/nginx" awslogs-stream-prefix: "rails-sample-nginx"* Nginxの設定ファイルは適宜用意してください。上記のnginxの欄にnginx設定ファイル群の設置・起動用のスクリプト
entrypoint: /bin/bash /etc/nginx/start.sh
を用意するなど。ecs-params.yml
タスク実行時に実行ロールの指定やコンテナに注入する環境変数をAWS Systems Managerから取得するして設定するためのファイル
rails-sample/ecs/production/app/ecs-params.ymlversion: 1 task_definition: # タスク実行時のロールを指定 task_execution_role: ecsTaskExecutionRole services: # 起動するコンテナを記載(app, nginx) app: # 何らかの理由で失敗・停止した際に、タスクに含まれる他のすべてのコンテナを停止するかどうか(デフォルトはtrue) essential: true # AWS Systems Managerから秘匿情報を取得してコンテナに環境変数を注入 secrets: - value_from: /production/database_username name: DATABASE_USERNAME - value_from: /production/database_password name: DATABASE_PASSWORD - value_from: /production/database_host name: DATABASE_HOST nginx: essential: true run_params: network_configuration: awsvpc_configuration: assign_public_ip: ENABLEDコンテナ全体に注入する環境変数の設定
各環境(production, stagingなど)ごとのディレクトリ以下に
env
ファイルを用意してそこに記載する# ここのファイルに追加した環境変数は全てのコンテナに展開されます # Rails APP_HOST=54.238.241.230 RAILS_ENV=production RAILS_LOG_TO_STDOUT=1 RAILS_SERVE_STATIC_FILES=1 # RDS DATABASE_NAME=rails-sample_production DATABASE_PORT=3306 DATABASE_POOL=10 # Unicorn UNICORN_PORT=23380 UNICORN_TIMEOUT=180 UNICORN_WORKER_PROSESSES=2 # Nginx専用 NGINX_APP_SERVER_NAME=app NGINX_APP_SERVER_PORT=23380 NGINX_DOCUMENT_ROOT=/projects/rails-sample/public NGINX_FRONT_SERVER_NAME=54.238.241.230構築の際に詰まる可能性のあるポイント
ECSコンテナインスタンスの作成
- インスタンスへのIAMロールを付与すること
- ecs-agentのインストール ( Amazon ECS コンテナエージェントのインストール - Amazon Elastic Container Service )
- EC2インスタンスの
/etc/ecs/ecs.config
にCLUSTER_NAME=クラスター名
の登録- 所属するクラスターを変更する場合、
/var/lib/ecs/data/ecs_agent_data.json
を削除してからecs-agentを再起動するDefaultクラスター作成しているし、IAMロールにecs:CreateClusterの権限付与されているから自動で作成なんかもしてくれるのかと思ったら作成してくれなかった。
なので、クラスター作成→インスタンス作成の方が良い(ちな、クラスター作成時にインスタンスも作成するようにはできるっぽい)
→カスタマイズされてるAMI利用時のみ初期スクリプトによってDefaultクラスターを作成しているのかもしれない参考
Amazon ECS コンテナインスタンスの起動 - Amazon Elastic Container Service
Amazon ECS-optimized AMI - Amazon Elastic Container Serviceインスタンスタイプについて
ある程度余裕持たないとタスク実行するための容量を持たなくて死ぬ
(ほんとは、ローカルや本番環境で動かした時の使用量見てタスク実行に必要なメモリを設定した方が良い)ecs-cliでのタスク実行
ecs-params.yml
ファイル内でtask_execution_role
を指定することtask_execution_role
で指定した適切なポリシーを適用したIAM Roleを用意すること(エラーが出なくて、単純に実行されないので気づきにくい)まとめ
ECSについてググればたくさん記事出てくるのですが、実際に活用しようとしてみるとたくさん落とし穴があります。もし利用しようか考えている人は一度デモアプリで利用してみることをお勧めします。
最後に
UUUMではインフラに詳しいエンジニアを欲しています。
詳しくはこちら →→→→→→ UUUM攻殻機動隊の紹介
- 投稿日:2019-12-02T21:53:36+09:00
mergeメソッド
- 投稿日:2019-12-02T21:11:10+09:00
Docker Rails Sampleアプリ構築
適当なRailsアプリを作成するのに脳死で作成する
前提
- Ruby 2.6.5
- Railsバージョン6.0.1
- MySQL 5.7
- Node.js 8系
- webpacker用のコンテナは用意していない
$ mkdir rails-sample $ rbenv local [使用するrubyバージョン] $ git init $ bundle init gem 'rails'のコメントアウトを外す $ bundle install --path vendor/bundle $ bundle exec rails new . -B -d mysql --skip-test -B bundle install をスキップする(お好み) -d 利用するDBを指定(デフォルトはSQLite) --skip-test railsのデフォルトのminitestというテストを利用しない場合は指定(お好み) Gemfileの上書きしていいかどうかは Y でEnter $ bundle exec rails webpacker:install .gitignore に vendor/bundleを追記(お好み)docker-compose.ymlとDockerfile作成
Dockerfile
FROM ruby:2.6.5 ENV LANG C.UTF-8 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs # nodejsとyarnはwebpackをインストールする際に必要 # Node.js RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ apt-get install nodejs # yarnパッケージ管理ツール RUN apt-get update && apt-get install -y curl apt-transport-https wget && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y yarn WORKDIR /tmp COPY Gemfile Gemfile COPY Gemfile.lock Gemfile.lock RUN bundle install ENV APP_HOME /rails-sample RUN mkdir -p $APP_HOME WORKDIR $APP_HOME COPY . /rails-sampledocker-compose.ymlversion: '3' services: db: image: mysql:5.7 environment: MYSQL_USER: root MYSQL_ROOT_PASSWORD: password volumes: - ./tmp/docker/mysql:/var/lib/mysql:delegated web: build: . command: bundle exec rails s -p 3000 -b '0.0.0.0' volumes: - .:/chiko ports: - "3000:3000" depends_on: - dbdatabase.ymlを編集(お好み)
database.ymldefault: &default adapter: mysql2 timeout: 5000 encoding: utf8mb4 charset: utf8mb4 collation: utf8mb4_general_ci pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password host: db port: 3306 development: <<: *default database: rails-sample_development test: <<: *default database: rails-sample_test production: <<: *default database: <%= ENV["DATABASE_NAME"] %> username: <%= ENV["DATABASE_USERNAME"] %> password: <%= ENV["DATABASE_PASSWORD"] %> host: <%= ENV["DATABASE_HOST"] %> port: <%= ENV["DATABASE_PORT"] %>defaultに
- charset: utf8mb4
- collation: utf8mb4_general_ci
- port: 3306を追記
productionは
- database: <%= ENV["DATABASE_NAME"] %>
- username: <%= ENV["DATABASE_USERNAME"] %>
- password: <%= ENV["DATABASE_PASSWORD"] %>
- host: <%= ENV["DATABASE_HOST"] %>
- port: <%= ENV["DATABASE_PORT"] %>を全部環境変数に変更
$ docker-compose build $ docker-compose run --rm web rails db:createScaffoldでUserモデル作成
$ docker-compose run --rm web rails g scaffold user name:string age:integerトップページを用意
$ bundle exec rails g home indexroutes.rbRails.application.routes.draw do root 'home#index' # これを追記 resources :users endUserページへのリンクを付与
index.html.erb<h1>Home#index</h1> <p>Find me in app/views/home/index.html.erb</p> <%= link_to "user", users_path %> <%# これを追記マイグレーションして、コンテナを立ち上げる
$ docker-compose run --rm web rails db:migrate $ docker-compose up -d=> http://localhost:3000 にアクセスして確認
- 投稿日:2019-12-02T21:01:37+09:00
【Rails】フレンドリーフォワーディング【Rails Tutorial 10章まとめ】
認可機能によって、ログインしていないユーザーが編集ページにアクセスしようとすると、ログインページにリダイレクトされる。
その後ログインするとプロフィールページに移動するが、ここでもともとアクセスしようとしていた編集ページに移動してくれると親切である。
これをフレンドリーフォワーディングと呼ぶ。フレンドリーフォワーディングのテスト
フレンドリーフォワーディングは少し複雑なので、テスト駆動開発で進める。
編集成功時のテストを修正する。test/integration/users_edit_test.rbtest "successful edit with friendly forwarding" do get edit_user_path(@user) log_in_as(@user) assert_redirected_to edit_user_url(@user) name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end編集ページにアクセス→ログインページにリダイレクトしてログイン→編集ページにリダイレクト、という流れになる。
フレンドリーフォワーディングの実装
store_locationとredirec_back_orメソッド
ユーザーを希望のページに転送するには、リクエスト時点のページを保存しておく必要がある。
app/helpers/sessions_helper.rb# 記憶したURL (もしくはデフォルト値) にリダイレクト def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end # アクセスしようとしたURLを覚えておく def store_location session[:forwarding_url] = request.original_url if request.get? endstore_locationメソッドでは、session[:forwarding_url]に、リクエスト先を取得するrequest.original_urlを入れる。
この時、セキュリティ上の問題を解決するために、request.getを使ってGETリクエストの場合のみとする。redirect_back_orメソッドでは、or演算子||を使って、session[:forwarding_url]がある場合はそちらにリダイレクトし、無い場合は引数のURLにリダイレクトする。
リダイレクト後はsession[:forwarding_url]を削除しておく。store_locationメソッドを使って、非ログイン時に編集ページにアクセスしたらURLを保存する。
app/controllers/users_controller.rb# beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? store_location flash[:danger] = "Please log in." redirect_to login_url end endredirect_back_orメソッドを使って、ログイン後に保存したURLにリダイレクトする。
app/controllers/sessions_controller.rbdef create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end
- 投稿日:2019-12-02T20:24:02+09:00
Rails と React+TypeScript でのGraphQLスキーマ運用プラクティス
Linc'well Advent Calendar3日目の記事です。
当社が展開するクリニックグループである CLINIC FOR の予約システム では一部機能にGraphQLを導入しているのですが、スキーマの管理等も踏まえてどのように構築し、運用したかについて紹介したいと思います。
※ ちなみに構築したのは2019/3~4頃であり、その点は割り引いてご覧ください
今だともっと良さげなプラクティスがあるかもしれません。また、予約システム全体としてはモノリシックなRailsアプリケーションになっているのですが、GraphQLを導入した一部機能ではフロントエンドがTypeScript+Reactで構築されており、ruby側で定義されたスキーマ情報はTSの型定義へスムーズに繋げる必要がありました。
スキーマファイルの生成
GraphQLには様々な実装があるかと思いますが、今回
rmosolgo/graphql-ruby
というgemを利用しました。こちらのgemはスキーマの定義ファイルを直接記述せず、個別のクエリや型の実装から最終的なスキーマを出力する code-first なアプローチを取っています。
なので処理を先に記述し、それを参照することで型定義が決定される、という順番になるのですが、最終的な定義の情報は、Schemaのクラスに生えている以下のメソッドで文字列として全出力され、その戻り値をファイル出力してあげることでスキーマファイルが出来上がります。
今回は上記メソッドを以下のようなrakeタスクに組み込み、差分が生じたらファイル出力の上でコミットしていくという運用で行くことにしました。
# rake graphql:dump_schema namespace :graphql do task dump_schema: :environment do schema_definition = LincwellSchema.to_definition schema_path = 'frontend/src/generated/schema.graphql' File.write(Rails.root.join(schema_path), schema_definition) puts "#{schema_path} updated." end endCIでのスキーマ定義チェック
定義ファイルが最新のものかチェックするため、rspecにてスキーマファイルの最新チェックも用意します。
describe 'Validate lastest-veresion' do let(:dump_path) { Rails.root.join('frontend', 'src', 'generated', 'schema.graphql') } let(:current_definition) { File.read(dump_path) } subject { described_class.to_definition } it { is_expected.to eq(current_definition) } end簡易な実装ですが、CIが回るたびにチェックが入るので、少なくともこれで更新忘れはなくなり、実装とスキーマの乖離を防止することができたのではないかと思います。
スキーマ情報からTSの型をつくる
続いてはこうして出来上がったスキーマ情報をフロントエンド側へ連携させていきます。
出力されたスキーマ情報をもとにTSの型を作りたいのですが、さすがに手ずから行うのは厳しいので
graphql-codegen
を用いて自動化することにします。# codegen.yml overwrite: true schema: "src/generated/schema.graphql" documents: "src/libs/queries/*.ts" generates: src/generated/graphql.tsx: plugins: - "typescript" - "typescript-operations" - "typescript-react-apollo" src/generated/graphql.schema.json: plugins: - "introspection""scripts": { "generate": "graphql-codegen --config codegen.yml" }上記設定の上、
npm run generate
コマンドを実行します。そうするとrailsから出力されたスキーマファイルを元に、
src/generated/graphql.tsx
にTSの型定義が自動生成されるようになります。ここまで繋がると気持ちイイですね!これでReactコンポーネントから自由に任意のクエリやオブジェクトをTSの型として利用できるようになりました。Rails側で定義されている
graphql/types
と乖離しないため、非常に楽に、かつ楽しく開発を進めることができます。{ "paths": { "Components/*": ["src/components/*"], "Styles/*": ["src/styles/*"], "Libs/*": ["src/libs/*"], "Generated/*": ["src/generated/*"] } } }tsconfig.jsonで上記のようにaliasを切り、Componentで以下のように呼び出せます。
import * as React from "react"; import { useQuery } from "react-apollo-hooks"; import styled from "styled-components"; import { CancelRate } from "Generated/graphql";めでたしめでたし
ちなみにフロントエンド側の型生成まわりに関しては以下の記事を大変参考にさせて頂きました。
https://qiita.com/mizchi/items/fb9f598cea94d2c8072d
投稿時期も非常にタイムリーで、もしこのエントリがなかったらと思うと(ry
まとめ
- code-firstな
graphql-ruby
の利用#to_definition
をrake経由で実行し、型定義の.graphql
ファイルをフロントへ配置rspec
にて最新dumpの検査graphql-codegen
にてTSの型定義ファイルへコンバート- フロントアプリケーションにて利用可能に
だいたいこんな感じになりました。
現状もこの当時からあまり大きく変わっていませんが、もしより良いプラクティスがあればフィードバック頂けたら幸いです。
- 投稿日:2019-12-02T20:05:31+09:00
【Ruby | Rails】Dockerfileの中で"ADD Gemfile ~ RUN bundle install"をするのはやめませんかという話
今回の検証環境
- Ruby
2.6.5
- Docker
19.03.5
- Docker-Compose
1.24.1
はじめに
- Railsの設定を例にすると結構複雑になっちゃうので、今回は単純にRubyをDocker上で使用する例で解説します。
Railsを使用する場合も要点は同じなので適宜読み替えてください。- 今回は
hogehoge
ディレクトリ配下で作業します。サンプルコード中のhogehoge
の部分は自由に変更して構いません。やめませんか
RailsやRubyのDocker環境構築の解説をしている記事で、
Dockerfile
内で以下のようにADD Gemfile
~RUN bundle install
している記事を本当にたくさんよく見かけます。DockerfileFROM ruby:2.6.5 WORKDIR /hogehoge RUN gem install bundler # ↓こういうの↓ ADD Gemfile Gemfile ADD Gemfile.lock Gemfile.lock RUN bundle installこれ、やめませんか?
なんでやねん
Dockerfile
の中でADD Gemfile
~RUN bundle install
をすることには以下のようなデメリットがあります。
- Gemfileを編集するたびに毎回
docker build
やdocker-compose build
をしないといけなくなる。
- Gemを1つ追加するだけでも全Gemをインストールし直す羽目になる。
nokogiri
とかインストール遅いよね!毎回待たされるの嫌だよね!何よりRubyistの皆さんとしては、
Gemfile
を編集したら本能的にbundle install
したいですよね?したくないですか?したいですよね?じゃあどうすんねん
docker-compose
を上手く使えばもっと効率よく楽しく開発できます。Dockerfile
containers
ディレクトリ配下にDockerfile
を作成します。containers/DockerfileFROM ruby:2.6.5 WORKDIR /hogehoge RUN gem install bundlerこんだけ。
docker-compose.yml
次に、アプリケーションのルートディレクトリに
docker-compose.yml
を作成します。docker-compose.ymlversion: '3' services: app: build: context: . dockerfile: containers/Dockerfile environment: # これがないとGemを`vendor/bundle`以下から読み込んでくれないので注意 # (正確には、`.bundle/config`の設定を読み込んでくれない) BUNDLE_APP_CONFIG: /hogehoge/.bundle volumes: - .:/hogehogeこんだけ。
単純にカレントディレクトリ全体をマウントしてるだけですね。ビルドしよう
さぁ
docker-compose build
していきましょう。$ docker-compose build Building app Step 1/4 : FROM ruby:2.6.5 ---> d98e4013532b Step 2/4 : ENV APP_ROOT /hogehoge ---> Using cache ---> 97b5a8bca2d0 Step 3/4 : WORKDIR $APP_ROOT ---> Using cache ---> 54066d2ae384 Step 4/4 : RUN gem install bundler ---> Using cache ---> 290d99a58c5b Successfully built 290d99a58c5b Successfully tagged hogehoge_app:latestすぐ終わりますね。
(初回だけruby:2.6.5
のDocker imageのpullに時間がかかります。)Gemをインストールしてみよう
とりあえず
Gemfile
を作成します。$ docker-compose run --rm app bundle init Writing new Gemfile to /hogehoge/Gemfile適当に
bcrypt
でも入れてみますかね。Gemfile# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } # gem "rails" # ↓追加↓ gem 'bcrypt'それでは念願の
bundle install
です。
docker-compose
経由で実行するのと、--path
オプションを指定するのを忘れずに。補足: 一度
--path
オプションを付けてbundle install
を実行すると、.bundle/config
が作成されて設定が追加されるため、次回以降bundle install
の際に--path
オプションを付ける必要はありません。$ docker-compose run --rm app bundle install --path vendor/bundle Creating network "hogehoge_default" with the default driver Fetching gem metadata from https://rubygems.org/. Resolving dependencies... Fetching bcrypt 3.1.13 Installing bcrypt 3.1.13 with native extensions Using bundler 2.0.2 Bundle complete! 1 Gemfile dependency, 2 gems now installed. Bundled gems are installed into `./vendor/bundle`ちゃんとインストールできているか確認してみましょう。
$ docker-compose run --rm app bundle exec gem list *** LOCAL GEMS *** bcrypt (3.1.13) bundler (2.0.2)できていますね。
次回以降もGemfileを編集した際にはdocker-compose run --rm app bundle install
するだけで大丈夫です。
docker-compose build
し直す必要はありません。試しにRubyスクリプトを実行してみよう
test.rb
を作って適当にbcrypt
を使ってみます。test.rb# vendor/bundle配下から読み込むようにしてくれる require 'bundler/setup' # Gemfileの中のGemを一発でrequireしてくれる Bundler.require # NOTE: ↑上の2つはRailsの場合は勝手にやってくれるため必要ないです↑ puts BCrypt::Password.create('password')$ docker-compose run --rm app ruby test.rb $2a$12$xWXitLplfvcIuxUdTg.1I.bb/Jo0btGGnqWE02ZiMFsne.hDQXaDW実行できましたね。
1つ問題点が!!
現状だとインストールしたGemはローカルの
vendor/bundle
ディレクトリ配下に配置されます。
docker-compose run ...
を実行するたびにこのvendor/bundle
ディレクトリ配下が毎回マウントされるため、
Gemが増えてくるとdocker-compose run ...
を実行するたびにマウントに時間がかかり、コマンド実行が遅くなってしまいます。(この問題はDocker for Macを使用している場合のみ発生するらしいです)「毎回
docker-compose build
し直すのが面倒だからこうしたのに、本末転倒じゃねぇか!!」落ち着いてください。こんな時のためにDockerには
volume
という機能があるじゃないですか。
vendor/bundle
をボリュームに切り出す
docker-compose.yml
を以下のように修正するだけで解決します。docker-compose.ymlversion: '3' services: app: build: context: . dockerfile: containers/Dockerfile environment: BUNDLE_APP_CONFIG: /hogehoge/.bundle # これがないとGemを`vendor/bundle`以下から読み込んでくれないので注意 volumes: - .:/hogehoge # ↓追加↓ - bundle:/hogehoge/vendor/bundle # ↓追加↓ volumes: bundle: driver: localローカルの方の
vendor/bundle
配下のファイルはもう必要ないため削除しちゃいましょう。$ rm -rf vendor/bundle/*ボリュームとして切り出したら改めて
bundle install
し直しましょう。$ docker-compose run --rm app bundle install Creating volume "hogehoge_bundle" with local driver Fetching gem metadata from https://rubygems.org/. Fetching bcrypt 3.1.13 Installing bcrypt 3.1.13 with native extensions Using bundler 2.0.2 Bundle complete! 1 Gemfile dependency, 2 gems now installed. Bundled gems are installed into `./vendor/bundle`これで
vendor/bundle
ディレクトリ配下はbundle
ボリュームとして切り出されて毎回ローカルからマウントされることがなくなるため、docker-compose run
で余計な時間がかかることはなくなります。注意しておくこと
Gemのコマンドを使う際には
bundle exec
を付け足すのを忘れないようにしてください。
こんな感じで↓$ docker-compose run --rm app bundle exec rpsecえ?
docker-compose run --rm app
だけでも長いのにbundle exec
まで毎回付けるのは面倒くさいって?
alias設定するなりMakefile
使うなりやりようはいくらでもあるじゃないですか。おわりに
僕自身エンジニア歴1年ちょっと、Dockerを使い始めて2ヶ月程度なので知識不足が否めません。
見当違いの事を言っている可能性も十分にあります。
誤った表現や設定等ありましたらコメントにてご指摘をお願いします。Docker便利ですね!
- 投稿日:2019-12-02T19:40:40+09:00
そのマイグレーション、戻せますか?
Railsを使うときに、マイグレーションは便利ですが、戻すときのことまで考えて実装していますでしょうか。
bin/rails db rollback
bin/rails db migrate
でマイグレーションを実行できますが、逆にbin/rails db rollback
で戻すこともできます。ただし、マイグレーションが戻ることを前提にしている必要があります。考えなくて良いパターン
create_table
やadd_column
など追加系のメソッドをchange
に書いていた場合、逆向きは「追加したものを削除する」ことが明らかですので、自分で書かなくても対応してくれます。戻し方を書けば対応してくれるパターン
drop_table
やremove_column
を行う場合、ただ削除する内容を書くだけでは、戻し方がわかりません。そこで、これらのメソッドはcreate_table
やadd_column
と同様に元の定義を書けるようになっています。ロールバック時にはそれが使われます。戻さない・戻せないパターン
たとえば、
NULL
の列をNOT NULL
に変えるとか、utf8
だった列をutf8mb4
に入れ替えるとか、int
の列をstring
に変えるとか、値域を広げる方向にマイグレーションをかけた場合、元に戻そうにも広がった後のデータが入らないことがあるので、戻しようがありません。このような場合は
up
だけ書いて、down
はraise ActiveRecord::IrreversibleMigration
とすることで、「戻せない」ことをコードで明示できます。また、現実問題としてデプロイ後にマイグレーションをロールバックするような運用は通常行いませんので、ある程度以上複雑なマイグレーションを書いた場合に、戻す方まで厳密に書かずに
raise ActiveRecord::IrreversibleMigration
で済ます、という手段も、状況によってはありかもしれません。戻す側が不要になる場合
時には、戻す側で何もしなくていいことがあります。たとえば、「あるカラムに空文字列と
NULL
が混在しているので、全てを一方に揃える」というようなマイグレーションを立てたとします。これのdown
側は、特に何もしなくても元のデータと整合するので、放置して構いません。このような場合、def down # 戻す側で何もしなくていい理由 endのように、コメントで間違いではないことを明記しておきましょう。
- 投稿日:2019-12-02T17:25:14+09:00
Railsでmodelに動的なカウンターをつける
要約
railsで一覧画面に何かしらの集計値とかフラグを表示したいときがあると思います。
いいね数とかそういうの。
それをcounter_cacheとかでデータとして持たずに、動的に集計してきれいに組み込む方法です。やり方
class Post < ApplicationRecord has_many :likes, as: :likable end class Like < ApplicationRecord belongs_to :likable, polymorphic: true end @posts = Post.all.select("posts.*, (select count(*) from likes l where l.likable_type='Post' and l.likable_id=posts.id) as likes_count") @posts.first.likes_count # as xxxがattributeになる => 3サブクエリ
(select count(*) from likes l where l.likable_type='Post' and l.likable_id=posts.id)
の中は好きなように書けるので、かなり汎用性高くフラグなりなんなりつけられるので、これだけ覚えておけば集計付き一覧はだいたいできると思います。個人的にrailsのjoin+preload/eager_load/includesなどを使ってrailsのオブジェクトとしてしっかりつくる、という方法にこだわってきたのですが、これだとassociationの書かれ方に影響されて毎回違う書き方になってしまいます。フラグがひとつふたつ付けばいい場合にはやりすぎになってしまいがちです。
一方で、この書き方は多少生のsql書いてしまいますが、使い回しが効くのではないかと思います。
- 投稿日:2019-12-02T16:43:43+09:00
[Rails] CircleCIでRspecを回すと「Please call Stripe() with your publishable key. You used an empty string.」というエラーが出る
問題
注文画面から「決済方法を選択する」をクリックし、決済方法選択画面に各種支払方法(クレカ、PayPal、銀行振込)が
表示されているかを確認するためfeature specを作成しました。ローカルでパスしたのでCIで確認したところ、このようなエラーがでました。
Capybara::Webkit::JavaScriptError: {:line_number=>1, :message=>"IntegrationError: Please call Stripe() with your publishable key. You used an empty string.", :source=>"https://js.stripe.com/v3/"}エラーをみてみる
Please call Stripe() with your publishable key.
You used an empty string."どうやらStripe()という関数?をpublishable keyと一緒に呼ばなければならないようです。
そもそもStripe()って何かわからなかったので、
公式ドキュメントをみたところ、発見しました。
https://stripe.com/docs/stripe-js/reference#stripe-functionやっぱり関数ですね。
そしてUse Stripe(publishableKey[, options]) to create an instance of the Stripe object.
とあるように、publishable keyを引数に渡してインスタンスを作る関数のようです。
となると、このエラー文は
1. Stripe()が呼べていない
2. publishable keyが存在しない
ことを意味しているように思われます。しかし
ローカルのENVにはしっかりとpublishable_keyが定義されていて、そちらもcredentialsで参照されています。
だからpublishable keyも存在するはず。そしてStripeMock
を使用しているので、Stripe()も呼ばれているはず。
謎は深まるばかりでした。結論
CircleCIにはCircleCIの環境変数があります。
そこでpublishable keyが定義されていませんでした。だから、CI上だけテストが落ちていました。
なので、新しく環境変数を定義してかつそれをテストで使用するような場合は
CircleCIの環境変数にも別途追加するようにしましょう、というお話でした。
- 投稿日:2019-12-02T15:33:01+09:00
Railsの新卒研修で役に立つもの一覧
この記事はZeals Advent Calendar 2019の3日目の記事です。
はじめまして。ZealsでRailsエンジニアとして働いている鈴木です。
プログラミングの初心者向けの方ように、新卒研修のカリキュラムを作成しました。
そのカリキュラム作成時に、役に立ったサイトや書籍を、紹介させていただきます。役に立った本一覧
なぜオブジェクト指向で作るのか
オブジェクト指向について1からわかりやすく書いているため、プログラミング言語の入門本でオブジェクト指向を知ったけど、よくわからないという人がオブジェクト指向について理解するために適しているので課題図書としてピックアップしました。
プロを目指す人のためのRuby入門
Rubyについてわかりやすく一通りのことを書いてあるため、Rubyの言語仕様をわかりやすく理解するのに一番適しているだろうということで課題図書としてピックアップしました。
現場で使える Ruby on Rails 5速習実践ガイド
万葉さんが公開している研修の参考本ということと、Railsで開発する時に必要になることを一通り解説しているということでこの本をピックアップしました。
Everyday Rails - RSpecによるRailsテスト入門
Everyday Rails - RSpecによるRailsテスト入門
Railsで開発をしていくためには、Rspecを使いこなすのが必要不可欠だと思います。
この本はFactoryBotやフィーチャースペックまで解説しているため課題図書としてピックアップしました改訂第3版 SQL書き方ドリル
改訂第3版 SQL書き方ドリル
SQLを理解するには、直接手を動かした記述していくほうが理解しやすくなるため課題図書としてピックアップしました。サルでもわかるGit入門
無料でGitのコマンドだけではなく、プルリクエストのやりとりまで解説しているため
この資料をピックアップしました。el-training
el-training
株式会社万葉さんの新卒研修の課題です
Railsアプリのタスク管理システムを開発していくので、実践的な課題になると思いピックアップしました。TDD Boot Campの課題
TDD Boot Campの課題一覧
Rubyやテストコードを書いていくのには、ある程度大きな問題になれる必要があります。
TDD Boot Campの課題は、TDD初心者がTDDに慣れるということを考えて問題設計をしていると思いピックアップしました最後に
以上の本やサイトがRailsでシステムを開発していくために、勉強しておいたほうがいいと思った本です。
今回リストアップしたのがこれから、Railsを勉強している人や新卒研修を考えている人の参考になれば幸いです。明日の担当者は、aburdさんです。 ぜひお楽しみにしてください。
- 投稿日:2019-12-02T13:41:58+09:00
Rails: can not be used with :count => 1. key ‘one’ is missing.
背景
オリジナルで制作中、Rails: can not be used with :count => 1. key ‘one’ is missing.のエラーがでた。
環境
Rails 5.2.3
Ruby 2.6.3解決策
おそらくenumが何か関係している
下記でラジオボタンのところで問題が発生していると予想
<li class="gender"> <%= form.radio_button :gender, "男性" %> <%= form.label :gender_men, "男性", class: "mens" %> <%= form.radio_button :gender, "女性" %> <%= form.label :gender_female, "女性", class: "females" %> </li>ここのモデルの記述を確認
enum gender: {男性: 0, 女性: 1} validates :gender, inclusion: {in: ["男性", "女性"]}ja: activerecord: attributes: staff: content: 自己紹介 gender: one: gender *この記述が必要だとのこと true: 男性 false: 女性 gender: 性別
- 投稿日:2019-12-02T12:54:21+09:00
hamlの基本とよくあるエラー集
◆概要
Ruby on Rails学習者向けのhamlの解説です。
初学者の方は基本的なclassなどの書き方は勿論ですが、特に後半の「hamlのよくあるエラー集」を参考にしていただければと思います。◆hamlって?
html.erbの派生版。
少し難易度は上がるもののコードの量が減りhtml.erbに比べて読みやすいコードになりやすい。
Railsで使用する際にはhaml-railsというgemが必要。●これがhtml.erb
<div id="information"> <% if controller.action_name == "index" %> ここはトップページです。 <% else %> ここはトップページではありません。 <% end %> </div> <div class="back-gray border-black" id="menu"> <%= link_to "トップページへ", root_path %> <br> <%= link_to hoge_path do %> hogeページへ <% end %> </div>●これがhtml.haml
#information -if controller.action_name == "index" ここはトップページです。 -else ここはトップページではありません。 #menu.back-gray.border-black = link_to "トップページへ", root_path %br = link_to hoge_path do hogeページへ●hamlのメリット
- 閉じタグ(</hoge>)や<% end %>を記述しなくて良いので記述量が減る。
- 閉じタグ忘れなどの些細なミスが減る。
- インデント(半角スペース)のルールが厳しいので、コードが汚くなりにくい。
●hamlのデメリット
- html.erbの頃では出なかったエラーが出るので最初はエラーが頻出する。
●Q. hamlってエラー出やすくなるだけだしerbで良くない?
●A. 確かに最初は戸惑いますがこの記事の内容が頭に入っていればそうそう躓くことはなくなるので慣れましょう!
◆hamlの基本
●タグ、class、IDの指定の仕方
htmlの場合<p class="hoge fuga" id="foo"> こんにちは <br> いいお天気ですね </p>hamlの場合%p#foo.hoge.fuga こんにちは %br いいお天気ですね
- タグを指定する際は「%タグ名」と書く(省略するとdivタグになる)。
- classを指定する際は「.class名」と書く。複数のclassを設定する場合は.class名を繋げる。
- idを指定する際は「#id名」と書く。
●タグ、class、idの書き方一覧
html.erbの場合 html.hamlの場合 タグ <p></p> %p タグ+class <p class="hoge fuga"></p> %p.hoge.fuga タグ+id <p id="fuga"></p> %p#fuga タグ+id+class <p id="fuga" class="foo bar"></p> %p#fuga.foo.bar ●ネスト(入れ子)の仕方
htmlの場合は以下のようにネストしました。
htmlの場合<p> <span> ほげほげふがふが <span> </p>hamlの場合は以下のようにネストしたいものの行のインデントを下げます(半角スペースを入れます。)
hamlの場合%p %span ほげほげふがふが●viewファイル内でRubyのコードを使う。
viewファイル内でRubyの処理を使いたい場合、erbでは以下のように
<%= %>
や<% %>
を使いました。erbの場合<% fruits = ["りんご", "みかん", "ぶどう"] %> <% fruits.each do |fruit| %> <%= fruit %> <% end %>hamlの場合は以下のように書くので
<% end %>
は不要です(書かないだけで存在はしています)。hamlの場合- fruits = ["りんご", "みかん", "ぶどう"] - fruits.each do |fruit| = fruit
html.erbの場合 html.hamlの場合 使い分け 画面に出力したくないもの <% if user_signed_in? %> - if user_signed_in? if, eachなど 画面に表示したいもの <%= current_user.name %> = current_user.name form_with, renderなど ◆hamlのよくあるエラー集
hamlではerbと違って文法を間違えるとすぐにエラーが起きます。エラーは大抵の場合インデントに依るものです(もしくはカンマなど)。
主要なものを紹介します。●パターン①:全体のインデントが統一されていない。
インデントの増やしていく数は、最初のインデントによって決まります。例えば最初にネストした際にインデントを2つ下げた場合、以降の行でも2つずつ下げる必要があります。途中で変更するとエラーが出ます。インデントは半角スペース2つずつで統一すると覚えておきましょう。
2行目はインデントが2つに対して、4行目、6行目はインデントが1つになっている.hoge .fuga .foo .bar .abc .def・実際のエラー画面
●パターン②:途中で一気にインデントが増えている。
インデントは徐々に増やしていく必要があります。例えば最初にインデントを2つ下げた場合、以降も2つずつ下げましょう。途中で一気に3つ4つ増やすとエラーが起きます。
2行目はインデントが2つに対して、3行目で一気に4つ増えている。.hoge .fuga .foo・実際のエラー画面
●パターン③:ネストしてはいけないものでネストしている、もしくはネストするべきものでネストしていない。
hamlでは
<% end %>
は記述しませんが省略しているだけで存在はしています。ネストを誤るとendの数がおかしくなります。
SyntaxErrorが出た際に以下のようなメッセージが含まれている場合はendの数がおかしくなっています。endが多いsyntax error, unexpected ensure, expecting end-of-input ensureendが足りないsyntax error, unexpected end-of-input, expecting endそれではパターンをいくつか見ていきましょう。
ネストしてはいけないものでネストするとエラーが発生します。
ネストしてはいけないものでネストしている①= render 'foo' .hogeネストしてはいけないものでネストしている②= form_for @message do |f| = f.text_field :text .hoge●実際のエラー画面
ネストするべきものでネストしていない場合もエラーが発生します。doがある場合は何らかをネストする必要があります。
form_forはネストしなければならない= form_for @message do |f| = f.text_field :texteach~doはネストしなければならない- fruits = ["りんご", "みかん", "ぶどう"] - fruits.each do |fruit| = fruitlink_to~doはネストしなければならない= link_to root_path do トップページへ●実際のエラー画面
文字列で何かをネストした際にもエラーが発生します。
文字列でネストしてはいけない①.hoge こんにちわ .fuga おはよう文字列でネストしてはいけない②.hoge こんにちわ .fuga おはよう●実際のエラー画面
「文字列でネストしてはいけない①と②」は書き方が違うだけで内容は同じです。②のような書き方(要素と同じ行に文字列を書く)はエラーを起こしやすいので避けましょう。
●パターン④:カンマに過不足がある。
haml特有というよりはerbでも発生しますが紹介します。
「:text」の後ろにカンマ(,)が必要= form_for @message do |f| = f.text_field :text placeholder: "ほげほげ"●実際のエラー画面
- 投稿日:2019-12-02T12:32:53+09:00
Rails6 のちょい足しな新機能を試す109(while_preventing_writes 編)
はじめに
Rails 6 に追加された新機能を試す第109段。 今回は、while_preventing_writes 編です。
Rails 6 では、while_preventing_writes
が追加されました。multi-db 関連のメソッドで、 while_preventing_writes のブロック内では、 DBへの書き込みができません。
同じDBに対してDBのConnection を別に作成しても書き込みはできないようになっています。Ruby 2.6.5, Rails 6.0.0 で確認しました。
$ rails --version Rails 6.0.0今回は、簡単なスクリプトを作って確認します。
Rails プロジェクトを作成する
$ rails new rails_sandbox $ cd rails_sandboxUser モデルを作成する
User
モデルを作成します。$ bin/rails g model User name
User モデルを編集する
User モデルのDBの Connection を
ActiveRecord::Base.connection
とは別になるように変更します。app/models/user.rbclass User < ApplicationRecord connects_to database: { writing: :primary, reading: :primary } end動作確認のスクリプトを作成する
ActiveRecord::Base.connection
とUser.connection
が違うことを確認し、while_preventing_writes
のブロック内では、DBへの書き込みができないことを確認します。scripts/while_preventing_writes.rbputs User.count if ActiveRecord::Base.connection.object_id != User.connection.object_id puts 'ActiveRecord::Base.connection != User.connection' end # ActiveRecord::Base.connection.while_preventing_writes do # Rails 6.0.0.rc1 ActiveRecord::Base.connection_handler.while_preventing_writes do # Rails 6.0.0 User.create!(name: 'Taro') end puts User.countマイグレーションを実行する
$ bin/rails db:create db:migrate
スクリプトを実行する
スクリプトを実行します。
Write query attempted while in readonly mode:
のメッセージが出力され、書き込みが失敗することがわかります。$ bin/rails runner scripts/while_preventing_writes.rb Running via Spring preloader in process 81 0 ActiveRecord::Base.connection != User.connection Traceback (most recent call last): ... /usr/local/bundle/gems/activerecord-6.0.0/lib/active_record/connection_adapters/postgresql_adapter.rb:643:in `execute_and_clear': Write query attempted while in readonly mode: INSERT INTO "users" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" (ActiveRecord::ReadOnlyError)ちなみに
Rails 6.0.0rc1 では、エラーにならず、保存できてしまいます。(6.0.0rc1 では、メソッドの定義場所が異なるため、
ActiveRecord::Base.connection.while_preventing_writes
とする必要があります。)試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try109_while_preventing_writes参考情報
- 投稿日:2019-12-02T11:43:05+09:00
Reformで親子関係のあるフォームオブジェクトを作ってみる
Ateam cyma Adevent Calendar 2019、4日目です!
本日は株式会社エイチームでcymaのエンジニアの @bayasist が務めさせていただきます。Ruby on Railsで書かれたプログラムでは、フォームオブジェクトを利用することで、分かりやすく書くことができる場合があります。RailsでFormObjectを簡単に作れるtrailblazerというgemのReformというものがあり、使いこなせばなかなか便利です。
フォームオブジェクトが楽に作れるようにいくつか機能はありますが、ドキュメント(特に日本語)が少ないので、今回は特に親子関係をもつデータのReformを用いたフォームオブジェクトの作り方を解説できたらと思います。フォームオブジェクトを利用する利点
フォームオブジェクトはmodelをそのままformにするのではなく、バリデートなどformに関係する処理を行うオブジェクトです。これらのオブジェクトを作成することで、以下のようなメリットがあります。
- 複数モデルをまたがったFormの処理をコントローラに書かなくても済む
- ActiveRecordのモデル以外のデータの更新などでも同じようなお作法でView,Controllerが書ける
Reformのメリット
Reformは下記のような特徴があります
フォームオブジェクトを簡易的に作成できるtrailblazerというgemのReformというものがあります。それを用いると下記のようなメリットを受けることができます。
- ActiveModelと同じような記法でValidateや要素などが記載できる
- ActiveModel以外のデータの読み書きでも同様の記法で扱える
- 親子関係など多少データ構造が多少複雑になってもプログラムが複雑になることはない
まずはReformでフォームオブジェクトの基本形を作る
まずは、一つのmodelのみでReformを使ったフォームオブジェクトを作っていきます。nameカラムを持ったparentというモデルを更新するだけのものを作ります。(あとでchildというモデルをparentの子供にします)
app/models/parent.rbclass Parent < ApplicationRecord endapp/forms/parent_form.rbclass ParentForm < Reform::Form property :name validates :name, length: { maximum: 5} endapp/controllers/parent_controller.rbclass ParentController < ActionController::Base def edit @form = form end def update @form = form if @form.validate(update_param) @form.save end end private def form ParentForm.new(Parent.find(params[:id])) end def update_param params.require(:parent).permit(:name) end endapp/views/parent/edit.html.rb<%= form_with model: @form do |form| %> <%= form.text_field :name %> <%= form.submit %> <% end %>formオブジェクトができました。
Controllerを見ていただきたいのですが、ほとんどActiveRecordのお作法で書くことができます。
一点大きく違うところはvalidateの部分。Reformでは、validateメソッドでパラメータのバリデートをし、フォームオブジェクトの各要素ににパラメータを渡していきます。
またsaveメソッドでは、フォームオブジェクトの値をもとのモデルに渡してから、モデルのsaveメソッドが呼び出されています。モデルのsaveメソッドを呼び出さず、元のモデルへのデータの移動のみをやりたい場合はsyncメソッドを用います。parentモデルにchildという子要素を作る
parentモデルの子要素としてchildモデルを作ります。
app/models/parent.rbclass Parent < ApplicationRecord has_many :children endapp/models/child.rbclass Child < ApplicationRecord belongs_to :parent endapp/forms/parent_form.rbclass ParentForm < Reform::Form property :name validates :name, length: { maximum: 5} collection :children, populate_if_empty: Child do property :name validates :name, length: { maximum: 5} end endapp/controllers/parent_controller.rbclass ParentController < ActionController::Base def edit @form = form end def update @form = form if @form.validate(update_param) @form.save end end private def form ParentForm.new(Parent.includes(:children).find(params[:id])) end def update_param params.require(:parent).permit(:name, children_attributes: [:name]) end endapp/views/parent/edit.html.rb<%= form_with model: @form do |form| %> <%= form.text_field :name %><br /> <%= form.fields_for :children do |child_form| %> <%= child_form.text_field :name %><br /> <% end %> <%= form.submit %> <% end %>Formオブジェクトでcollectionを使うこと以外はActiveRecordを使用したときとほとんど変わらずに実装できます。
複数モデルのバリデーションや保存処理はフォームオブジェクトが引き受けるため、モデルが複数になってもControllerが散らかったりすることなく記述できます。また、Reformではそれらの処理をほとんど書くことなく行えます。上記プログラムの重要な問題点
上記プログラムではChildのアップデートの際に、Textboxの値を上からDBで検索された順にあてはめていきます。/editの表示からアップデートまでにほかのブラウザなどでChildの一部要素がdeleteやinsert等されると予期せぬ動作につながります。
そこで、/editの表示の際にHiddenFieldにchildのidを入れておき、保存の際にChildのidとPOSTで送られてきたidを突合しながら保存していく必要があります。
そのために下記のプログラムを変更する必要があります。app/controllers/parent_controller.rbclass ParentController < ActionController::Base # (中略) def update_param params.require(:parent).permit(:name, children_attributes: [:id, :name]) end endapp/views/parent/edit.html.rb<%= form_with model: @form do |form| %> <%= form.text_field :name %><br /> <%= form.fields_for :children do |child_form| %> <%= child_form.text_field :name %><br /> <%= child_form.hidden_field :id %> <% end %> <%= form.submit %> <% end %>app/forms/parent_form.rbclass ParentForm < Reform::Form property :name validates :name, length: { maximum: 5} collection :children, populate_if_empty: Child, populator: ->(fragment:, **) { children.find_by(id: fragment["id"].to_i) } do property :name validates :name, length: { maximum: 5} end endこれを行うことでHiddenFieldのidとDBのIDが同一のものを更新するようになります。ほかのブラウザで該当レコードが削除されていた際はvalidateを行う際にエラーとなり、ほかの関係ないデータを更新しに行くということはありません。
ここではfragmentはvalidateの際に送られてきたデータ(今回でいうとformで入力したデータ)、childrenはDBから持ってきたデータとなります。
この機能を用いれば、複合キーなどID以外で突合することもできます。例)
app/forms/parent_form.rbclass ParentForm < Reform::Form property :name validates :name, length: { maximum: 5} collection :children, populate_if_empty: Child, populator: ->(fragment:, **) { children.find(***_id: fragment["***_id"].to_i, ~~~_code: fragment["~~~_code"].to_i) } do property :name validates :name, length: { maximum: 5} end endChildのFormを外だしする
Childが複雑になってきたら、Childのフォームを別のファイルに移したくなるかもしれません。そのようにFormObjectの子要素を外に出すことも可能です。
app/forms/parent_form.rbclass ParentForm < Reform::Form property :name validates :name, length: { maximum: 5} collection :children, populate_if_empty: Child, populator: ->(fragment:, **) { children.find_by(id: fragment["id"].to_i) }, form: ChildForm endapp/forms/child_form.rbclass ChildForm < Reform::Form property :name validates :name, length: { maximum: 5} end他にもいろいろなことができます
Reformを使うことで、子要素の追加や削除、デフォルト値の設定等がModel,Controllerを大きく汚すことなく比較的簡単に行えます。また、ActiveRecord以外のインスタンスにも利用できるため、FormからDBと関係ないインスタンスにデータを移すときに重宝します。
もっと調べてみたい人は公式ドキュメントを見てみてくださいね。最後に
Ateam cyma Adevent Calendar 2019 の 4日目、いかがでしたか。
5日目は cymaのインフラつよつよエンジニアの @ihsiek がSQLのチューニング入門の記事を書くそうですよ!SQL苦手な人、早く動くSQLを書きたい人は必見ですよ!株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。
エンジニアとしての働き方に興味を持たれた方はcymaの Qiita Jobs をご覧ください。
そのほかの職種は、エイチームグループ採用サイトをご覧ください。
- 投稿日:2019-12-02T11:16:09+09:00
rails-tutorial第7章
Restfulってなんだ?
RESTfulなアーキテクチャの場合、URLは同一のものを使うかわりに、HTTPリクエストメソッドにそれぞれ、GET、POST、PATCH、DELETEを使って異なるアクションに結びつけるようです。
こうすることでURLを名詞とし、HTTPリクエストメソッドを動詞とすることができ
シンプルにURLをいろいろなアクションに結びつけることができるようになります。ところで、現在のブラウザにはPATCHやDELETEといったメソッドはないようです。
Railsは存在しないHTTPメソッドを存在するかのように見せかけ
RESTfulなアーキテクチャの実装を実現しているようです。なんでRestfulなアーキテクチャがいいのか?
Progateのrailsアプリを作った時もそうだったけど、
現在使われているHTTPリクエストメソッドは、GETとPOSTのため
普通は、showアクションは/users_show/1、updateアクションは/users_update/1、
destroyアクションは/users_destroy/1とかのURLを考えますよね。昔Django(pythonのwebフレームワーク)でアプリを作った時は実際にそうしてました。
でもそうするとurlを管理するファイルの中がアクションの数によっては大変なことになったのを
記憶しています。これをresources :users
とすることで、ファイルを見やすくすることができる。ユーザー登録機能を実装する
まずは開発環境でのみデバッグ情報を表示するようにしてみよう
app/views/layouts/application.html.erb<!DOCTYPE html> <html> . . . <body> <%= render 'layouts/header' %> <div class="container"> <%= yield %> <%= render 'layouts/footer' %> <%= debug(params) if Rails.env.development? %> </div> </body> </html><%= debug(params) if Rails.env.development? %>
ここではデバッグメソッドが使われている。paramsでデバッグ情報を受け取り、開発環境でのみそれを表示させる。putsメソッドと似ている。ついでに、後置if文を使うときは1行で済む時に使われることが多い。2行以上の時は前置if文を使おう。ルーティングを設定しよう
config/routes.rbRails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' resources :users endここで、どのurlがどのアクションに対応するのか知りたい。
そんな時は、$ rails routes
このコマンドを打つと、
ec2-user:~/environment/sample_app (sign-up) $ rails routes Prefix Verb URI Pattern Controller#Action root GET / static_pages#home help GET /help(.:format) static_pages#help about GET /about(.:format) static_pages#about contact GET /contact(.:format) static_pages#contact signup GET /signup(.:format) users#new users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroyというように、urlと対応するアクションを調べることができる。
PUTリクエストとPATCHリクエストの違いって?
どちらもupdateアクションを指定しているが、PUTリクエストは基本的に全ての情報を更新する時に使う。PATCHリクエストは全部、または一部の情報を更新する時に使う。そのため情報の更新にはPATCHリクエストの方が適切であると言える。
ローカル変数とインスタンス変数
user ローカル変数
ローカル変数のスコープはメソッド内。@userはインスタンス変数
インスタンス変数は、method外、例えばviewで使うことができる。Userリソースのshowアクションを実装
app/controllers/users_controller.rbclass UsersController < ApplicationController def show @user = User.find(params[:id]) end def new end endshowアクションが実行される条件は、GET /users/:id
そのため、showアクションが実行される時は必ずidがurlに含まれている。
paramsにはハッシュで情報が保存されるので、params[:id]という書き方で情報を取得する。params[:id]超わかりやすく解説
・User.new ~ @user.saveでインスタンスが保存される。
・@userは {id: 1}というハッシュの情報を持っている。
・で、例えば/users/1というurlにアクセスしたとする。
・このurlは/users/:idという型に当てはまるので{id: 1}という前提でshowアクションが呼び出される。
・重要なのは、urlにアクセスすると、{id: 1}という情報が送られ、それをparamsで取得することができるということ。debuggerメソッド
app/controllers/users_controller.rbdef show @user = User.find(params[:id]) debugger end def new end enddebuggerメソッドはブレイクポイントみたいなもの。
そこで処理を止めて何が起きてるかrails sをしたターミナルに表示される。ユーザー登録機能を作ろう
form_forを使ったUser登録フォーム
app/views/users/new.html.erb<% provide(:title, 'Sign up') %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.label :email %> <%= f.email_field :email %> <%= f.label :password %> <%= f.password_field :password %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation %> <%= f.submit "Create my account", class: "btn btn-primary" %> <% end %> </div> </div>例えば、
<%= f.label :email %>
<%= f.email_field :email %>
は、ユーザーがemail欄に書いたvalueを:emailというキーに対応させますよーって意味。<%= f.label :password_confirmation, "Confirmation" %>
はpassword_confirmationという文字列だと長いので、"Confirmation"に上書きするよーって意味。
表示される文字がConfirmationになる。<%= f.submit "Create my account", class: "btn btn-primary" %>
このボタンを押すと、フォームの中身がparamsに代入される。createアクションを見てみよう
app/controllers/users_controller.rbdef create @user = User.new(params[:user]) # 実装は終わっていないことに注意! if @user.save # 保存の成功をここで扱う。 else render 'new' end end本来User登録は、
User.new(name: ~~, email: ~~....)という感じ。で、paramsには
{user: {name:, email:}}というハッシュが代入されている。
なので、params[:user]とすれば、:userをキーとするハッシュが代入されてインスタンスを作れるが、、、、、このままだとクラッキングされてしまう。
{ admin: true }などのメッセージを入れられると、管理者権限を付与することになってしまう。そこで、、
Strong Parametersを使おう
app/controllers/users_controller.rbclass UsersController < ApplicationController . . . def create @user = User.new(user_params) if @user.save # 保存の成功をここで扱う。 else render 'new' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endこうすることで、permitで指定したキー以外は扱わないよー、変なキーと値が入ってたら弾くよーってしている。
エラーメッセージを出そう。
validationに引っかかって登録に失敗した時、errors.full_messagesオブジェクトは、エラーメッセージの配列を持っています。
なので、
>> user.errors.full_messages => ["Email is invalid", "Password is too short (minimum is 6 characters)"]このように、失敗した理由を出すことができる。エラー要因が複数あれば複数渡してくれる。
登録フォームのviewにエラーメッセージを表示させよう。
app/views/users/new.html.erb<% provide(:title, 'Sign up') %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Create my account", class: "btn btn-primary" %> <% end %> </div> </div>render 'shared/error_messages'これはエラーメッセージを表示するviewをパーシャル化しますよーってこと。
ディレクトリの作成
$ mkdir app/views/shared
$ touch app/views/shared/_error_messages.html.erbapp/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 %>signup(失敗時)の統合テストを書いていこう。
$ rails generate integration_test users_signup
インテグレーションテストの名前の付け方は、この場合、signupという一連の動作を確認するものだから、users_signupとしている。
test/integration/users_signup_test.rbrequire 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest test "invalid signup information" do get signup_path 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' end endpost users_pathは/usersにpostリクエストを送っていますよー。
その際に、params: { user: ~~~}を送ってますよーって意味。上記のテストはユーザー登録が失敗することを期待している。
じゃあ、どうやってそれを判断するのか?
test/integration/users_signup_test.rbassert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } endassert_no_difference は引数(この場合、User.count)がdo end を実行する前と後では変更ないよね?っていうアサーション。
この場合、validationを設定しているのでuserインスタンスは登録されず、テストは通る。
ユーザー登録成功
まずはcreateアクションの中身を埋めよう
app/controllers/users_controller.rbclass UsersController < ApplicationController . . . def create @user = User.new(user_params) if @user.save redirect_to @user else render 'new' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endredirect_to @userこれ気になる。
rails routesを見てみよう
user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy/users/:id は名前付きルートでuser_pathで表すことができる。
本来は user_path(@user.id)で実現できる。
しかし、user_pathはデフォルトで:idに値を入れて渡すそのため、user_path(@user)というように引数を渡すことでuserインスタンスの情報を渡すことができる。
これをさらに省略すると、
redirect_to @user というようにredirect_toメソッドの引数に直接Userオブジェクトを渡すことで、showアクションへリクエストできる。補足すると、redirect_toは基本的に指定したurlにgetリクエストを送るという考えでいいと思う。
flash 成功時に一時的なメッセージを出そう!
flashは特殊な変数で、使いたい時はflashという特殊な変数が最初から用意されていると考えるとわかりやすい。実際はメソッド。
usersコントローラに実装してみよう
app/controllers/users_controller.rbclass UsersController < ApplicationController . . . 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 private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endflashはキーと値を設定すると、それが次のリクエストまで残り、次の次のリクエストが来た時に消えてくれるという特徴をもつ。
flashメッセージを画面に表示するには?
flashはいろいろなところで使われるので、共通のapplicationテンプレートに表示するためのコードをかくと便利。
具体的には、
app/views/layouts/application.html.erb<!DOCTYPE html> <html> . . . <body> <%= render 'layouts/header' %> <div class="container"> <% flash.each do |message_type, message| %> <div class="alert alert-<%= message_type %>"><%= message %></div> <% end %> <%= yield %> <%= render 'layouts/footer' %> <%= debug(params) if Rails.env.development? %> </div> . . . </body> </html><% flash.each do |message_type, message| %>には先ほどコントローラで設定したキーと値が入っている。
この場合、キーがmessage_type 値が、messageに代入されてeachメソッドが実行される。
先ほどは キーに :successを入れた。
実はbootstrapで alert-successというclassが元から用意されているため、キーをsuccessにした。これはcssによって、緑色の文字と縁を作る。
そのため、実際に表示されるのはmessageに代入された値だけとなる。成功時のテスト
test/integration/users_signup_test.rbclass 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 endfollow_redirect!は「POSTリクエストを送信した結果を見て、指定されたリダイレクト先に移動するメソッド」だそうです。ちなみに、このアプリケーションではユーザー登録がうまくいった場合そのユーザーのページ(users/show.html.erb)にリダイレクトするようにしています。assert_template 'users/show'はそれをチェックしているわけですね。
つまり、 redirect_toする前のテストの結果を精査してから、redirect_to後のテストに進みたい時に使うってこと?ch7には以下のように書いてある。
ここで、users_pathにPOSTリクエストを送信した後に、follow_redirect!というメソッドを使っていることに注目してください。このメソッドは、POSTリクエストを送信した結果を見て、指定されたリダイレクト先に移動するメソッドです。したがって、この行の直後では'users/show'テンプレートが表示されているはずです。
これが わかりやすい!!!!!!!
つまり、follow_redirect!は、assert_difference内で、users_pathへのPOSTリクエスト(URL:/users、アクション:create)を送信した結果(レスポンス)を見て、controllerで指定しているリダイレクト先のuser_url @user(ユーザ登録完了後のユーザ画面)へ移動している。
これにより、assert_template 'users/show'がテストされるのはpostリクエストがうまくいった時のみとなる。重要なのはredirect_toの前と後どちらもテストが存在するということ。
SSLを使ったデプロイ
SSLを使うと、http から httpsになる。
httpsは流れる情報が暗号化されるらしい。herokuのサブドメインの場合は問題ないが、自分で独自ドメインを設定する際はSSL証明書を発行する必要がある。
Railsではありがたいことに、本番環境用の設定ファイルであるproduction.rbのコードをたった1行変更するだけでSSLを強制し、httpsによる安全な通信を確立できます。具体的には次のリスト 7.36に示すように、config.force_sslをtrueに設定するだけで完了です。
config/environments/production.rbRails.application.configure do . . . # Force all access to the app over SSL, use Strict-Transport-Security, # and use secure cookies. config.force_ssl = true . . . endコメントアウトされてるので外してあげればOK
pumaの設定は7章見ながら設定すればOK
- 投稿日:2019-12-02T11:11:47+09:00
Classiの新卒エンジニア向け研修、「万葉研修」について
皆様こんにちは!この記事はClassi Advent Calendar 4日目の記事です。
新卒の小野優子(@yukoono)と申します。ポートフォリオチームで、高校生がやったことを記録し、振り返るための「ポートフォリオ」の開発を行っております。
5月にClassiにjoinして以来、合同会社Fjordさんの「Fjord boot camp」で2ヶ月の研修→社内で2ヶ月半の研修→チームに配属され業務へ、というフローで動いておりました。
今回は社内で受けた新卒研修、通称「万葉研修」について書いていきます。万葉研修とは
株式会社万葉さんがgithub上に公開している、Ruby on Railsのプログラマーになるための教育プログラムです。リンクはこちら。25ステップ+αあり、エンジニアとしてClassiにjoinした新卒メンバーは、この研修プログラムに2ヶ月程かけて取り組みます。メンターとして、「ゼロからわかるRuby超入門」の著者であるigaigaさん、12/21のTokyoGirls.rbで登壇されるただあきさんをはじめとした社内のエンジニアの皆様にお世話になりました。
大まかな流れ
プログラムのステップ3を終えた後、ステップ4で自分が作りたいアプリの仕組みやDB構造を考えます。私の場合は、「暇な時間を使って、普段先延ばしにしていることをやるアプリ」をコンセプトに置きました。利用の流れとしては、
1. アプリに筋トレや英語の勉強など、「やりたいけれど緊急ではない」ことを登録する
2. 暇な時間ができたら、何分程度暇なのかを登録する
3. 所要時間に合わせて、やることをアプリが提案する
4. 終わった後、記録をアプリに入力し、保存するという感じです。このアプリのペーパープロトタイピングやDB構造案を見せながら、メンターとどう実装するかを相談します。私の場合は最初にやりたいことをかなり盛りだくさんで考えていたため、「まずはやりたいことを登録するだけの、最小の機能で実装してみよう。研修が進むのに合わせて、コンセプトの要素を取り入れていこう」という話になりました。
このようにしてアプリの方向性を決定した後は、研修のステップに沿って実装していきます。方向性がそれぞれ違うため、同じ研修を受けていても出来上がるアプリは人によってかなり違ったものになります。例として、同期の@ruru8は社内で利用できる書籍サービスを作っていました。詳細は22日の投稿をお楽しみに。研修期間中の過ごし方
- プログラムに沿って、アプリに機能を実装する
- 取り組んでいたステップの実装が終わる、または途中でもキリのいい単位で実装が行えたら、Githubの自分のリポジトリにローカルの内容をpushする
- 見て欲しいところにコメントを書いてプルリクエストを出し、メンターにレビューをお願いする
- LGTM(Looks Good To Me…レビューがOKであるという意味)がもらえれば次の実装に取り掛かる。修正が必要な場合は、ローカルで修正を行い、pushして再レビューをお願いする
という感じです。
わからないことがあったときは、会社で買っていただいている参考書籍を読むか、メンターや社内の先輩に質問します。参考書籍についてはこちらにまとめられています。
また、Classiはコミュニケーションツールとしてslackを使っておりまして、個人が分報チャンネルを持ち、困りごとや思ったことなどをつぶやく文化が活発です。分報についてはこちら。私も自分の分報である「times_ono」というチャンネルに研修の進捗を書くようにしておりまして、ここで質問するとチャンネルを見ていただいている先輩からよくアドバイスを頂けます。これはtimes_onoの一幕でして、私がわからない!と言ったことに対して、同期の@hxrxchang君が席に助けに来てくれて、参考になるリンクも貼ってくれたところです。こんな感じで、気軽にSOSが出せて、助けてくれる人が社内にたくさんいる、すごく温かい環境です。
成果物
出来上がったアプリがこんな感じです。
私はレビューで時間をかけたため、ステップ4で考えていたコンセプトの実現は十分には行えず、シンプルに万葉研修のステップを進めたアプリ、という感じの成果物になりました。ただ、私の場合はレビューで得た知識が評価され、チームで研修内容の共有会を開かせていただきました。
研修のゴールは人それぞれです。私はステップ23まで+研修内容の共有会の開催がゴールになりましたが、今研修中のベトナム人の新卒二人は1ヶ月で早くもステップ25を完了し、オプション要件に取り組んでいます。また、フロントエンドのスキルがあるメンバーはRailsのプログラミングと並行してAngular jsで画面の作成を行い、操作性に優れた格好いいアプリを作っています。Classiの先輩方に協力してもらい、ユーザビリティテストを行なったメンバーもいます。
研修を終えて
研修を通して、「プログラマーの仕事の大部分は、人への気遣いである」という感想を持ちました。なぜなら、メンターからの指摘事項のほとんどは、「他のエンジニアがアプリをメンテナンスする時に分かりやすい/後で困らないコーディング」についてのアドバイスだったためです。
- こうした方が、他のプログラマーが分かりやすい!
- この方がRailsや他のgemのアップデートに関わらず使える!
- このコメントをつけた方が、レビュアーや他のエンジニアに親切!
などのコメントをたくさん頂くうちに、プログラマーの仕事像が「黙々と実装だけ行えれば良い、他の人とは関わらない」から、「他の人へ気遣い、情報を伝達し、理解して貰えるコードを書く」に変わっていきました。この意識は、現在チームで仕事をする上でも、チームメンバーへの気遣いや、伝わりやすいプルリクエストという形で活きています。
終わりに
この研修について社内の先輩エンジニアに話したところ、「羨ましい」という声を多く頂きました。2ヶ月以上かけてコードの書き方を教えてもらえる機会はとても貴重で、多くのプログラマーは仕事の中でコードの書き方やプルリクエストの仕方を覚えていくそうです。手厚い研修プログラムを組んでいただいたigaigaさん、ただあきさん、わからないところを教えて頂いた先輩エンジニアの皆さん、また充実した研修を受けさせて貰える会社の懐の広さに感謝しつつ、この投稿を締めます。
明日の投稿は@onigraさんです。お楽しみに。
- 投稿日:2019-12-02T11:11:47+09:00
Classiの新卒エンジニア向け研修について
皆様こんにちは!この記事はClassi Advent Calendar 4日目の記事です。
新卒の小野優子(@yukoono)と申します。ポートフォリオチームで、高校生がやったことを記録し、振り返るための「ポートフォリオ」の開発を行っております。
5月にClassiにjoinして以来、合同会社Fjordさんの「Fjord boot camp」で2ヶ月の研修→社内で2ヶ月半の研修→チームに配属され業務へ、というフローで動いておりました。
今回は社内で受けた新卒研修について書いていきます。研修内容
株式会社万葉さんがgithub上に公開している、Ruby on Railsのプログラマーになるための教育プログラムを使っています。リンクはこちら。
25ステップ+αあり、エンジニアとしてClassiにjoinした新卒メンバーは、この研修プログラムに2ヶ月程かけて取り組みます。メンターとして、「ゼロからわかるRuby超入門」の著者であるigaigaさん、12/21のTokyoGirls.rbで登壇されるただあきさんをはじめとした社内のエンジニアの皆様にお世話になりました。大まかな流れ
プログラムのステップ3を終えた後、ステップ4で自分が作りたいアプリの仕組みやDB構造を考えます。私の場合は、「暇な時間を使って、普段先延ばしにしていることをやるアプリ」をコンセプトに置きました。利用の流れとしては、
1. アプリに筋トレや英語の勉強など、「やりたいけれど緊急ではない」ことを登録する
2. 暇な時間ができたら、何分程度暇なのかを登録する
3. 所要時間に合わせて、やることをアプリが提案する
4. 終わった後、記録をアプリに入力し、保存するという感じです。このアプリのペーパープロトタイピングやDB構造案を見せながら、メンターとどう実装するかを相談します。私の場合は最初にやりたいことをかなり盛りだくさんで考えていたため、「まずはやりたいことを登録するだけの、最小の機能で実装してみよう。研修が進むのに合わせて、コンセプトの要素を取り入れていこう」という話になりました。
このようにしてアプリの方向性を決定した後は、研修のステップに沿って実装していきます。方向性がそれぞれ違うため、同じ研修を受けていても出来上がるアプリは人によってかなり違ったものになります。例として、同期の@ruru8は社内で利用できる書籍サービスを作っていました。詳細は22日の投稿をお楽しみに。研修期間中の過ごし方
- プログラムに沿って、アプリに機能を実装する
- 取り組んでいたステップの実装が終わる、または途中でもキリのいい単位で実装が行えたら、Githubの自分のリポジトリにローカルの内容をpushする
- 見て欲しいところにコメントを書いてプルリクエストを出し、メンターにレビューをお願いする
- LGTM(Looks Good To Me…レビューがOKであるという意味)がもらえれば次の実装に取り掛かる。修正が必要な場合は、ローカルで修正を行い、pushして再レビューをお願いする
という感じです。
わからないことがあったときは、会社で買っていただいている参考書籍を読むか、メンターや社内の先輩に質問します。参考書籍についてはこちらにまとめられています。
また、Classiはコミュニケーションツールとしてslackを使っておりまして、個人が分報チャンネルを持ち、困りごとや思ったことなどをつぶやく文化が活発です。分報についてはこちら。私も自分の分報である「times_ono」というチャンネルに研修の進捗を書くようにしておりまして、ここで質問するとチャンネルを見ていただいている先輩からよくアドバイスを頂けます。これはtimes_onoの一幕でして、私がわからない!と言ったことに対して、同期の@hxrxchang君が席に助けに来てくれて、参考になるリンクも貼ってくれたところです。こんな感じで、気軽にSOSが出せて、助けてくれる人が社内にたくさんいる、すごく温かい環境です。
成果物
出来上がったアプリがこんな感じです。
私はレビューで時間をかけたため、ステップ4で考えていたコンセプトの実現は十分には行えず、シンプルに研修プログラムのステップを進めたアプリ、という感じの成果物になりました。ただ、私の場合はレビューで得た知識が評価され、チームで研修内容の共有会を開かせていただきました。
研修のゴールは人それぞれです。私はステップ23まで+研修内容の共有会の開催がゴールになりましたが、今研修中のベトナム人の新卒二人は1ヶ月で早くもステップ25を完了し、オプション要件に取り組んでいます。また、フロントエンドのスキルがあるメンバーはRailsのプログラミングと並行してAngularで画面の作成を行い、操作性に優れた格好いいアプリを作っています。Classiの先輩方に協力してもらい、ユーザビリティテストを行なったメンバーもいます。
研修を終えて
研修を通して、「プログラマーの仕事の大部分は、人への気遣いである」という感想を持ちました。なぜなら、メンターからの指摘事項のほとんどは、「他のエンジニアがアプリをメンテナンスする時に分かりやすい/後で困らないコーディング」についてのアドバイスだったためです。
- こうした方が、他のプログラマーが分かりやすい!
- この方がRailsや他のgemのアップデートに関わらず使える!
- このコメントをつけた方が、レビュアーや他のエンジニアに親切!
などのコメントをたくさん頂くうちに、プログラマーの仕事像が「黙々と実装だけ行えれば良い、他の人とは関わらない」から、「他の人へ気遣い、情報を伝達し、理解して貰えるコードを書く」に変わっていきました。この意識は、現在チームで仕事をする上でも、チームメンバーへの気遣いや、伝わりやすいプルリクエストという形で活きています。
終わりに
この研修について社内の先輩エンジニアに話したところ、「羨ましい」という声を多く頂きました。2ヶ月以上かけてコードの書き方を教えてもらえる機会はとても貴重で、多くのプログラマーは仕事の中でコードの書き方やプルリクエストの仕方を覚えていくそうです。手厚い研修プログラムを組んでいただいたigaigaさん、ただあきさん、わからないところを教えて頂いた先輩エンジニアの皆さん、また充実した研修を受けさせて貰える会社の懐の広さに感謝しつつ、この投稿を締めます。
明日の投稿は@onigraさんです。お楽しみに。
- 投稿日:2019-12-02T09:59:43+09:00
【Rails】errors.addって何?
モデルでは特に考えずに
errors.add(:base, '名前の文字数オーバー')
とかしておけば良いかーみたいな風潮ありますよね
これ> user = User.new > user.errors => #<ActiveModel::Errors:0x00007fc0ed8b5a50 @base=#<User id: nil, name: nil, created_at: nil, updated_at: nil>, @messages={}, @details={}> > user.errors.add(:base, '名前の文字数オーバー') > user.errors.full_messages => ["名前の文字数オーバー"]そもそも
.add
ってなんなのさと、すぐraiseされるならわかるけど、何個も追加(add)できるの????errors.addの使い方一覧
結論から書きます。
# シンプルな構文(どんなエラーなのか) > user.errors.add(:base, '名前の文字数オーバー') > user.errors.full_messages => ["名前の文字数オーバー"] # 一般的な構文(どのカラムの、どんなエラーなのか) > user.errors.add(:name, '文字数オーバー') > user.errors.full_messages => ["Name 文字数オーバー"] # I18nを利用した構文 # (対象がないと translation missing) ## activerecord.errors.models.user.attributes.name.over_char_limit ## activerecord.errors.models.user.over_char_limit ## activerecord.errors.messages.over_char_limit ## errors.attributes.name.over_char_limit ## errors.messages.over_char_limit > user.errors.add(:name, :over_char_limit) > user.errors.full_messages => ["Name 文字数オーバー"] # すぐraiseしたい構文 > user.errors.add(:name, :over_char_limit, strict: true) ActiveModel::StrictValidationFailed (Name 文字数オーバー) # StrictValidationFailed < StandardError です # 何の為にあるのか不明 > user.errors.add(:name, :over, message: '文字数オーバー') > user.errors.full_messages => ["Name 文字数オーバー"] # .details のため? > user.errors.details => {:name=>[{:error=>:over}]}すぐraiseできるじゃん!
良い機会なので、ここからerrors
とvalidations
の関係性を考えます。ここから下は読まなくて良いです。
railsを調べてみた。
rails 5-1-stable
ブランチを見てます!errorsのmethodはどこから?
すぐ見つけた
active_model/validations.rbactivemodel/lib/active_model/validations.rb# Errors の引数でモデルのオブジェクトを渡してる def errors @errors ||= Errors.new(self) endモデルは
ActiveRecord::Base
を継承しているので、辿っていくと以下のようにincludeされてる# activerecord/lib/active_record/base.rb include Validations # activerecord/lib/active_record/validations.rb include ActiveModel::Validations.addのmethodはどうなってるの?
normalize_message
→I18n
で使えるように頑張ってパースしてた
raiseとかはここで制御してるんだね
active_model/errors.rbactivemodel/lib/active_model/errors.rbdef add(attribute, message = :invalid, options = {}) message = message.call if message.respond_to?(:call) detail = normalize_detail(message, options) message = normalize_message(attribute, message, options) if exception = options[:strict] exception = ActiveModel::StrictValidationFailed if exception == true raise exception, full_message(attribute, message) end details[attribute.to_sym] << detail messages[attribute.to_sym] << message end.addしたけど、いつraiseされるの?
railsでは
.create!
も.update!
も結果的に.save!
が呼ばれます。
perform_validations
のvalid?
でfalseになるとraiseされます。activerecord/lib/active_record/validations.rbdef save!(options = {}) perform_validations(options) ? super : raise_validation_error end private def raise_validation_error raise(RecordInvalid.new(self)) end def perform_validations(options = {}) options[:validate] == false || valid?(options[:context]) endRecordInvalidが呼び出されたら
errorsの配列のメッセージはここでjoinされてるんだね
activerecord/lib/active_record/validations.rbclass RecordInvalid < ActiveRecordError def initialize(record = nil) if record @record = record errors = @record.errors.full_messages.join(", ") message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid") else message = "Record invalid" end super(message) end end以下の
ja.yml
が必要なので忘れないように!config/locales/models/ja.ymlja: activerecord: errors: messages: record_invalid: "%{errors}"valid?では何をしてるの?
errors.clear
: valid?以前に追加されたerrorsを削除run_validations!
: モデルに紐づいてる:validateを1つずつ実行してるerrors.empty?
: errorsの配列にあればraise!つまり、コンソール上で
errors.add
をしても何も起きないんです?(clearされる)activerecord/lib/active_record/validations.rbdef valid?(context = nil) context ||= default_validation_context output = super(context) errors.empty? && output endactivemodel/lib/active_model/validations.rbdef valid?(context = nil) current_context, self.validation_context = validation_context, context errors.clear run_validations! ensure self.validation_context = current_context end
run_validations!
はちょっと難しかった
active_support/callbacks.rb のrun_callbacks
が呼ばれるんだけど、これが結構難しい
(結果的に、:validateを1つずつ実行してるだけなんだけどね)解説は以下のqiitaにありました
Railsのvalidationの実行過程を調べるどんな:validateが実行するのか知りたいなら
# モデルに紐づいてる:validateの一覧 > user.__callbacks[:validate] # 自作validatorの一覧 > user._validators自作validatorを作りたいなら
これは弊社でもおなじみのvalidatorの作り方です!
オリジナルのバリデーションクラス:validates_with所感
初めてrailsのソースコード読んでみたけど、結構読みやすかった!
でもproc
とかyield
とか意味は知ってるけど、普段使わない関数が急に出てくると困惑するし、急にわからなくなるなあ。これを機に俺もrailsのソースコードを読み込んでみよう!と10分思ったが、めんどくさいし、タピオカ飲みたいので辞めた?
- 投稿日:2019-12-02T09:07:42+09:00
CarrierWaveでpng画像を処理したときに色空間がGRAYになってしまってた
バージョン
- ruby 2.6.1
- Ruby on Rails 5.2.3
- MacOS Catalina 10.15.1
- ImageMagick 7.0.9-5
- gem mini_magick 4.9.5
- gem carrierwave 1.3.1
問題
CarrierWaveでMiniMagickを使って画像を処理する時。例えば、resize_to_fit とかresize_and_pad とか resize_to_fill とかを使うと、png画像の色空間が元々RGBだったのにGRAYになってしまう現象があった。
調査
どうやら、色空間をカラーで持っていても、グレイスケールっぽい画像だとImageMagickがカラースペースをグレイスケールに変換してしまうようだった。
なぜ・・、やめてくれ・・!解決方法
上記した resize_to_fit とかのメソッドには、 combine_options という名前付き引数が渡せる。これはImageMagickのオプションと対応しているので、とにかく大量のものが指定できて、その中で、png画像での色の設定は、
-define
オプションでpng:color-type=?
を指定できる。
今回はRGBAで色情報を持ってほしかったので、png:color-type=6
となる。つまり、
combine_options: {define: 'png:color-type=6'}
を指定することで解決した。
Carrierwaveがどうとかいう問題ではなく、ImageMagick側の話だったようだ。注意
これで解決したんだけど、このオプションは要注意なことが書いてあるので、png:color-typeの説明は見ておいた方が良さそう。
最初は色空間が変わるという現象だったから、combine_optionsに
-colorspace sRGB
を指定してたんだけど、これでは問題は解決できなかったのでちょっとハマった・・。
- 投稿日:2019-12-02T08:22:39+09:00
redirect_toとrenderの違い・使い分け
【結論】アクションを実行するかしないかの違い
xxx_controller.rbdef create message = Message.new(message_params) if message.save redirect_to :new else render :new end endこのようなcreateアクションがあります。
・
save
が成功した場合はnewアクションが実行され
、一度ページがリセットされnewのページが表示されます。・
save
が失敗した場合はnewアクションを通さず
、入力された情報はそのままにnewのページが表示されます。
保存が成功したらフォームに情報を残しておく必要はありませんよね。
保存が失敗したらエラーメッセージを表示するなどして、入力情報のエラー部分のみを変更してもらいましょう。
参考
ではまた!
- 投稿日:2019-12-02T08:19:45+09:00
フォロー機能がほしいぃぃ
RailsTutorial
を参考にフォロー機能をつけました。簡単にまとめます。
※クラス名・id名は本家と変えたところがあります。
※ところどころ自己流にアレンジしています。ご容赦ください。Relationshipモデル
ターミナル$ rails g model Relationship follower_id:integer followed_id:integerdb/migrate/20191128014554_create_relationships.rbclass CreateRelationships < ActiveRecord::Migration[5.2] def change create_table :relationships do |t| t.integer :follower_id t.integer :followed_id t.timestamps end add_index :relationships, :follower_id add_index :relationships, :followed_id add_index :relationships, [:follower_id, :followed_id], unique: true end endターミナル$ rails db:migrateapp/models/relationship.rbclass Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" validates :follower_id, presence: true validates :followed_id, presence: true endUserモデル
app/models/user.rbclass User < ApplicationRecord 略 has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed has_many :followers, through: :passive_relationships, source: :follower def follow(other_user) following << other_user end def unfollow(other_user) active_relationships.find_by(followed_id: other_user.id).destroy end def following?(other_user) following.include?(other_user) end endルーティング
app/config/locals/routes.rbRails.application.routes.draw do 略 resources :users, only: [:show] do member do get :following, :followers end end resources :relationships, only: [:create, :destroy] endビュー
app/views/users/_stats.html.haml- @user ||= current_user %div = link_to following_user_path(@user) do #following = @user.following.count = link_to followers_user_path(@user) do #followers = @user.followers.countapp/views/users/_follow_form.html.haml- unless current_user == @user #follow_form - if current_user.following?(@user) = render 'unfollow' - else = render 'follow'app/views/users/_follow.html.haml= form_for(current_user.active_relationships.build, remote: true) do |f| = hidden_field_tag :followed_id, @user.id = f.submit "Follow", class: "btn btn-primary"app/views/users/_unfollow.html.haml= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, remote: true) do |f| = f.submit "Unfollow", class: "btn"xxx.html.haml#好きなところへ = render "stats" = render "follow_form" if user_signed_in?
以下2つは完全自己流なのでスルーしてください。following.html.haml.contents = render "tweets/side-bar" .contents__right .contents__right__tweets .contents__right__tweets__follows .contents__right__tweets__follows__user-nickname = @user.nickname さん .contents__right__tweets__follows__text フォロー中 - @users.each do |user| .contents__right__tweets__follow-users %p.contents__right__tweets__follow-users__user = link_to "#{user.nickname}", user_path(user) .contents__right__tweets__paginate = paginate(@users)followers.html.haml.contents = render "tweets/side-bar" .contents__right .contents__right__tweets .contents__right__tweets__follows .contents__right__tweets__follows__user-nickname = @user.nickname さん .contents__right__tweets__follows__text フォロワー - @users.each do |user| .contents__right__tweets__follow-users %p.contents__right__tweets__follow-users__user = link_to "#{user.nickname}", user_path(user) .contents__right__tweets__paginate = paginate(@users)本家は
show_follow.html.erb
というビューファイルを作って、そこにレンダリングしています。
僕はfollowing
followers
2つのビューファイルを作りました。冗長かもしれませんが明確に分けて管理したかったのでこうしました。コントローラー
relationships_controller.rbclass RelationshipsController < ApplicationController before_action :authenticate_user! def create @user = User.find(params[:followed_id]) current_user.follow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end def destroy @user = Relationship.find(params[:id]).followed current_user.unfollow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end endusers_controller.rbclass UsersController < ApplicationController before_action :authenticate_user!, only: [:following, :followers] 略 def following @user = User.find(params[:id]) @users = @user.following.page(params[:page]).per(3) end def followers @user = User.find(params[:id]) @users = @user.followers.page(params[:page]).per(3) end end
本家大正義RailsTutorial
https://railstutorial.jp/chapters/following_users?version=5.1#cha-following_users
ではまた!
- 投稿日:2019-12-02T07:50:49+09:00
Railsチュートリアル 第11章 アカウントの有効化 - 前提
サンプルアプリケーションの現状と、これから実装する変更内容
Railsチュートリアルの第10章終了時点では、「新規登録したユーザーは、はじめからすべての機能にアクセスできる」という実装となっています。第11章では、「アカウントを有効化するステップを新規登録の途中に差し込むことで、本当にそのメールアドレスの持ち主なのかを確認する」という機能を実装していきます。
アカウントの有効化プロセス
概要
「アカウントの有効化」というユースケースは、大まかには以下のプロセスからなります。
- 有効化トークン・ダイジェストを新規に生成したユーザー情報と関連付ける
- 有効化トークンを含むリンクを、ユーザーにメールで送信する
- ユーザーが2.のリンクをクリックすると、当該アカウントが有効化される
詳細
上記「アカウントの有効化プロセスの概要」をさらに詳細に見ていくと、以下のような機能の実装が必要となるのがわかります。
- ユーザーの初期状態は、「有効化されていない(unactivated)」であること
- ユーザー登録が行われた際に、以下が生成されること
- 有効化トークン
- 当該有効化トークンに対応する有効化ダイジェスト
- 有効化ダイジェストがRDBに保存されること
- 新規登録ユーザーの登録メールアドレス宛に有効化用メールが送信されること
- 当該有効化用メール中に記載されるリンクに有効化トークンが含まれること
- 有効化トークンの認証機能
- ユーザーが有効化用メール中のリンクをクリックしたことが検出できる機能
- 登録メールアドレスをキーとしてユーザーを検索する機能
- ユーザーから送信された有効化トークンと、RDB中の有効化ダイジェストを比較する機能
- ユーザーの状態を「有効化されていない」から「有効化済み(activated)」に変更する機能
- 有効化トークンの認証に成功した時点で変更される
実は、上記の仕組みは、これまで実装してきた「パスワード」「(永続cookiesにおける)記憶トークン」の仕組みとよく似ています。少なからぬメソッドは使いまわしも可能なのです。
検索キー string digest authentication password
password_digest
authenticate(password)
id
remember_token
remember_digest
authenticated?(:remember, token)
activation_token
activation_digest
authenticated?(:activation, token)
reset_token
reset_digest
ahthenticated?(:reset, token)
第11章で行う実装
- アカウント有効化に必要なリソースやデータモデルの生成
- アカウント有効化時のメール送信部分の実装
- Railsの
mailer
機能を用いるUser#authenticated?
メソッドの機能拡張- アカウント有効化処理の本体の実装
- 機能拡張した
User#authenticated?
を用いる
- 投稿日:2019-12-02T07:50:39+09:00
【Rails】ユーザー情報の編集【Rails Tutorial 10章まとめ】
Usersコントローラの編集
Usersコントローラにアクションを追加し、ユーザー情報の編集やユーザー一覧の表示、ユーザーの削除を行えるようにする。
ユーザー情報の編集
editアクションと編集フォーム
Usersコントローラにeditアクションを追加する。
editアクションに対応するURLはusers/:id/edit(名前付きルートはedit_user_path(user))なので、params[:id]を使えば編集したいユーザーを取得できる。app/controllers/users_controller.rbdef edit @user = User.find(params[:id]) endeditアクションに対応するeditビューは以下のようになる。
app/views/users/edit.html.erb<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a> </div> </div> </div>これはユーザー新規登録フォームとほぼ同じである。
編集フォームのform_forは以下のようなhtmlに変換される。<form accept-charset="UTF-8" action="/users/1" class="edit_user" id="edit_user_1" method="post"> <input name="_method" type="hidden" value="patch" /> . . . </form>form_forのコードは新規登録フォームと同じなのに、各属性が更新用になっているのは、Railsが新規ユーザーであるか既存ユーザーの編集であるかを自動で判別してくれるからである。
なお、gravatar用のaタグについているtarget="_blank"とrel="noopener"は、リンク先を別のタブで開くためのものである(rel属性はセキュリティ上の問題を解決するためのもの)。
editアクションとビューができたので、レイアウトのヘッダー部分にリンクを貼っておく。
app/views/layouts/_header.html.erb<% if logged_in? %> <li><%= link_to "Users", '#' %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li> <li class="divider"></li> <li><%= link_to "Log out", logout_path, method: :delete %></li> </ul> </li> <% else %>editページとsignupページのパーシャル
ユーザー編集フォームと新規登録フォームは、フォームの送信ボタンの文字と、gravatarのリンクしか違いがないので、パーシャルにまとめる。
app/views/users/_form.html.erb<%= form_for(@user, url: yield(:url)) do |f| %> <%= render 'shared/error_messages', object: @user %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit yield(:button_text), class: "btn btn-primary" %> <% end %>signupビュー
app/views/users/new.html.erb<% provide(:title, 'Sign up') %> <% provide(:url, signup_path) %> <% provide(:button_text, 'Create my account') %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> </div> </div>editビュー
app/views/users/edit.html.erb<% provide(:title, 'Edit user') %> <% provide(:url, user_path) %> <% provide(:button_text, 'Save changes') %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank">Change</a> </div> </div> </div>ここでは、2つのprovideメソッドとyieldを使っている。
一つはform_forの送信先である。
新規登録フォームではcreateアクションに送信するので、signup_pathとする。
編集フォームではupdateアクションに送信するので、user_pathとする(引数(user)は不要)。もう一つはフォーム送信ボタンの文字である。
編集の失敗
編集失敗時の処理
更新フォームの送信先であるupdateアクションを書く。
app/controllers/users_controller.rbdef edit @user = User.find(params[:id]) end def update @user = User.find(params[:id]) if @user.update_attributes(user_params) # 更新に成功した場合を扱う。 else render 'edit' end endユーザーを取得した後update_attributesでその属性を更新する。
更新に失敗した場合は編集画面に戻る。編集失敗時のテスト
ユーザー編集用の統合テストを作成する。
$ rails generate integration_test users_edit
ユーザー情報の更新に失敗した場合のテストを書く。
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } } assert_template 'users/edit' end endupdateアクションにはPATCHリクエストを送信する点に注意する。
編集の成功
編集成功時の処理
編集に成功した場合は、新規登録のcreateアクションと同様に、フラッシュメッセージを表示してユーザー表示ページに移動する。
app/controllers/users_controller.rbdef update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end編集成功時のテスト
ユーザー情報の編集に成功した場合のテストを書く。
test/integration/users_edit_test.rbtest "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end変更したいnameやemailを変数に入れているのは、編集完了後のUserオブジェクトの各属性と比較するためである。
assert_equalでエラーが出れば、編集に成功したはずなのにnameやemailが変わっていないことになる。ここでテストはREDになるのだが、それはパスワードが有効な値でないためである。
パスワードの変更は必須ではないため、Userモデルのパスワードのバリデーションにallow_nil: trueを追加して、この例外に対応する。app/models/user.rbvalidates :password, presence: true, length: { minimum: 6 }, allow_nil: trueテストがGREENとなることを確認する。
ユーザーの認可
認可
ユーザーの編集機能が完成したが、この状態では誰もが任意のユーザー情報を変更できてしまう。
そこで、認可(authorization)機能を追加して、特定のページへのアクセスを制限する。before_actionとlogged_in_userメソッド
before_acitonメソッドを使うことで、各アクションの実行前に特定の操作を行うことができる。
ログインしていないとユーザー情報を編集できないようにする。
editアクションとupdateアクションの前にログインしているかを判定して、ログインしていなければログインページにリダイレクトするlogged_in_userメソッドを定義する。app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] . . . private . . . # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end endbefore_actionでは、アクション前に実行するメソッドをハッシュの形(:logged_in_user)で指定する。
これで、ログインしていないユーザーだとeditページにアクセスできなくなる。
また、これが原因でテストがREDになるので修正する。
各テストの前にlog_in_asヘルパーメソッドを使ってログインしておく。test/integration/users_edit_test.rbtest "unsuccessful edit" do log_in_as(@user) get edit_user_path(@user) . . . end test "successful edit" do log_in_as(@user) get edit_user_path(@user) . . . endlogged_in_userのテスト
logged_in_userのテストを書く。
test/controllers/users_controller_test.rbtest "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_url end test "should redirect update when not logged in" do patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert_not flash.empty? assert_redirected_to login_url endupdateアクションでは、PATCHリクエストでユーザー情報を送信する。
テストの前後でbefore_actionをコメントアウトして、正しくテストできているかを確認しておく。正しいユーザーを要求する
ユーザーが他のユーザー情報を編集できないようにするために、correct_userメソッドを作成し、before_actionでedit、updateアクションに設定する。
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) unless @user == current_user flash[:danger] = "You have no authority for the page" redirect_to(root_url) end end endこのメソッドに該当するのはログイン済みのユーザーなので、ログインページではなくルートURLにリダイレクトする。
(ちなみに、チュートリアルではここでフラッシュメッセージを入れてないせいで後のテストがREDになるので勝手に入れてます。)慣習として、unless @user == current_userの部分をcurrent_user?というヘルパーメソッドにしておく。
app/helpers/sessions_helper.rb# 渡されたユーザーがログイン済みユーザーであればtrueを返す def current_user?(user) user == current_user endapp/controllers/users_controller.rb# 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) unless current_user?(@user) flash[:danger] = "You have no authority for the page" redirect_to(root_url) end end正しいユーザーのテスト
認可機能をテストするために、fixtureファイルに別のユーザーを作成する。
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>テストを書く。
test/integration/users_edit_test.rbdef setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url endlog_in_asメソッドを使って別のユーザーでログインすることと、ルートURLにリダイレクトすること以外は、非ログイン時のテストと同じである。
- 投稿日:2019-12-02T07:50:39+09:00
【Rails】ユーザーの情報の編集【Rails Tutorial 10章まとめ】
Usersコントローラの編集
Usersコントローラにアクションを追加し、ユーザー情報の編集やユーザー一覧の表示、ユーザーの削除を行えるようにする。
ユーザー情報の編集
editアクションと編集フォーム
Usersコントローラにeditアクションを追加する。
editアクションに対応するURLはusers/:id/edit(名前付きルートはedit_user_path(user))なので、params[:id]を使えば編集したいユーザーを取得できる。app/controllers/users_controller.rbdef edit @user = User.find(params[:id]) endeditアクションに対応するeditビューは以下のようになる。
app/views/users/edit.html.erb<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a> </div> </div> </div>これはユーザー新規登録フォームとほぼ同じである。
編集フォームのform_forは以下のようなhtmlに変換される。<form accept-charset="UTF-8" action="/users/1" class="edit_user" id="edit_user_1" method="post"> <input name="_method" type="hidden" value="patch" /> . . . </form>form_forのコードは新規登録フォームと同じなのに、各属性が更新用になっているのは、Railsが新規ユーザーであるか既存ユーザーの編集であるかを自動で判別してくれるからである。
なお、gravatar用のaタグについているtarget="_blank"とrel="noopener"は、リンク先を別のタブで開くためのものである(rel属性はセキュリティ上の問題を解決するためのもの)。
editアクションとビューができたので、レイアウトのヘッダー部分にリンクを貼っておく。
app/views/layouts/_header.html.erb<% if logged_in? %> <li><%= link_to "Users", '#' %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li> <li class="divider"></li> <li><%= link_to "Log out", logout_path, method: :delete %></li> </ul> </li> <% else %>editページとsignupページのパーシャル
ユーザー編集フォームと新規登録フォームは、フォームの送信ボタンの文字と、gravatarのリンクしか違いがないので、パーシャルにまとめる。
app/views/users/_form.html.erb<%= form_for(@user, url: yield(:url)) do |f| %> <%= render 'shared/error_messages', object: @user %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit yield(:button_text), class: "btn btn-primary" %> <% end %>signupビュー
app/views/users/new.html.erb<% provide(:title, 'Sign up') %> <% provide(:url, signup_path) %> <% provide(:button_text, 'Create my account') %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> </div> </div>editビュー
app/views/users/edit.html.erb<% provide(:title, 'Edit user') %> <% provide(:url, user_path) %> <% provide(:button_text, 'Save changes') %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank">Change</a> </div> </div> </div>ここでは、2つのprovideメソッドとyieldを使っている。
一つはform_forの送信先である。
新規登録フォームではcreateアクションに送信するので、signup_pathとする。
編集フォームではupdateアクションに送信するので、user_pathとする(引数(user)は不要)。もう一つはフォーム送信ボタンの文字である。
編集の失敗
編集失敗時の処理
更新フォームの送信先であるupdateアクションを書く。
app/controllers/users_controller.rbdef edit @user = User.find(params[:id]) end def update @user = User.find(params[:id]) if @user.update_attributes(user_params) # 更新に成功した場合を扱う。 else render 'edit' end endユーザーを取得した後update_attributesでその属性を更新する。
更新に失敗した場合は編集画面に戻る。編集失敗時のテスト
ユーザー編集用の統合テストを作成する。
$ rails generate integration_test users_edit
ユーザー情報の更新に失敗した場合のテストを書く。
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } } assert_template 'users/edit' end endupdateアクションにはPATCHリクエストを送信する点に注意する。
編集の成功
編集成功時の処理
編集に成功した場合は、新規登録のcreateアクションと同様に、フラッシュメッセージを表示してユーザー表示ページに移動する。
app/controllers/users_controller.rbdef update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end編集成功時のテスト
ユーザー情報の編集に成功した場合のテストを書く。
test/integration/users_edit_test.rbtest "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end変更したいnameやemailを変数に入れているのは、編集完了後のUserオブジェクトの各属性と比較するためである。
assert_equalでエラーが出れば、編集に成功したはずなのにnameやemailが変わっていないことになる。ここでテストはREDになるのだが、それはパスワードが有効な値でないためである。
パスワードの変更は必須ではないため、Userモデルのパスワードのバリデーションにallow_nil: trueを追加して、この例外に対応する。app/models/user.rbvalidates :password, presence: true, length: { minimum: 6 }, allow_nil: trueテストがGREENとなることを確認する。
- 投稿日:2019-12-02T04:24:39+09:00
【Rails】パンクズリストの作り方
まえがき
みなさんパンくずリストは知っていますでしょうか。どこかで耳にした方もいるかもしれません。ちょっと変わった名前ですがbreadcrumbsと書くとかっこいいですね。このパンくずリストですが名前によらずとても便利な物なのでぜひ作り方を覚えましょう。
パンクズリストとは
ホーム>おすすめ一覧>家電製品
のような現在いる位置を視覚的に見ることができる物です。大抵画面上部に用いられることが多いです。
これはよくSEO対策的に使われることが多く、SEOに詳しい人にとってはおなじみなのではないでしょうか。パンクズリストを作るメリットは以下の通りですです。
設置するメリット
ユーザビリティーの向上
パンくずリストを用いることで視覚的に現在いる位置を構造的に見ることができるので、そのwebアプリを使うユーザーがサイト内で迷子になることがなくなり、ストレスなくサイト内巡回をすることができます。また、様々なユーザーはみなトップページからサイトに訪れるわけではなくそれぞれ必要なページに直接飛んでくるので、本当に目的のページを開けているか確認をするという使い方もされます。
検索ページに表示される時がある
googleなどで検索をかけた時にページの説明欄にパンクズリストが表示されているのを見たことがある人もいるかもしれません。パンくずが表示されていると検索ページにいるだけでサイト内の構造を見ることができ、飛びたいページの上層カテゴリも表示されるのでクリック率対策になります。
内部SEO対策
googleはページ内を評価する際に一つ一つのページに飛びリンクの文字などを認識しそのページが有用なのかどうかを判断しています。その際にパンクズリストがあるとその作業がスムーズになるために高く評価されSEO対策に効くとされています。
設置方法
では本題です。今回はRailsでの設置になります。
RailsではGretelというgemを用意してくれています。GitHub
https://github.com/lassebunk/gretelgem "gretel"このようにgemfileの一番下に追加します。
$ bundle installそしてコマンドを打ち、設定ファイルを作っていきます。
$ rails generate gretel:install
すると以下のようなファイルが生成されます。
config/breadcrumbs.rbcrumb :root do link "Home", root_path end # crumb :projects do # link "Projects", projects_path # end # crumb :project do |project| # link project.name, project_path(project) # parent :projects # end # crumb :project_issues do |project| # link "Issues", project_issues_path(project) # parent :project, project # end # crumb :issue do |issue| # link issue.title, issue_path(issue) # parent :project_issues, issue.project # end # If you want to split your breadcrumbs configuration over multiple files, you # can create a folder named `config/breadcrumbs` and put your configuration # files there. All *.rb files (e.g. `frontend.rb` or `products.rb`) in that # folder are loaded and reloaded automatically when you change them, just like # this file (`config/breadcrumbs.rb`).今回はマイページを作っていきたいのでrootを設定した後にマイページを表記していきます。
config/breadcrumbs.rb# ルート crumb :root do link "ホーム", root_path end # マイページ crumb :mypage do link "マイページ", mypage_users_path end
crumbs :mypage do
はなんという名前でhtml上に表記し呼び出すかを書きます、後々使います。
link
はパンクズリストに表示される文字とそのページがどこのパスに属しているかを表記します。
パンクズリストはリンクになっている場合が多いのでそのリンクはここで設定します。ではビューファイルに表記していきます
mypage.html.haml- breadcrumb :mypage = breadcrumbs pretext: "You are here:",separator: " › "
- breadcrumb :mypage
はconfig/breadcrumbs.rbに定義したmypageを呼び出すことができ、
= breadcrumbs pretext: "You are here:",separator: " › "
で表示したい位置を指定することができます。
›
という表記はHTML特殊文字と言われる物で>
の部分を表記しています。また親子の関係を示すために以下のような表記方法もあります
profile.haml.haml# プロフィール crumb :profile do link "プロフィール", edit_user_path parent :mypage end
parent
と表記しdoとendで挟むことによりcrumb :profile do
の親を書くことができます。
この表記だと次のような表示になります。
ホーム > マイページ > プロフィール
といった感じでしょうか。まとめ
いかがだったでしょうか、今回書いて行ったパンクズリストは企業としても重宝する技術ですし、習得していて損はないのではないでしょうか。ぜひポートフォリオなどにも実装してみてください!