- 投稿日:2020-01-10T22:02:52+09:00
Dockerで環境構築する際、ソースコードの変更が即時反映されない場合の対処法
記事の目的
docker-composeで環境構築する際に起こったRailsのソースコードを修正しても即時反映されない問題に対しての対処法を共有すること。
対処法
1.development.rbを編集
開発環境用の設定ファイルである
config/environments/development.rbを編集。
「 ~ 」は元々の記述を省略しているものとする。
この中にconfig.reload_classes_only_on_change = falseという記述を追加する。development.rbRails.application.configure do ~ config.reload_classes_only_on_change = false end2.railsコンテナを再起動
設定を記載しただけでは反映されないので、最後にRailsを再起動する。
ターミナル上で、
$ docker-compose restartと入力する。
以上でソースコードの変更が即時反映される。
終わりに
Dockerで環境構築をした際に、もう設定しておくと良い。
- 投稿日:2020-01-10T21:23:16+09:00
#Rails + #MySQL / begin rescue end と Transaction を外側・内側に置いた動作の違いを確認してみたコード断片ですが
- Transactionの中で エラーが発生した場合、後続の処理を続けずに ROLLBACK させ、なおかつエラーをハンドリングして特定の処理をおこないたい
- Transaction で begin rescue end で囲ってしまうと、特定のエラーを rescue するときに、ROLLBACK を発生させるための例外も起こらなくなってしまい、やりたいことが出来ない
- Transaction を内側に、begin rescue end を外側に書いてみる。こうすることで ROLLBACK が起こった後に 例外をキャッチして、特定のエラーハンドリング処理をおこなうということを実現する
class User < ApplicationRecord validates_uniqueness_of :unique_id enddef call_inner_transaction(id: , something_wrong: false) begin ActiveRecord::Base.transaction do StripeWebhookSucceededEvent.create!(unique_id: id) raise 'SOMETHING WRONG' if something_wrong puts '-' * 100 puts 'EXECUTED!' puts '-' * 100 end rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e puts '*' * 100 puts "RAISED!" puts '*' * 100 puts e.message end enddupulicated_id = rand(999_999_999_999) # EXECUTE # COMMIT SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s) # [88] pry(main)> SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s) # (0.4ms) BEGIN # StripeWebhookSucceededEvent Create (0.7ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:13') # ---------------------------------------------------------------------------------------------------- # EXECUTED! # ---------------------------------------------------------------------------------------------------- # (2.5ms) COMMIT # => nil # DUPULICATE and RAISE # NO EXECUTE # ROLLBACK SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s) # [89] pry(main)> SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s) # (0.4ms) BEGIN # StripeWebhookSucceededEvent Create (1.1ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:17') # (2.6ms) ROLLBACK # **************************************************************************************************** # RAISED! # **************************************************************************************************** # Mysql2::Error: Duplicate entry '225043383392' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:17') # => nil # DUPULICATE and RAISE before SOMETHING WRONG # NO EXECUTE # ROLLBACK SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s, something_wrong: true) # [90] pry(main)> SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s, something_wrong: true) # (0.4ms) BEGIN # StripeWebhookSucceededEvent Create (2.6ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:24') # (3.3ms) ROLLBACK # **************************************************************************************************** # RAISED! # **************************************************************************************************** # Mysql2::Error: Duplicate entry '225043383392' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:24') # => nil # SOMETHING WRONG # NO DUPULICATE # NO EXECUTE # RAISE AND ROLLBACK SomeClass.new.call_inner_transaction(id: rand(999_999_999_999).to_s, something_wrong: true) # [91] pry(main)> SomeClass.new.call_inner_transaction(id: rand(999_999_999_999).to_s, something_wrong: true) # (0.9ms) BEGIN # StripeWebhookSucceededEvent Create (1.0ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('357583484588', '2020-01-09 10:53:28') # (3.0ms) ROLLBACK # RuntimeError: SOMETHING WRONG # from (pry):120:in `block in call_inner_transaction'def call_outer_transaction(id: , something_wrong: false) ActiveRecord::Base.transaction do begin StripeWebhookSucceededEvent.create!(unique_id: id) raise 'SOMETHING WRONG' if something_wrong puts '-' * 100 puts 'EXECUTED!' puts '-' * 100 rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e puts '*' * 100 puts "RAISED!" puts '*' * 100 puts e.message end end enddupulicated_id = rand(999_999_999_999) # EXECUTE # COMMIT SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s) # [93] pry(main)> SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s) # (0.4ms) BEGIN # StripeWebhookSucceededEvent Create (0.6ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:18') # ---------------------------------------------------------------------------------------------------- # EXECUTED! # ---------------------------------------------------------------------------------------------------- # (4.7ms) COMMIT # => nil # DUPLUCATED AND RAISED # NO EXECUTE # BUT COMMIT HAPPENS SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s) # [94] pry(main)> SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s) # (0.5ms) BEGIN # StripeWebhookSucceededEvent Create (6.4ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:26') # **************************************************************************************************** # RAISED! # **************************************************************************************************** # Mysql2::Error: Duplicate entry '64910455241' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:26') # (5.4ms) COMMIT # => nil # DUPLUCATED AND RAISED # NO EXECUTE # BUT COMMIT HAPPENS SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s, something_wrong: true) # [95] pry(main)> SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s, something_wrong: true) # (1.0ms) BEGIN # StripeWebhookSucceededEvent Create (1.3ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:32') # **************************************************************************************************** # RAISED! # **************************************************************************************************** # Mysql2::Error: Duplicate entry '64910455241' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:32') # (4.3ms) COMMIT # => nil # SOMETHING WRONG # NO DUPULICATE # NO EXECUTE # BUT RAISE AND ROLLBACK SomeClass.new.call_outer_transaction(id: rand(999_999_999_999).to_s, something_wrong: true) # [96] pry(main)> SomeClass.new.call_outer_transaction(id: rand(999_999_999_999).to_s, something_wrong: true) # (0.5ms) BEGIN # StripeWebhookSucceededEvent Create (0.8ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('710309047775', '2020-01-09 10:54:37') # (2.4ms) ROLLBACK # RuntimeError: SOMETHING WRONG # from (pry):137:in `block in call_outer_transaction'Original by Github issue
- 投稿日:2020-01-10T21:23:16+09:00
#Rails + #MySQL / ApplicationRecord.transaction / begin rescue end / COMMIT or ROLLBACK / raise in Inner or outer Transaction / Ruby examples
class User < ApplicationRecord validates_uniqueness_of :unique_id endI HOPE IT
def call_inner_transaction(id: , something_wrong: false) begin ActiveRecord::Base.transaction do StripeWebhookSucceededEvent.create!(unique_id: id) raise 'SOMETHING WRONG' if something_wrong puts '-' * 100 puts 'EXECUTED!' puts '-' * 100 end rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e puts '*' * 100 puts "RAISED!" puts '*' * 100 puts e.message end enddupulicated_id = rand(999_999_999_999) # EXECUTE # COMMIT SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s) # [88] pry(main)> SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s) # (0.4ms) BEGIN # StripeWebhookSucceededEvent Create (0.7ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:13') # ---------------------------------------------------------------------------------------------------- # EXECUTED! # ---------------------------------------------------------------------------------------------------- # (2.5ms) COMMIT # => nil # DUPULICATE and RAISE # NO EXECUTE # ROLLBACK SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s) # [89] pry(main)> SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s) # (0.4ms) BEGIN # StripeWebhookSucceededEvent Create (1.1ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:17') # (2.6ms) ROLLBACK # **************************************************************************************************** # RAISED! # **************************************************************************************************** # Mysql2::Error: Duplicate entry '225043383392' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:17') # => nil # DUPULICATE and RAISE before SOMETHING WRONG # NO EXECUTE # ROLLBACK SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s, something_wrong: true) # [90] pry(main)> SomeClass.new.call_inner_transaction(id: dupulicated_id.to_s, something_wrong: true) # (0.4ms) BEGIN # StripeWebhookSucceededEvent Create (2.6ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:24') # (3.3ms) ROLLBACK # **************************************************************************************************** # RAISED! # **************************************************************************************************** # Mysql2::Error: Duplicate entry '225043383392' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('225043383392', '2020-01-09 10:53:24') # => nil # SOMETHING WRONG # NO DUPULICATE # NO EXECUTE # RAISE AND ROLLBACK SomeClass.new.call_inner_transaction(id: rand(999_999_999_999).to_s, something_wrong: true) # [91] pry(main)> SomeClass.new.call_inner_transaction(id: rand(999_999_999_999).to_s, something_wrong: true) # (0.9ms) BEGIN # StripeWebhookSucceededEvent Create (1.0ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('357583484588', '2020-01-09 10:53:28') # (3.0ms) ROLLBACK # RuntimeError: SOMETHING WRONG # from (pry):120:in `block in call_inner_transaction'I DO NOT HOPE IT
def call_outer_transaction(id: , something_wrong: false) ActiveRecord::Base.transaction do begin StripeWebhookSucceededEvent.create!(unique_id: id) raise 'SOMETHING WRONG' if something_wrong puts '-' * 100 puts 'EXECUTED!' puts '-' * 100 rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e puts '*' * 100 puts "RAISED!" puts '*' * 100 puts e.message end end enddupulicated_id = rand(999_999_999_999) # EXECUTE # COMMIT SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s) # [93] pry(main)> SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s) # (0.4ms) BEGIN # StripeWebhookSucceededEvent Create (0.6ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:18') # ---------------------------------------------------------------------------------------------------- # EXECUTED! # ---------------------------------------------------------------------------------------------------- # (4.7ms) COMMIT # => nil # DUPLUCATED AND RAISED # NO EXECUTE # BUT COMMIT HAPPENS SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s) # [94] pry(main)> SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s) # (0.5ms) BEGIN # StripeWebhookSucceededEvent Create (6.4ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:26') # **************************************************************************************************** # RAISED! # **************************************************************************************************** # Mysql2::Error: Duplicate entry '64910455241' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:26') # (5.4ms) COMMIT # => nil # DUPLUCATED AND RAISED # NO EXECUTE # BUT COMMIT HAPPENS SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s, something_wrong: true) # [95] pry(main)> SomeClass.new.call_outer_transaction(id: dupulicated_id.to_s, something_wrong: true) # (1.0ms) BEGIN # StripeWebhookSucceededEvent Create (1.3ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:32') # **************************************************************************************************** # RAISED! # **************************************************************************************************** # Mysql2::Error: Duplicate entry '64910455241' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('64910455241', '2020-01-09 10:54:32') # (4.3ms) COMMIT # => nil # SOMETHING WRONG # NO DUPULICATE # NO EXECUTE # BUT RAISE AND ROLLBACK SomeClass.new.call_outer_transaction(id: rand(999_999_999_999).to_s, something_wrong: true) # [96] pry(main)> SomeClass.new.call_outer_transaction(id: rand(999_999_999_999).to_s, something_wrong: true) # (0.5ms) BEGIN # StripeWebhookSucceededEvent Create (0.8ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('710309047775', '2020-01-09 10:54:37') # (2.4ms) ROLLBACK # RuntimeError: SOMETHING WRONG # from (pry):137:in `block in call_outer_transaction'Original by Github issue
- 投稿日:2020-01-10T20:15:02+09:00
【第8章】Railsチュートリアル 5.1(第4版) 基本的なログイン機構
大まかな流れの自己整理が目的(コード外文章中のSession(s)の表記などはスルー状態)のため、内容そのものに不足・誤り等あれば追記&訂正していきますのでご指摘頂けますと幸いです
なお、筆者はYassLabさんの動画版で学んでいるため、本記事は「チュートリアル sample_app」+「他補足」個人的に「電子ページ以上に分かりやすい!」と感じた解説部分+参考記事を整理してみようと試みた劣化の内容寄りになってます。8.1 セッション
セッション(Session)とは
ユーザがログイン後、ページ遷移しても再度ログインしなくてもいいように、「ログインしている」ことを記憶しておく機能のこと。
ページを移動しても変数の内容を保持する仕組みなので、ログイン機能以外でも利用可能。Sessionsリソース
「ログインしているかどうか?」が知りたく、いちいちDB書き換えは面倒...
→ 今回はモデル(兼DB)は使わない!
Sessionやrailsサーバに一時的に情報を保持するやり方や、クライアント側で保持するCookieもあるので、今回はサーバ(に?)とブラウザに保存する。railsサーバは「$rails server」 → 「ctrl c」で落ちてしまう...
参考
【Rails入門説明書】sessionについて解説
Railsのリソースとルーティングについて
まずはルーティング作成のため、トピックブランチにチェックアウト&Sessionsコントローラを生成する。$ git checkout -b basic-login $ rails generate controller Sessions new
Usersリソースの時は専用のresourcesを使ってルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないので、「名前付きルーティング」だけを使うためルーティングに追加する。routes.rbget '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy'サーバ起動して、「/login」にアクセスするとnewアクションのview画面が出る。
Sessionsコントローラのテストで名前付きルートを使うようにする( 「get sessions_new_url」 → 「get login_path」へ変更)。テストしてGREEN。
sessions_controller_test.rbrequire 'test_helper' # 「::」 は 「ActionDispatch/IntegrationTest」 ディレクトリみたいなイメージ class SessionsControllerTest < ActionDispatch::IntegrationTest test "should get new" do get login_path assert_response :success end end
ログインフォーム
ユーザコントローラ(users_controller.rb)では、モデルがあって、それを@user:インスタンス変数に格納→それをform_forに渡す→newアクションテンプレートが動き出した。users_controller.rbdef new @user = User.new # => form_for @user endただSessionではモデル作ってないからできない。
POSTリクエストを「/login」に送ってcreateアクションが動くメソッドが欲しいので、sessionsのコントローラ、ビュー画面に下記を追加する。sessions_controller.rbclass SessionsController < ApplicationController # GET /login def new # POST /login => create action end # POST /login def create end end
なお、form_forの引数には@userを入れなくてもシンボル(:session)で対応可能であり、引数にurlを加えればokapp/views/sessions/new.html.erb<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <!-- ↓↓↓ params[:session][:email] --> <!-- ↓↓↓ params[:session][:password] --> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>参考
POSTメソッドcreateメソッドに追記(今回はインスタンス変数@userでなくローカル変数userに情報を入れる)
def create #まずはUser情報が必要 user = User.find_by(email:params[:session][:email]) #=> User object or false # (【rubyの仕組み】falseとnil以外はtrue) if user.authenticate(params[:session][:password]) # Sucess else # Failure (sessionモデルがないのでバリデーションが使えない) flash[:danger] = 'Invalid email/password combination' render 'new' end end
ただ、この時点では「登録されていない(間違った)emailアドレスをパラメータに渡す(ユーザが入力する)と発生するエラー」がある。
find_byが見つからない、nil(nilオブジェクト:authenticateが適応されてない) がuserに入ってauthenticateメソッド → NoMethodError発生(メソッドが見つからない)ここで真っ先に思いつく
(筆者のかろじての発想)のが「if user == nil...」などの書き方だが、
Rubyの特徴としてuserにユーザオブジェクトが入ればtrue、nilが入ればfalseを利用すれば、
&&(ex. a && b 意:「aがtrueでかつ、bもtrueだったらtrue結果、どちらかfalseだったらfalse結果」 )を使って下記に表現できる。
ちなみに、rubyに限らずコンピュータ言語の&&では、左側(今回のuser)がfalseの場合は右側(user.authenticate)は判断してもしなくても必然的にfalseになるので機械側が判断せず省略する(処理を止める)。if user && user.authenticate(params[:session][:password])参考
演算子式(Ruby 2.7.0 リファレンスマニュアル)
ここで小さなバグとして、エラーメッセージのflashの表示時間が長い(思ったより生き延びている..?)問題がある。原因として、flashの生存期間としては、次のリクエストが来るまで
前回がredirectに対して今回はrender(リクエストを発行するのでなく、「このテンプレートを描画してね」)、つまり、失敗してflash登場後に次のリクエストが来るまで残り続ける
①表示(0リウエスト目)、②リロード(1リクエスト目)、③リロード(2リクエスト目)すぐに直さず回帰バグを防ぐため、統合テストを行う。
$ rails generate integration_test users_loginusers_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest test "login with invalid information" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: "", password: "" } } assert_template 'sessions/new' assert_not flash.empty? get root_path assert flash.empty? end end
ログインに失敗したときのテスト(意図的にログインを失敗させたときの動作)flashのバグも再現させる
1.login_pathにgetリクエストを送る
2.sessionsのnewテンプレートが呼び出される
3.login_pathに失敗させる空のパラメータ(params)を送る
4.(たぶん失敗してるので)またnewテンプレート再描画
5.invalid~のflashが出てるか?
6.その後さらにtopページにリクエスト
7.その時にflashがないか?(消えてるか?)
この時点ではrailsテストは失敗(RED:正しい)。
直すにはcreateメソッドのflashにメソッドとしてnowを追記する。
# ユーザーログイン後にユーザー情報のページにリダイレクトする flash.now[:danger] = 'Invalid email/password combination'
これにてテスト通過(GREEN)。
8.2 ログイン
ヘッドの部分を下記のように変更する。
ログインしてる時 → ログアウト(の選択項目)表示
ログアウトしてる時 → ログイン(の選択項目)表示Sessionsメソッド
Sessionsという特殊な変数(railsの機能)を使い、サーバ側に一時的に情報を保存する。
Sessionsのデータはviewとcontrollerで利用できる。
Sessionsが切れるまでの保存期間はRailsサーバが落ちるまで+ブラウザが生き残ってる(ex.ログイン後、タブやChromeを閉じる前)まで。
session[:user_id] = user.id<参考>
【Rails】Sessionの使い方について
Session管理とRailsのcookie store
log_inメソッドと組み合わせるapp/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end end
Applicationコントローラのcreateメソッドに「log_in」、newへのリダイレクトを加える。
log_in user redirect_to user
ユーザがProfileページへ飛ぶ時、今ログインしているユーザが誰か?(ちゃんと別の人でなく自分のユーザ情報か?)の情報を持ってこないことにははじまらない。
→ Session情報からユーザ情報をもってくるようsessionヘルパーを編集する。
ユーザーIDを一時セッションの中に安全に置けるようになったので、今度はそのユーザーIDを別のページで取り出す。
→ current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。app/helpers/sessions_helper.rbdef current_user # (元祖)current_user = User.find(session[:user_id]) # view側でも引き出せるようにインスタンス変数にする # (改訂1)@current_user = User.find(session[:user_id]) # findは失敗したらエラーを返すログイン中にsessionが切れる可能性はあるので、 # 失敗しても「nil」を返すfind_byを使う。 # (改訂2)@current_user = User.find_by(id: session[:user_id]) # @current_user = User.find_by(id: session[:user_id]) # if @current_user.nil? # @current_user = User.find_by(id: session[:user_id]) # else # @current_user # end # 「or」演算子を使いわずか1行で # @current_user = @current_user || User.find_by(id: session[:user_id]) # さらにRubyっぽくして完成(よく使われる形らしい) @current_user ||= User.find_by(id: session[:user_id]) end
セッションにユーザーIDが存在しない場合、このコードは単に終了して自動的にnilを返す(何度もDB問い合わせなく処理が早い)。レイアウトのリンクを変更する方法として、ヘルパーにメソッドを追加する。
sessions_helper.rb# ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end
ヘルパーヘッダーのhtml記入。setting(設定)はダミーリンクに。
link_to はデフォルトの振る舞いとして、引数にユーザオブジェクト(cuurent_user)が加わったらユーザのProfieページに飛ばす。
Log outでdeleteのリクエストを送る。<header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <!--元<li><%= link_to "Log in", '#' %></li>--> <% 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", '#' %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: :delete %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header>補足(エラー)
ここでログインしようとしてエラー
sessionsコントローラでlog_inメソッドが見つからない。ヘルパーはもともとviewで使われる。ただ、今回はコントローラでヘルパーを使いたいので、module(ヘルパー側)と対応するincludeを追記する。
sessions_controller.rbclass SessionsController < ApplicationController #moduleと対応させる include SessionsHelper # 省略
Bootstrapの機能を使うため、jsに追加する。app/assets/javascripts/application.js//= require jquery //= require bootstrapもう一度リロードするとログイン状態になり、右上の項目が変わる(プルダウンの起動もok)。
レイアウトの変更をテストする
fixture(テストを実行、成功させるための状態や前提条件の集合)をYAMLで用意する。
YAMLファイル
YAMLファイルとは、
構造化されたデータを表現するためのデータ形式のひとつ(HTML,XMLなどいろんなファイルの書き方のひとつ)。
配列(先頭に「-」)、ハッシュ(キー:値)、スカラー(文字列、数値、真偽知など)で構成される。< 主な用途 >
・各種設定ファイル
・データ保存用 (シリアライゼーション)
・データ交換用フォーマット
・ログファイル<参考>
YAMLとは|「分かりそう」で「分からない」でも「分かった」気に ...
プログラマーのための YAML 入門 (初級編)
【Ruby入門】YAMLの使い方をわかりやすく解説!
name,email,password_digest(あとでBCryptでハッシュ化)を含むYAMLを作成する。ラベルはmichaeltest/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %>
3項演算子の作りとしては、
nilかfalseであれば(「:」から)下側のcost(精査)が、
「?」の左側の「min_cost」がそれ以外であれば上側の 「MIN_COST(簡易チェック)」が選択される。app/models/user.rb# 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end
統合テストに成功してたときのメソッド・テスト文を追加する。test/integration/users_login_test.rbclass UsersLoginTest < ActionDispatch::IntegrationTest #成功した時のメソッド(上部に追加) def setup @user = users(:michael) end 省略 test "login with valid information" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } # @userにリダイレクトされているかチェックしてされれば以下が動く assert_redirected_to @user follow_redirect! assert_template 'users/show' # count:0は「そのリンクが存在しないよね?」のチェックができる assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) end先ほど作ったfixtureのサンプルデータにはラベルにmichaelを入れたので呼び出しができる、
さらにsignup(ユーザ登録)し終えたらログインの過程はスキップしていいので、ユーザコントローラのリファクタリングとして下記を追記する。
① createアクションに保存成功時log_inへ
② ヘルパーから使うのでincludeを冒頭にapp/controllers/users_controller.rbclass UsersController < ApplicationController include SessionsHelper #=> ヘルパー連動 省略 def create @user = User.new(user_params) if @user.save log_in @user #=> ユーザー登録時にログイン flash[:success] = "Welcome to the Sample App!"
ただ、このタイミングで usersコントローラとsessionsコントローラの両方でApplicationControllerから継承されている(下記)ので DRY に< ApplicationController双方のincludeを消してapplicationコントローラのみincludeを追加する。
application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper 省略テストのヘルパーメソッド(追加)
test/test_helper.rb# テストユーザーがログイン中の場合にtrueを返す def is_logged_in? !session[:user_id].nil? end
assert(バグが無い様に条件式を埋め込んで、明示すること)追加。test/integration/users_signup_test.rb#showテンプレート表示後 assert is_logged_in? #=> signup 終えた人はログインも終わってるか?8.3 ログアウト
ユーザが「Log out」をクリックしたらDELETEアクションが反応するようにする。
<大まかな流れ>
1.セッションからユーザーIDを削除(ユーザのログアウト)
2.ヘルパーにlog_outメソッド、sessionsコントローラにdeleteメソッドの実装
3.統合テストapp/helpers/sessions_helper.rb# 現在のユーザーをログアウトする def log_out # キーを指定すると、キーに該当するバリュー(今回はユーザid)を削除してくれる session.delete(:user_id) @current_user = nil end
Sessionsコントローラにdestroyアクション追加(DELETEリクエストがlog_outアクションにきたら動く、rootにリダイレクト)。app/controllers/sessions_controller.rb# DELETE /logout def destroy log_out redirect_to root_url endこの時点でページに行き「Log out」をクリックするとホームに戻ってる。
ログアウトのテストも記述するが、ログインのテストの末尾に追記していく。
test/integration/users_login_test.rb# 分かるよう下記""の末尾に「 followed by logout(ログアウトもしてるよ)」追加 test "login with valid information followed by logout" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } # ログインしてるよね?追記(なくてもok) assert is_logged_in? assert_redirected_to @user follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) # ~~ 下記すべてログアウトテスト分 ~~ # deleteリクエストをlogout_pathに送りつける delete logout_path # sessions情報が消えるので、ログインしてないですよね? assert_not is_logged_in? assert_redirected_to root_url follow_redirect! assert_select "a[href=?]", login_path assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 end一通り終了!!
herokuへデプロイして、ログアウト動作の確認。
$ git checkout master $ git merge basic-login $ git push heroku master「Log out」クリックして、
無事ホーム(root)に戻ったのでok!
- 投稿日:2020-01-10T19:34:54+09:00
[Rails]form_withでutf8=✓を消す方法
- 投稿日:2020-01-10T19:21:52+09:00
[Rails]Deviseの有効期限設定する方法
最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!URLの有効期限
設定ファイル
config/initializers/devise.rbを編集config/initializers/devise.rbDevise.setup do |config| # 省略 config.confirm_within = 1.minutes # 省略 endこの記述内容はアカウントを登録してから
・1分以内なら確認化
・1分以降なら確認不可
になる設定ができる!参考記事
- 投稿日:2020-01-10T19:06:18+09:00
#Rails + #MySQL / Transaction と INSERT RECORD と ROLLBACK で排他制御の実験
パターン1 - プロセスA
INSERTのあと、しばらく後続の処理が続くが、最後には失敗するケース
Transaction内の処理が失敗してロールバックするActiveRecord::Base.transaction { User.create!(unique_id: 'X1'); sleep 15; raise; } # (0.7ms) BEGIN # User Create (0.9ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X1', '2020-01-09 08:18:34') # # ... wait ... # # (10.4ms) ROLLBACK # from (pry):14:in `block in <main>'パターン1 - プロセスB
プロセスAのTransaction内での処理と重複するレコードをINSERTする
プロセスAの処理が失敗するのを待ってからコミットされ、さらに後続の処理が開始・成功するActiveRecord::Base.transaction { User.create!(unique_id: 'X1'); puts 'OK!'; } # # (0.5ms) BEGIN # # ... wait ... # # User Create (2573.1ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X1', '2020-01-09 08:18:41') # OK! # (9.4ms) COMMIT # => nilパターン2 - プロセスA
INSERTのあと、しばらく後続の処理が続いて、最後に成功するケース
ActiveRecord::Base.transaction { User.create!(unique_id: 'X2'); sleep 15; puts 'OK!'; } # (1.0ms) BEGIN # User Create (2.2ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X2', '2020-01-09 08:22:14') # # ... wait ... # # OK! # (19.2ms) COMMIT # => nilパターン2 - プロセスB
プロセスAの処理が成功するまで INSERT は待ち受け状態になる
プロセスAの処理が成功してTransactionが終了すると、こちらのINSERTはユニーク制限に反することが確定するため、そのタイミングで処理が失敗する
INSERT より後続の処理は実行されないActiveRecord::Base.transaction { User.create!(unique_id: 'X2'); puts 'OK!'; } # # ... wait ... # # (0.6ms) BEGIN # User Create (6831.2ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X2', '2020-01-09 08:22:22') # (5.2ms) ROLLBACK # ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry 'X2' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X2', '2020-01-09 08:22:22')パターン3 - プロセスA
他のパターンと同じく、INSERTの後にしばらくTransaction内の処理が続く
ActiveRecord::Base.transaction { User.create!(unique_id: 'X3'); sleep 15; puts 'OK!'; }パターン3 - プロセスB
プロセスAのTransaction内での処理とは重複しないレコードをINSERTする
ユニーク制限に弾かれない登録なので、すぐコミットされるActiveRecord::Base.transaction { User.create!(unique_id: 'Y1'); puts 'OK!'; } # (0.8ms) BEGIN # User Create (0.9ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('Y1', '2020-01-09 08:18:38') # OK! # (2.9ms) COMMIT # => nilOriginal by Github issue
- 投稿日:2020-01-10T19:06:16+09:00
#Rails + #MySQL / Transaction & INSERT RECORD & ROLLBACK / Lock and wait example
Pattern1 - ProcessA
ActiveRecord::Base.transaction { User.create!(unique_id: 'X1'); sleep 15; raise; } # (0.7ms) BEGIN # User Create (0.9ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X1', '2020-01-09 08:18:34') # # ... wait ... # # (10.4ms) ROLLBACK # from (pry):14:in `block in <main>'Pattern1 - ProcessB
ActiveRecord::Base.transaction { User.create!(unique_id: 'X1'); puts 'OK!'; } # # (0.5ms) BEGIN # # ... wait ... # # User Create (2573.1ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X1', '2020-01-09 08:18:41') # OK! # (9.4ms) COMMIT # => nilPattern2 - ProcessA
ActiveRecord::Base.transaction { User.create!(unique_id: 'X2'); sleep 15; puts 'OK!'; } # (1.0ms) BEGIN # User Create (2.2ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X2', '2020-01-09 08:22:14') # # ... wait ... # # OK! # (19.2ms) COMMIT # => nilPattern2 - ProcessB
ActiveRecord::Base.transaction { User.create!(unique_id: 'X2'); puts 'OK!'; } # # ... wait ... # # (0.6ms) BEGIN # User Create (6831.2ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X2', '2020-01-09 08:22:22') # (5.2ms) ROLLBACK # ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry 'X2' for key 'index_users_on_unique_id': INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('X2', '2020-01-09 08:22:22')Pattern3 - ProcessA
ActiveRecord::Base.transaction { User.create!(unique_id: 'X3'); sleep 15; puts 'OK!'; }Pattern3 - ProcessB
ActiveRecord::Base.transaction { User.create!(unique_id: 'Y1'); puts 'OK!'; } # (0.8ms) BEGIN # User Create (0.9ms) INSERT INTO `users` (`unique_id`, `created_at`) VALUES ('Y1', '2020-01-09 08:18:38') # OK! # (2.9ms) COMMIT # => nilOriginal by Github issue
- 投稿日:2020-01-10T18:58:52+09:00
【rails】Ajaxを使ったフォローボタンの実装で躓いたところ
はじめに
初学者がポートフォリオ作成の際に躓いたことをメモします。
今回は躓いた部分だけ記載します。全体を知りたい方は下記のリンクがわかりやすいです。
https://railstutorial.jp/chapters/following_users?version=4.2#sec-a_working_follow_button_the_standard_way
https://qiita.com/Kaisyou/items/e918b77465e3f55c97a2Ajaxとは
『Ajaxとは、あるWebページを表示した状態のまま、別のページや再読込などを伴わずにWebサーバ側と通信を行い、動的に表示内容を変更する手法。ページ上でプログラムを実行できるプログラミング言語JavaScriptの拡張機能を用いる。』
簡単に言うと、『javascriptによって必要な部分だけを更新する技術。その為ページ全体を再読み込みする必要がなく、素早いレスポンスが可能になる』と言ったところでしょうか?
前提
今回はユーザー一覧画面(users/index)でフォローボタンを表示します。
users_controller.rbdef index @q = User.ransack(params[:q]) @users = @q.result(distinct: true) endusers/index.slim- @users.each do |user| # (中略) == render "follow_form" , object: @user if logged_in?users/_follow_form.slim- unless current_user?(object) div id="follow_form_#{object.id}" #フォロー一覧画面でボタンを表示させる際にuserを区別する為に動的にしている - if current_user.following?(object) = render 'unfollow' , object: object - else = render 'follow' , object: objectusers/_follow.slim= form_with(model: current_user.active_relationships.build) do |f| = hidden_field_tag :followed_id, object.id = f.submit "Follow", class: "btn btn-primary"躓いたところ(1) relationships_controller.rb内の
respond_toについて他の記事を見ると
respond_toを使っている場合が多かったが内容が理解出来なかった。
respond_toについて以下のリンクがわかりやすかった。
https://www.javadrive.jp/rails/controller/index7.html#section3
送られてくるリクエストの形式によって処理を分ける。リクエストがHTML形式で送られてきた場合とjs形式で送られてきた場合で処理を分けている。relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_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 end
しかし今回はform_withでremote:trueを設定している為、HTML形式でリクエストが送られることはないので以下のようにした。relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create @user = User.find(params[:followed_id]) current_user.follow(@user) end def destroy @user = Relationship.find(params[:id]).followed current_user.unfollow(@user) end endremote:trueを設定している為、JS形式のリクエストが送られる。デフォルトでアクション名と同じ名前のjsファイルが呼ばれる。
( createアクションの場合: app/views/relationships/create.js.erb )
躓いたところ(2) create.js.erb内の
"#follow_form_<%= @user.id %>JQueryのコードの理解が足りずバグが発生した。users/show画面では
#follow_formで良いが、ユーザー 一覧画面の場合ユーザー を区別しなければならない為、idを動的に変更する必要がある。views/relationships/create.js.erb$("#follow_form_<%= @user.id %>").html("<%= j(render 'users/unfollow', object: @user) %>"); $("#followers").html('<%= @user.followers.count %>'); #ユーザー一覧画面では使わない(フォロワーの数を表示しない為)上記のコードはJQueryで書かれており、htmlメソッドは指定した要素を書き換えることができる。
htmlメソッドについて https://www.sejuku.net/blog/38267
一行目でusers/showの#follow_form<%= @user.id %>の内容をusers/unfollowに置き換えている。また@userをusers/unfollow内でobjectとして参照している。最後に
誰かのお役にたてれば幸いです。
間違えている部分があればコメントお願いいたします。
- 投稿日:2020-01-10T18:53:32+09:00
アイデアは早めに形にした方がいいぞ!2020
アイデアは早めに形にした方がいいぞ!2020
似たような内容の記事は探せば5万と出てくると思います。
私もインターネットをクロールしているときに見たことがある気がします。ですがそういった歴史上の出来事がすっかり頭から抜け落ちていた私は、どちらかといえば経験から学ぶタイプ、つまり愚者側の人間だったためこのような事態になってしまいました。
今後このようなことがないよう戒めのために記録を残しておくことにします。
読み物と思って見ていただければ感激です。
TL;DR
アイデアは早い段階で形にしたほうがいいぞ!
前書き
令和時代になった今、AWSをまともに触っていないのは自分だけなのではないかと思い、何かしらのWebアプリケーションをデプロイしてみようと思った9月の秋が始まりである。
何を作った?
動画の切り抜きをシェアするWebアプリを開発した。
個人的にめちゃくちゃ欲しかった。隙間時間を有効に
CUTTERSは、動画や生放送を見たいが時間がない人のためのツールです。
他人が投稿したおすすめの動画ポイントをシェアして、盛り上がりましょう。仕組みとしては単純で、youtubeの動画はパラメータを指定してやることで一定の範囲内を再生することができるため、これを利用したものになる。
入力値として
動画のURL、切り抜きの開始時間、切り抜きの終了時間を指定してやることで下記のようなクエリを作成する。切り抜きのiframe要素<iframe src="https://www.youtube.com/embed<%= @uri.path %>?start=<%= @cutter.start_time %>&end=<%= @cutter.end_time %>" > </iframe>これをiframeなどの要素で表示することで切り抜きを簡単に投稿、シェアすることができるというサービスである。
今回はAWSに慣れるということに重きを置いていたので、Webアプリケーションを楽にデプロイできるという認識のもとElastic BeanstalkというAWSのサービスを利用することにした。
また、慣れているという理由からRuby on Railsを使用して開発をすることに決めた。
仕組み自体は難しいものでもなく、EC2を利用したことはある。短い時間で公開ができそうだな。
開発環境
- Mac book pro
- Runy on Rails 6.0.2
- ruby 2.6.3
- Docker
問題が発生する
だが、アプリの公開は順調に進まなかった(今では後悔ばかりだ)
問題1. Elastic Beanstalkわからない
単にWebアプリを開発してデプロイするだけなら簡単そうだと考えた俺は背後から近づいてくるwenpackerに気づかなかった。
Rails6は標準でwebpackerでのjavascriptの処理をサポートしており、Elastic Beanstalkでデプロイするときにも、独特の設定を記述しないといけない(例えば
yarnなどのコマンドがインストールされていないといけないなどだ。)設定ファイルは以下のドキュメントを見ながら設定することができる。
https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/ebextensions.html
まずここの設定で詰まってしまった。
あるブログを参考に、公開されている設定ファイルに追加で以下のように記述することでエラーログが発生しなくなった
19_install_webpack_rails: command: "bundle exec rails webpacker:install" command: "rails webpacker:install"しかし、今度はエラーログが発生していないのにもかかわらず、状態が
Failedになるという問題にぶつかってしまった。
sshで接続し、直接エラーログを参照しても、コマンドは正常に終了していると表示されており、エラーが発生していないため原因は謎だ。
余談だが私はAWSの無料枠を利用しており、無料枠の80%を超えると「そろそろ超えるよ」というメールを受け取るように設定していたのだが、「そろそろ超えるよ」メールと、料金発生メールが同時にくるという現象に襲われてしまった。
このようにAWSと格闘していると2週間の時がすぎてしまった
問題2. 先駆者現る
もたもたしている間に同じことを考えていた人が似たサービスを公開してしまった。
完全にレスポンシブデザインで作成された美しいそれはcssやWebデザイン初学者の私を過去へ置き去りにした
AWSを学ぶことに重きを置いていたとはいえ先をこされてしまったのは純粋に悔しかった。
主機能さえ作ってしまえば細かいところはあとで足していけば良かったのである。物づくりのモチベーションが落ちるのは、自分の作っているものに価値がない時がついたときだと知った。
結論
主機能が出来次第デプロイし、徐々に改善していくのがいいかも
- 投稿日:2020-01-10T17:57:29+09:00
アイデアは早めに形にした方がいいぞ!2020
アイデアは早めに形にした方がいいぞ!2020
似たような内容の記事は探せば5万と出てくると思います。
私もインターネットをクロールしているときに見たことがある気がします。ですがそういった歴史上の出来事がすっかり頭から抜け落ちていた私は、どちらかといえば経験から学ぶタイプ、つまり愚者側の人間だったためこのような事態になってしまいました。
今後このようなことがないよう戒めのために記録を残しておくことにします。
読み物と思って見ていただければ感激です。
TL;DR
アイデアは早い段階で形にしたほうがいいぞ!
前書き
令和時代になった今、AWSをまともに触っていないのは自分だけなのではないかと思い、何かしらのWebアプリケーションをデプロイしてみようと思った9月の秋が始まりである。
何を作った?
動画の切り抜きをシェアするWebアプリを開発した。
個人的にめちゃくちゃ欲しかった。隙間時間を有効に
CUTTERSは、動画や生放送を見たいが時間がない人のためのツールです。
他人が投稿したおすすめの動画ポイントをシェアして、盛り上がりましょう。仕組みとしては単純で、youtubeの動画はパラメータを指定してやることで一定の範囲内を再生することができるため、これを利用したものになる。
入力値として
動画のURL、切り抜きの開始時間、切り抜きの終了時間を指定してやることで下記のようなクエリを作成する。切り抜きのiframe要素<iframe src="https://www.youtube.com/embed<%= @uri.path %>?start=<%= @cutter.start_time %>&end=<%= @cutter.end_time %>" > </iframe>これをiframeなどの要素で表示することで切り抜きを簡単に投稿、シェアすることができるというサービスである。
今回はAWSに慣れるということに重きを置いていたので、Webアプリケーションを楽にデプロイできるという認識のもとElastic BeanstalkというAWSのサービスを利用することにした。
また、慣れているという理由からRuby on Railsを使用して開発をすることに決めた。
仕組み自体は難しいものでもなく、EC2を利用したことはある。短い時間で公開ができそうだな。
開発環境
- Mac book pro
- Runy on Rails 6.0.2
- ruby 2.6.3
- Docker
問題が発生する
だが、アプリの公開は順調に進まなかった(今では後悔ばかりだ)
問題1. Elastic Beanstalkわからない
単にWebアプリを開発してデプロイするだけなら簡単そうだと考えた俺は背後から近づいてくるwenpackerに気づかなかった。
Rails6は標準でwebpackerでのjavascriptの処理をサポートしており、Elastic Beanstalkでデプロイするときにも、独特の設定を記述しないといけない(例えば
yarnなどのコマンドがインストールされていないといけないなどだ。)設定ファイルは以下のドキュメントを見ながら設定することができる。
https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/ebextensions.html
まずここの設定で詰まってしまった。
あるブログを参考に、公開されている設定ファイルに追加で以下のように記述することでエラーログが発生しなくなった
19_install_webpack_rails: command: "bundle exec rails webpacker:install" command: "rails webpacker:install"しかし、今度はエラーログが発生していないのにもかかわらず、状態が
Failedになるという問題にぶつかってしまった。
sshで接続し、直接エラーログを参照しても、コマンドは正常に終了していると表示されており、エラーが発生していないため原因は謎だ。
余談だが私はAWSの無料枠を利用しており、無料枠の80%を超えると「そろそろ超えるよ」というメールを受け取るように設定していたのだが、「そろそろ超えるよ」メールと、料金発生メールが同時にくるという現象に襲われてしまった。
このようにAWSと格闘していると2週間の時がすぎてしまった
問題2. 先駆者現る
もたもたしている間に同じことを考えていた人が似たサービスを公開してしまった。
完全にレスポンシブデザインで作成された美しいそれはcssやWebデザイン初学者の私を過去へ置き去りにした
AWSを学ぶことに重きを置いていたとはいえ先をこされてしまったのは純粋に悔しかった。
主機能さえ作ってしまえば細かいところはあとで足していけば良かったのである。物づくりのモチベーションが落ちるのは、自分の作っているものに価値がない時がついたときだと知った。
結論
主機能が出来次第デプロイし、徐々に改善していくのがいいかも
- 投稿日:2020-01-10T17:23:05+09:00
【Rails】アカウント有効化
- あらかじめDBには有効化されていない状態のレコードがある
- 顧客ID、メールアドレス、電話番号を入力すると有効化メールが送信される
- リンクを踏んで有効化、ユーザー情報更新、ログイン
という流れを実装したのでまとめます。
前提
- customersテーブルには
activation_digest、activated_atの2つのカラムが必要(無い場合は追加)Model
attr_accessorでactivation_token属性を有効化- トークンとダイジェストを生成、代入するメソッドを作成
- 引数にトークンを取って、ダイジェストと比較するメソッドを作成
app/models/customer.rbclass Customer < ApplicationRecord attr_accessor :activation_token . . # 引数のハッシュ値を返す def self.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end # ランダムトークン生成 def self.new_token SecureRandom.urlsafe_base64 end # 有効化トークンとダイジェストを作成及び代入 def create_activation_digest self.activation_token = self.class.new_token self.activation_digest = self.class.digest(activation_token) end # トークンがダイジェストと一致したらtrueを返す def authenticated?(activation_token) return false if activation_digest.nil? BCrypt::Password.new(activation_digest).is_password?(activation_token) end endMailer
インスタンス変数を引数にとり、メールアドレスやトークンを抽出して送信します。
なお、本番環境のメール送信部分は未完成です。サーバーのログからメールの文面を確認してます。
【Rails】メールを送信する(ローカル)app/mailers/customer_mailer.rbclass CustomerMailer < ApplicationMailer def account_activation(customer) @customer = customer mail to: customer.email, subject: 'アカウント有効化' end endapp/views/customer_mailer/account_activation.text.erb以下のアドレスにアクセスし、アカウントを有効化してください。 <%= edit_account_activation_url(id: @customer.activation_token, email: @customer.email) %>Controller
- newアクション:メールアドレス等からユーザー(アクティベートされてない)の検索フォームを表示する
- createアクション:該当するユーザーが見つかったら、トークンとダイジェストを生成し、メールを送信
- sentアクション:「メールを送信しました」という画面を表示する
- editアクション:トークンとダイジェストを比較し、一致したらユーザー登録フォームを表示
- updateアクション:ユーザー情報更新 + アカウント有効化
app/controllers/account_activations_controller.rbclass AccountActivationsController < PublicController #メールアドレス等からユーザー(アクティベートされてない)の検索フォームを表示する def new @customer = Customer.new end #該当するユーザーが見つかったら、トークンとダイジェストを生成し、メールを送信 def create customer_id = params[:customer][:customer_id] email = params[:customer][:email] phone = params[:customer][:phone] @customer = Customer.find_by('(customer_id = ?) AND (email = ?) AND (phone = ?)', customer_id, email, phone) unless @customer flash.now[:danger] = '該当するユーザーが見つかりませんでした' @customer = Customer.new render :new and return end @customer.create_activation_digest @customer.save CustomerMailer.account_activation(@customer).deliver_now redirect_to account_activations_sent_url(customer_id: @customer.id) end #「メールを送信しました」という画面を表示する def sent @customer = Customer.find_by(customer_id: params[:customer_id]) end #トークンとダイジェストを比較し、一致したらユーザー登録フォームを表示 def edit @customer = Customer.find_by(email: params[:email]) unless @customer flash[:danger] = 'ユーザーが見つかりませんでした' redirect_to login_url and return end unless @customer.activated_at.nil? flash[:danger] = 'このアカウントはすでに有効化されています' redirect_to login_url and return end unless @customer&.authenticated?(params[:id]) flash[:danger] = 'エラーが発生しました。もう一度やり直してください。' redirect_to login_url and return end end #ユーザー情報更新 + アカウント有効化 def update @customer = Customer.find_by(customer_id: params[:id]) unless activation_params[:password].length >= 8 flash.now[:danger] = 'パスワードは8文字以上で入力してください' render :edit and return end if @customer.update(activation_params) @customer.update_attributes(activated_at: Time.zone.now) log_in @customer render 'account_activations/complete' else flash.now[:danger] = '入力内容に誤りがあります' render :edit end end private def activation_params params.require(:customer).permit(:customer_id, :email, :nickname, :phone, :password, :password_confirmation) end endView
app/views/account_activations/new.html.erb<%= form_with model: @customer, url: '/account_activations', local: true do |f| %> <%= f.text_field :customer_id %> <%= f.text_field :phone %> <%= f.text_field :email %> <%= f.submit value: '送信' %> <% end %>app/views/account_activations/sent.html.erb<p> ご入力いただいたメールアドレス(<%= @customer.email %>)に、確認用メールを送信しました。 メール本文に記載されているURLを開き、会員登録を完了させてください。 </p>app/views/account_activations/edit.html.erb<%= form_with model: @customer, url: "/account_activations/#{@customer.customer_id}", local: true do |f| %> <%= f.text_field :customer_id, readonly: true %> <%= f.text_field :phone, readonly: true %> <%= f.text_field :email, readonly: true %> <%= f.password_field :password, minlength: '8', required: 'true' %> <%= f.password_field :password_confirmation, minlength: '8', required: 'true' %> <%= f.text_field :nickname %> <%= f.submit '登録' %> <% end %>app/views/account_activations/edit.html.erb<p class="box__toptxt">ご登録が完了いたしました。</p> <%= link_to 'トップページ', root_path %>ルーティング
config/routes.rbresources :account_activations, only: %i[new create edit update] get '/account_activations/sent', to: 'account_activations#sent'問題点
Modelでパスワードに入れる値に制限をかけたら、editアクションで
@customerを取得することができなくなってしまったので、仕方なくコントローラーでバリデーションの処理を実装しました。
- 投稿日:2020-01-10T17:23:05+09:00
【Rails】アカウントをアクティベート
- あらかじめDBには有効化されていない状態のレコードがある
- 顧客ID、メールアドレス、電話番号を入力すると有効化メールが送信される
- リンクを踏んで有効化、ユーザー情報更新、ログイン
という流れを実装したのでまとめます。
前提
- customersテーブルには
activation_digest、activated_atの2つのカラムが必要(無い場合は追加)Model
attr_accessorでactivation_token属性を有効化- トークンとダイジェストを生成、代入するメソッドを作成
- 引数にトークンを取って、ダイジェストと比較するメソッドを作成
app/models/customer.rbclass Customer < ApplicationRecord attr_accessor :activation_token . . # 引数のハッシュ値を返す def self.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end # ランダムトークン生成 def self.new_token SecureRandom.urlsafe_base64 end # 有効化トークンとダイジェストを作成及び代入 def create_activation_digest self.activation_token = self.class.new_token self.activation_digest = self.class.digest(activation_token) end # トークンがダイジェストと一致したらtrueを返す def authenticated?(activation_token) return false if activation_digest.nil? BCrypt::Password.new(activation_digest).is_password?(activation_token) end endMailer
インスタンス変数を引数にとり、メールアドレスやトークンを抽出して送信します。
なお、本番環境のメール送信部分は未完成です。サーバーのログからメールの文面を確認してます。
【Rails】メールを送信する(ローカル)app/mailers/customer_mailer.rbclass CustomerMailer < ApplicationMailer def account_activation(customer) @customer = customer mail to: customer.email, subject: 'アカウント有効化' end endapp/views/customer_mailer/account_activation.text.erb以下のアドレスにアクセスし、アカウントを有効化してください。 <%= edit_account_activation_url(id: @customer.activation_token, email: @customer.email) %>Controller
- newアクション:メールアドレス等からユーザー(アクティベートされてない)の検索フォームを表示する
- createアクション:該当するユーザーが見つかったら、トークンとダイジェストを生成し、メールを送信
- sentアクション:「メールを送信しました」という画面を表示する
- editアクション:トークンとダイジェストを比較し、一致したらユーザー登録フォームを表示
- updateアクション:ユーザー情報更新 + アカウント有効化
app/controllers/account_activations_controller.rbclass AccountActivationsController < PublicController #メールアドレス等からユーザー(アクティベートされてない)の検索フォームを表示する def new @customer = Customer.new end #該当するユーザーが見つかったら、トークンとダイジェストを生成し、メールを送信 def create customer_id = params[:customer][:customer_id] email = params[:customer][:email] phone = params[:customer][:phone] @customer = Customer.find_by('(customer_id = ?) AND (email = ?) AND (phone = ?)', customer_id, email, phone) unless @customer flash.now[:danger] = '該当するユーザーが見つかりませんでした' @customer = Customer.new render :new and return end @customer.create_activation_digest @customer.save CustomerMailer.account_activation(@customer).deliver_now redirect_to account_activations_sent_url(customer_id: @customer.id) end #「メールを送信しました」という画面を表示する def sent @customer = Customer.find_by(customer_id: params[:customer_id]) end #トークンとダイジェストを比較し、一致したらユーザー登録フォームを表示 def edit @customer = Customer.find_by(email: params[:email]) unless @customer flash[:danger] = 'ユーザーが見つかりませんでした' redirect_to login_url and return end unless @customer.activated_at.nil? flash[:danger] = 'このアカウントはすでに有効化されています' redirect_to login_url and return end unless @customer&.authenticated?(params[:id]) flash[:danger] = 'エラーが発生しました。もう一度やり直してください。' redirect_to login_url and return end end #ユーザー情報更新 + アカウント有効化 def update @customer = Customer.find_by(customer_id: params[:id]) unless activation_params[:password].length >= 8 flash.now[:danger] = 'パスワードは8文字以上で入力してください' render :edit and return end if @customer.update(activation_params) @customer.update_attributes(activated_at: Time.zone.now) log_in @customer render 'account_activations/complete' else flash.now[:danger] = '入力内容に誤りがあります' render :edit end end private def activation_params params.require(:customer).permit(:customer_id, :email, :nickname, :phone, :password, :password_confirmation) end endView
app/views/account_activations/new.html.erb<%= form_with model: @customer, url: '/account_activations', local: true do |f| %> <%= f.text_field :customer_id %> <%= f.text_field :phone %> <%= f.text_field :email %> <%= f.submit value: '送信' %> <% end %>app/views/account_activations/sent.html.erb<p> ご入力いただいたメールアドレス(<%= @customer.email %>)に、確認用メールを送信しました。 メール本文に記載されているURLを開き、会員登録を完了させてください。 </p>app/views/account_activations/edit.html.erb<%= form_with model: @customer, url: "/account_activations/#{@customer.customer_id}", local: true do |f| %> <%= f.text_field :customer_id, readonly: true %> <%= f.text_field :phone, readonly: true %> <%= f.text_field :email, readonly: true %> <%= f.password_field :password, minlength: '8', required: 'true' %> <%= f.password_field :password_confirmation, minlength: '8', required: 'true' %> <%= f.text_field :nickname %> <%= f.submit '登録' %> <% end %>app/views/account_activations/complete.html.erb<p class="box__toptxt">ご登録が完了いたしました。</p> <%= link_to 'トップページ', root_path %>ルーティング
config/routes.rbresources :account_activations, only: %i[new create edit update] get '/account_activations/sent', to: 'account_activations#sent'問題点
Modelでパスワードに入れる値に制限をかけたら、editアクションで
@customerを取得することができなくなってしまったので、仕方なくコントローラーでバリデーションの処理を実装しました。
- 投稿日:2020-01-10T17:09:11+09:00
bundle installするときに "can't find gem bundler (>= 0.a) with executable bundler (Gem::GemNotFoundException)" とErrorが出る
問題が起きたらググれって偉い人が言ってた
対象のディレクトリ下でbundle installするときに起った現象のメモ
※初学者なので細かめに記載git cloneしたアプリのgemを入れようとしたら以下のエラーが出た。
consoleTraceback (most recent call last): 2: from /Users/mac/.rbenv/versions/2.5.3/bin/bundler:23:in `<main>' 1: from /Users/mac/.rbenv/versions/2.5.3/lib/ruby/2.5.0/rubygems.rb:308:in `activate_bin_path' /Users/mac/.rbenv/versions/2.5.3/lib/ruby/2.5.0/rubygems.rb:289:in `find_spec_for_exe': can't find gem bundler (>= 0.a) with executable bundler (Gem::GemNotFoundException)結局原因ってなんなの?
どうやらbundlerとGemfile.lockのversionが異なっていたので、
『bundlerのgemが見つからないよ!!!!!』って怒られていたらしい。
version違うだけで見つからないのは、当たり前とはいえ
ぶち当たるとちょっとテンション落ちる。
ドジっ子と思えばそうでもないかもしれない。どうやって解決したの?
方法は2つ
・bundlerのダウングレードをする
・Gemfile.lockにある 'BUNDLED WITH' をbunderのversionに対応させるbunderのダウングレードはいっぱい記事があるので、
検索してみてください。そもそものbundlerのversionチェック
ルートディレクトリで以下のコマンドを叩く
bundlercheck$ bundle -vGemfile.lockにある 'BUNDLED WITH' をbunderのversionに対応させるの方法
bundler$ cd ~/[対象のディレクトリ]bundler$ vim Gemfile.lockファイルの中身が見れるようになるので、
G(Shift+g)で最終行へ
x.x.xを対応のversionに書き換えます。Gemfile.lockBUNDLED WITH x.x.x書き換え方
i → インサートモードへ(書き換えができるようになる)
esc → インサートモードの終了
:wq!を入力 → 保存して強制終了これでbundle -v したり bundle install して、
問題なければヨシッ!まとめ
いろんな記事を読んで模索した結果なので、これが最善の解説策かはわかりません。
なのでこれだけでなくいろんな記事を読んで、
ここは一つの参考にしていただければと思います。
- 投稿日:2020-01-10T16:25:45+09:00
モデルのテストの記述方法を細かくみてみた
下記のようなテスト文を、後からわかりやすいように、細かくみてみた。
spec/models.rb/message_spec.rbrequire 'rails helper' RSpec.describe Message, type: :model do describe '#create' do context 'can save' do it 'is valid with content' do expect(build(:message,image: nil)).to be_valid end end context 'can not save' do it 'is invalid without content and image' do message = build(:message, content: nil, image: nil) message.valid? expect(message.errors[:content]).to include("を入力してください") end end end end以下に説明を記述。
spec/models.rb/message_spec.rbrequire 'rails helper' RSpec.describe Message, type: :model do # RSpec.describe で、テストのグループ化を宣言。モデル名, type: model: で、モデルに関するテストであることを付け加えている。 describe '#create' do # 上記でcreateアクション時のテストであると宣言 context 'can save' do # この中にメッセージを保存できる場合のテストを記述 it 'is valid with content' do #メッセージがある expect(build(:message,image: nil)).to be_valid # expect(X).to Y で、XのときYされることを期待するという意味のコードになる # build(カラム名: 値)の形で引数を渡すことによって、ファクトリーで定義されたデフォルトの値を上書きする。(buildメソッド) # expect(build(:メッセージ,画像:ない).to 左記のときに保存された場合テストにパスするといったコードである。 end end context 'can not save' do # この中にメッセージを保存できない場合のテストを記述 it 'is invalid without content and image' do # 画像もメッセージもない message = build(:message, content: nil, image: nil) message.valid? # 下記を表示する前にメッセージの検証を行う。 expect(message.errors[:content]).to include("を入力してください") # error時の表示を設定する。 # message.errors[:カラム名] でそのカラムが原因のエラー文が入った配列を取り出す(エラー文はrails内に自動で設定されている) # 上記で生成されたエラー文に"を入力されています"を含んでいる(include)場合テストにパスするといったコードである。 end end end end
- 投稿日:2020-01-10T16:19:44+09:00
Railsでカンマ区切り+小数点以下表示で数値を出力する方法
目的
整数部をカンマ区切りで表示し、小数点以下を表示したい
コード
number_with_precision(123456789, precision: 2, delimiter: ',') #=> "123,456,789.00"参考リンク
ruby-on-rails — 大きな数字にカンマを追加するためのRailsトリックはありますか?
ActionView::Helpers::NumberHelper
- 投稿日:2020-01-10T15:43:15+09:00
【Rails】has_secure_passwordメソッド
has_secure_passwordメソッドについてのメモです。
準備
usersテーブルにpassword_digestカラムを追加しておく。
bcryptをインストールする。
Gemfilegem 'bcrypt'ターミナル$ bundle installモデルに
has_secure_passwordを記載する。app/models/user.rbclass User < ApplicationRecord has_secure_password end機能
- 暗号化されたパスワードがpassword_digestカラムに保存される。
- 仮想属性
password_confirmationを使用して、入力ミスを減らすためにパスワードの確認入力をさせることができる。- authenticateメソッドを使用して、メールアドレスとパスワードに基いてユーザー認証を行うことができる。
- 投稿日:2020-01-10T15:05:28+09:00
Railsアプリケーションのメモリが膨れ上がる問題
次のグラフは、Herokuで運用している、とあるRailsアプリケーションのMemory Usageです。1日1回再起動がかかったあと、スワップが発生するまでメモリ使用量が増え続けます。特に大きなデータをメモリ上に置き続けているわけではありません。
Passengerの作者Hongli Lai氏が、Rubyのメモリが膨れ上がる問題について研究して記事を書いています(2019年3月)。
日本語での概略は、次の記事の真ん中あたりで読めます。
Hongli Lai氏が見つけたのは、Rubyはメモリを正しく解放しているが、Cのライブラリ(glibc)のmalloc周りがなかなかメモリを回収してくれない、ということです。メモリ使用量よりパフォーマンスを優先しているせい、との見立てです。
メモリ使用量が膨れ上がるのを防ぐ方法は、3つあります。1と2は以前から知られた方法で、3はHongli Lai氏が発見した方法です。Hongli Lai氏の説が正しければ、3が本質的な解決方法ということになります。
- mallocの代わりにjemallocを使ってRubyをコンパイルする。
- 環境変数 MALLOC_ARENA_MAX=2 を指定する。
- ガベージコレクションの後でmalloc_trimを呼ぶ。
Hongli Lai氏は、この件について一緒に研究してくれるよう呼びかけています。
RubyのBug trackerでも報告されていますが、話は進んでいないもよう。
Rubyに上記の3のパッチを当てるプロジェクトが公開されています。残念ながらHeroku用は開発が進んでいないようです。
Herokuでは、上記の2の環境変数MALLOC_ARENA_MAX=2はデフォルトになりました。ただし、2019年9月24日より前に作られたアプリケーションでは自分で設定する必要があります。
MALLOC_ARENA_MAXを指定すると、レスポンスタイムが若干増えるという実験結果があります。
以上です。私自身は上記1-3のどれも実際には試していません、すいません。?♂️
- 投稿日:2020-01-10T14:25:28+09:00
rails usersテーブルの値をpasswordなしで更新する
はじめに
Userテーブルのカラムをpasswordなしで更新しようと思ったときに、更新ができずに手間取ってしまったのでその備忘録として記載する。
概要
usersテーブルにprofileというカラムを持たせていたのだが、ユーザー登録後にユーザーページからユーザー情報を更新したい。
ぶつかった課題
controllerで以下のように定義して実行した際にRollbackが起こり、値が更新されなかった。
mypage_controller.rbclass MypageController < ApplicationController # 途中略 def update user=User.find(current_user.id) #パスワードなしでのプロフィールの変更 user.update_without_password(nickname:params[:mypage][:nickname],profile: params[:mypage][:profile]) redirect_to root_path,notice: 'プロフィールが変更されました' end # 途中略 end
- エラー内容
[2] pry(#<MypageController>)> user.update_without_password(nickname:params[:mypage][:nickname],profile: params[:mypage][:profile]) (0.3ms) BEGIN ↳ (pry):2 User Exists (3.4ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'ytest@gmail.com' AND `users`.`id` != 21 LIMIT 1 ↳ (pry):2 (0.2ms) ROLLBACK ↳ (pry):2 => false [3] pry(#<MypageController>)> user.errors => #<ActiveModel::Errors:0x00007f8b3304a270 @base= #<User id: 21, email: "ytest@gmail.com", last_name: "test", first_name: "test", last_name_kana: "test", first_name_kana: "test", nickname: "test", profile_image: nil, birthday: "1990-12-04", telephone: "12345678", created_at: "2020-01-10 04:36:29", updated_at: "2020-01-10 04:36:29", provider: nil, uid: nil, profile: "test">, @details={:password=>[{:error=>:too_short, :count=>6}, {:error=>:blank}], :password_confirmation=>[{:error=>:blank}]}, @messages={:password=>["は6文字以上で入力してください", "を入力してください"], :password_confirmation=>["を入力してください"]}>解決策
modelファイルのバリデーションいじった。
詳細は以下
- 変更前
user.rbclass User < ApplicationRecord # 途中略 validates :password, length: { minimum: 6 } validates :password_confirmation, presence: true validates :password, presence: true, unless: :uid? end
- 変更後
passwordのバリデーションに対して、on create を追加することで、 userの新規登録するときにのみバリデーションによる入力チェックがかかるように変更した。コード内容は以下
user.rbclass User < ApplicationRecord # 途中略 # create のみバリーデーションをかける validates :password, length: { minimum: 6 }, on: :create validates :password_confirmation, presence: true, on: :create validates :password, presence: true, unless: :uid?, on: :create endとりあえずバリデーションを状況によって、かけたい場合には、オプションとしてon< xxx >を指定すればよいらしい。
ちなみに参考資料はこちらもっと勉強しないとなぁ〜
- 投稿日:2020-01-10T14:16:02+09:00
【Rails】redirect_toで変数を渡す
- 投稿日:2020-01-10T13:59:38+09:00
Railsで開発した個人アプリにDockerを導入する手順と最低限の知識
Railsで開発した個人アプリに、後からDockerを導入しました。
その時知識不足なせいでハマってしまった場面があったので、自分の中で整理してアウトプットすることを目的にこの記事を書き残します。ちなみに、以下の記事を主に参考にさせていただきました。
DockerをMacにインストールする
Docker + Rails + Puma + Nginx + MySQLRailsアプリにDockerを導入する手順+事前知識
まずは事前知識として、これは知っておいたほうがいいというものを簡単に書いていきます。
事前知識 - Docker関係
Docker(ドッカー)
※1.軽量な仮想化環境を実現するためのツール。
OS やアプリケーションを設定したものを丸ごと実行イメージとして保存できるので、Docker が導入されている別のマシンにそのまま持って行くことができる。
実行環境をテキストファイルとして共有できるのでとても便利。
※1.仮想化環境とは、コンピュータ上にソフトウェアによって仮想的に構築されたコンピュータ(仮想マシン)が備える仕様や機能の総体のこと。
Dockerイメージ
Dockerイメージは、コンテナを起動させるためのベースとなるもの(オブジェクト指向でいうと「クラス」にあたる)
テキストファイル(Dockerfile)からビルドされる。(後に記述)
DockerHub(Docker向けのコンテナ共有サービス)では、既に多くのイメージが公開されている。
Dockerコンテナ
Dockerのコンテナは、 Dockerイメージを元に作成される仮想環境の実行部分(オブジェクト指向でいうと「インスタンス」にあたる)
原則1コンテナ1アプリ。
Dockerfile(ドッカーファイル)
指定したベースのDockerイメージに加える変更を記述するファイル。
Dockerfileを使うことでオリジナルのDockerイメージを作成することができる。
docker-compose(ドッカーコンポーズ)
複数のコンテナから構成されるアプリケーションで、Dockerイメージのビルドや各コンテナの起動・停止などをより簡単に行えるようにするツール。
docker-composeを使用する際は「docker-compose.yml」が必要になる。
事前知識 - サーバ関係
ミドルウェア
OSとアプリケーションの間に入り、中間的な処理を行うソフトウェアのこと。
ー 例 ー
- Webサーバ
- Apache、NginXなど
- APサーバ
- Puma
- Unicornなど
- DBサーバ
- MySQL
- PostgreSQLなど
Nginx(エンジンエックス)
webサーバの一つ。
Apacheよりも処理能力が高い。
puma(プーマ)
※1.Rackという機能を提供するためのアプリケーションサーバ。
webサーバの1つでもある。
※1.RackとはWeb サーバと Rubyやフレームワークをつなぐ最小のインタフェースを提供するもの。
Docker導入手順
① Docker for Macを公式サイトからインストール、そして起動
公式サイトで会員登録を済ませた後、Docker for Macをダウンロードしインストール。
インストールが終わったら、Dockerを起動しておく。
(MACの画面上部にDockerのマークが出れば起動できてる証拠)②作成済みのアプリケーションフォルダの直下に「Dockerfile」、「docker-compose.yml」ファイルを新しく作成する。
フォルダ構成
- 既存のRailsアプリフォルダ
- app
- bin
- config
- db
- ・・・
- Dockerfile
- docker-compose.yml
③Dockerfileに記述する(アプリケーションフォルダ直下)
DockerfileFROM ruby:2.5.1 RUN apt-get update && \ apt-get install -y mysql-client nodejs vim --no-install-recommends && \ rm -rf /var/lib/apt/lists/* RUN mkdir /myproject WORKDIR /myproject ADD Gemfile /myproject/Gemfile ADD Gemfile.lock /myproject/Gemfile.lock RUN gem install bundler RUN bundle install ADD . /myproject RUN mkdir -p tmp/sockets④docker-compose.ymlに記述する(アプリケーションフォルダ直下)
docker-compose.ymlversion: '2' services: db: image: mysql:5.6 environment: - ./environments/db.env volumes: - mysql-data:/var/lib/mysql ports: - "4306:3306" app: build: . command: bundle exec puma -C config/puma.rb volumes: - .:/myproject - public-data:/myproject/public - tmp-data:/myproject/tmp - log-data:/myproject/log web: build: context: containers/nginx volumes: - public-data:/myproject/public - tmp-data:/myproject/tmp ports: - 80:80 volumes: mysql-data: public-data: tmp-data: log-data:⑤アプリケーションフォルダ直下に「environments」フォルダを作成、さらにenvironmentsフォルダ直下に「db.env」ファイルを作成する。
- 既存のRailsアプリフォルダ
- app
- bin
- config
- db
- environments
- db.env
- ・・・
- Dockerfile
- docker-compose.yml
⑥db.envを編集
environments/db.envMYSQL_ROOT_PASSWORD=password MYSQL_USER=user MYSQL_PASSWORD=password⑦database.ymlを編集
config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV.fetch('MYSQL_USER') { 'root' } %> password: <%= ENV.fetch('MYSQL_PASSWORD') { 'root' } %> host: db⑧アプリケーションフォルダ直下に「containers」フォルダを作成、さらにcontainersフォルダ直下に「nginx」フォルダを作成する。
フォルダ構成
- 既存のRailsアプリフォルダ
- app
- bin
- config
- containers
- nginx
- db
- environments
- db.env
- ・・・
- Dockerfile
- docker-compose.yml
⑨作成したnginxフォルダ直下に「Dockerfile」、「nginx.conf」ファイルを作成する。
- 既存のRailsアプリフォルダ
- app
- bin
- config
- containers
- nginx
- Dockerfile
- nginx.conf
- db
- environments
- db.env
- ・・・
- Dockerfile
- docker-compose.yml
⑩Dockerfileに記述する(containers/nginxフォルダ直下)
containers/nginx/DockerfileFROM nginx:1.15.8 RUN rm -f /etc/nginx/conf.d/* ADD nginx.conf /etc/nginx/conf.d/myproject.conf CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf⑪nginx.confに記述する(containers/nginxフォルダ直下)
containers/nginx/nginx.confupstream myproject { server unix:///myproject/tmp/sockets/puma.sock; } server { listen 80; server_name 13.112.60.229; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; root /myproject/public; client_max_body_size 100m; error_page 404 /404.html; error_page 505 502 503 504 /500.html; try_files $uri/index.html $uri @myproject; keepalive_timeout 5; location @myproject { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://myproject; } }⑫puma.rbを編集
puma.rbapp_root = File.expand_path("../..", __FILE__) bind "unix://#{app_root}/tmp/sockets/puma.sock" stdout_redirect "#{app_root}/log/puma.stdout.log", "#{app_root}/log/puma.stderr.log", true⑬ターミナルでコマンドを実行
ファイルの準備が整ったら、最後にターミナル上でコマンドを実行。
(railsアプリケーションフォルダ内で実行する)イメージを構築する
ターミナル$ docker-compose buildrailsのコンテナを作成し、データベースの作成処理を行う
ターミナル$ docker-compose run --rm app rake db:create db:migrate全てのコンテナを構築・起動する
ターミナル$ docker-compose up上記の流れが終わったあとlocalhostにアクセスすると、しっかり確認することができました。
ちなみに今回書きまとめたものは、DBもDockerで管理することになりますが、DBはローカルのものを参照したいという場合、以下の記事が参考になるかと思います。
既存のRailsアプリをDocker化し、ローカルのDBに接続する方法(おまけ)
Dockerfileに記述できるコマンド一覧
コマンド 意味 FROM ベースとなるイメージ RUN docker build 時に実行するコマンド CMD docker run 時に実行するコマンド ENTRYPOINT docker run 時に実行するコマンド MAINTAINER 作者情報 LABEL ラベル情報(メタデータ) EXPOSE 公開ポート番号 ENV 環境変数 ARG 一時変数 COPY ホストからコンテナへのファイルコピー ADD ファイル/ディレクトリの追加 VOLUME ボリュームのマウント USER 実行ユーザ SHELL シェル指定 WORKDIR ワークディレクトリ ONBUILD ビルド時に実行するコマンド STOPSIGNAL コンテナ終了時に送信されるシグナル HEALTHCHECK ヘルスチェック ターミナル上で実行できるdocker-composeコマンド一覧
コマンド 意味 build サービスの構築または再構築 config compose ファイルの確認と表示 create サービスの作成 down コンテナ・ネットワーク・イメージ・ボリュームの停止と削除 events コンテナからリアルタイムにイベントを受信 help コマンド上でヘルプを表示 kill コンテナを kill (強制停止) logs コンテナの出力を表示 pause サービスを一時停止 port ポートに割り当てる公開用ポートを表示 ps コンテナ一覧 pull サービス用イメージの取得 restart サービスの再起動 rm 停止中のコンテナを削除 run 1度だけコマンドを実行 scale サービス用コンテナの数を指定ド start サービスの開始 stop サービスの停止 up コンテナの作成と開始 version Docker Compose のバージョン情報を表示
- 投稿日:2020-01-10T13:49:18+09:00
既存のRailsアプリをDocker上で環境構築する方法+sequel proによるDBコンテナ可視化
はじめに
新規アプリをDocker環境で開発するやり方はたくさんあるけど、既存アプリをDocker環境で構築するやり方は全然見当たらず、わりと苦労しました。
振り返ってみると新規アプリでの構築の仕方とあまり変わらないはずなのですが、色々エラー出て苦労したのでまとめておきます。個々のコマンドの意味もできるだけ記載しました。
単なる環境構築だけでなく、DBコンテナのsequel proによる可視化やAPPコンテナでのbinding.pryの仕方、bundle install後にbuildし直さなくてもよい設定にする方法などもまとめました。同じく既存のRailsアプリをDocker上で構築したい人の参考になれば幸いです。
内容について間違っていたら教えていただけると嬉しいです。
開発環境
- Ruby: 2.5.1
- Rails: 5.2.4
- MySQL: 5.6
- MacOS
前提
- Docker for Macはインストール済み
- Dockerについての基礎知識
- 今回は開発環境のみ
対象読者
作成した既存のアプリをDocker上で構築したい人
目次
- コンテナ起動までの大まかな流れ
- 実際の作業
- 番外編:sequel proによるDBコンテナの可視化
- 開発する上でのDockerコマンド
- おまけ
- railsコンテナ上でbinding.pryをする方法
- railsコンテナ上でbundle installした時に、変更内容をコンテナ上に反映させる方法
コンテナ起動までの大まかな流れ
DockerfileにてRubyのベースイメージをもとにイメージを作成する
↓
作成したイメージをもとにdocker-compose.ymlでappコンテナを作成すると同時に、DBのコンテナのイメージを作成し、これらのコンテナを連携させる
↓
database.ymlを修正してappコンテナからdbコンテナへ接続できるように設定する実際の作業
- Dockerfileとdocker-compose.ymlの作成
- Dockerfileの記載
- docker-compose.ymlの記載
- database.ymlの変更
1. Dockerfileとdocker-compose.ymlの作成
まず、開発しているアプリで、Dockerfileとdocker-compose.ymlを以下のように作成します。
アプリ名 |- app |- bin |- config #略 |- vendor - .gitignore - config.rb - Dockerfile #追加 - docker-compose.yml #追加 - Gemfile - Gemfile.lock #略2. Dockerfileの記載
続いてDockerfileの中身を書いていきます。
myprojectのところはコンテナ起動の際に作成するディレクトリ名なので、何でも大丈夫です
ただし、それ以降の記述でも随時書き換えてくださいDockerfileFROM ruby:2.5.1 RUN apt-get update && \ apt-get install -y mysql-client nodejs vim --no-install-recommends && \ rm -rf /var/lib/apt/lists/* RUN mkdir /myproject WORKDIR /myproject ADD Gemfile /myproject/Gemfile ADD Gemfile.lock /myproject/Gemfile.lock RUN gem install bundler RUN bundle install ADD . /myproject詳細な説明は省きますが、ざっと説明すると
- ruby2.5.1をベースイメージとする
- コンテナ内で必要なコマンドをインストール
- myprojectというディレクトリを作成して基点にする
- Gemfileをコンテナ上にコピーした後、bundle install
- ローカルのディレクトリ、ファイルをコンテナ上にコピー
という感じかと思います。
3. docker-compose.ymlの記載
Rubyのコンテナは作成できるようになりました。
続いてこれをもとにアプリケーションのコンテナとデータベースのコンテナを作成し、それらのコンテナをリンクさせるための作業をしていきます。
docker-compose.ymlを以下のように記載します。
*mysqlは8.0以上だと認証方法が異なるようなので注意
https://qiita.com/yensaki/items/9e453b7320ca2d0461c7docker-compose.ymlversion: '2' services: db: image: mysql:5.6 volumes: - mysql-data:/var/lib/mysql #データの永続化のために必要 ports: - "4306:3306" #両方3306でもok。詳細は下の「番外編:DBをsequel proで可視化したい」へ app: tty: true #コンテナ上でbinding.pryするために必要 stdin_open: true #コンテナ上でbinding.pryするために必要 build: . command: bundle exec rails s -p 3000 -b '0.0.0.0' volumes: - .:/myproject #ローカルのディレクトリをコンテナ上にマウント - bundle:/usr/local/bundle #bundle installした後buildし直さなくてよくなる links: - db volumes: mysql-data: bundle: #bundle installした後buildし直さなくてよくなる要点は以下
- versionは2でも3でもいいと思いますが、使えるコマンドが違ってくるみたいです。
- servicesのところにdbとappがありますが、これらがそれぞれコンテナになります。
- appのコンテナのlinksにdbがあり、これによってappコンテナとdbコンテナが連携できるようになります
- 一番下のvolumesには永続化させたいデータを記載
- appコンテナ上でvolumes: - .:/myprojectとすることで、ローカルのディレクトリをマウントしている。
- dbコンテナ上でvolumes: -mysql-data:/var/lib/mysqlとすることでデータベースで変更されたデータを永続化。この記述がないと、コンテナを壊した時に変更したデータが消えてしまいます。
- bundle installとbinding.pryのための記述は本記事、最後のおまけを参照
4. database.ymlの変更
これまでの作業でappコンテナとdbコンテナを連携させる設定をしました。
最後にappコンテナからdbコンテナに接続するために、接続設定をします。
database.ymlの中身はおそらく初期設定のままだとこんな感じの記述になってるかと思います。config/database.yml変更前default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: socket: /tmp/mysql.sockこのままだとsocket通信でDB接続をするので、せっかく作成したdbコンテナが意味なくなってしまいます。
作成したdbコンテナに接続するために以下のように変更します。config/database.yml変更後default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password:password host: db #変更(docker-compose.ymlのservice名を記載)usernameやpasswordは環境変数で設定している記事もいくつか見ますが、開発環境なら気にしなくてもいいかなと思いました。
本番環境ではちゃんと環境変数を設定します。以上で準備ができました。
あとはターミナルで該当アプリのディレクトリまで移動して、docker-compose upと入力するだけです
(Gemfile.lockの中身を削除して空にしないとエラーになるかもしれません)
localhost:3000で接続確認して終了です!今回はMySQLを使いましたが、他のDBでも可能なはずです(未検証)
*ポート番号は変わるはずなのでご注意ください番外編:DBをsequel proで可視化したい
今まで開発していた時、DBをsequel proで可視化していて便利だったのですが、docker上のDBも可視化したい!
という方に向けて以下に方法を記載しますdocker-compose.ymlで以下のように記載していました
Ruby:docker-compose.yml
db:
ports:
- "4306:3306"
これはホストが4306で接続した時にコンテナ上では3306に置き換えますという意味です。
準備はこれでokなのでsequel proで接続します。
sequel proを開いて、標準タブに切り替えます。
そこで以下の内容を入力します名前: 任意、変えなくてもok ホスト: 127.0.0.1 ユーザー名: root (database.ymlに記載のユーザー名) パスワード: password (database.ymlにパスワード) データベース: 空でok ポート: 4306(docker-compose.ymlのportsに記載した左側)ホストの127.0.0.1は自分自身を表すIPアドレス
これで見れるようになるはず!
その他開発する上で必要なコマンド
docker上でrails g controllerやrails db:migrateなどを行う時は以下のようにコンテナを通して入力します
appの部分はdocker-compose.ymlで作成したコンテナ名のappのことですdocker-compose run --rm app 入力したいコマンド(例: rails db:migrate)その他必要なコマンドは以下の記事参照
https://qiita.com/gold-kou/items/44860fbda1a34a001fc1
全てのコンテナやイメージを削除する場合はこちら#全てのコンテナ停止 docker stop $(docker ps -q) #全てのコンテナ削除 docker rm $(docker ps -q -a) #全てのイメージ削除 docker rmi $(docker images -q)おまけ
bundle installしたい時
開発していく中で、gemを追加してbundle installしたい場面が出てくると思います
そのままやってもできるんですが、追加したgem内容がコンテナ内に反映されないため、イメージをbuildし直さなくてはなりません。
いくつか解決方法はあるみたいですが、今回はvolumeでマウントするという方法で解決しました。docker-compose.ymlapp: volumes: - bundle:/usr/local/bundle #中略 volumes: bundle:参考記事
https://qiita.com/neko-neko/items/abe912eba9c113fd527ebindin.pryしたい時
binding.pryをするためには以下の記述を追加します
docker-compose.ymlapp: tty: true stdin_open: trueおわりに
dockerについて全くわからないところから環境構築するのはかなり大変でした
自分なりにまとめられて良かった
同じような状況の人の参考になれば幸いです間違ってる場所があれば指摘していただけると幸いです
本番環境でDockerを使うのも苦労したので、そのうち開発環境との違いなどもまとめようかなと思います
参考記事
Dockerについての概要と色々なTIPSを知りたい場合は下記リンクがおすすめ
https://qiita.com/gold-kou/items/44860fbda1a34a001fc1実際の作業で参考にした記事
https://qiita.com/azul915/items/5b7063cbc80192343fc0
https://qiita.com/Nishi53454367/items/aee4cf0c346bc115be99
- 投稿日:2020-01-10T13:49:18+09:00
既存のRailsアプリをDockerコンテナで動かす方法+sequel proによるDBコンテナ可視化
はじめに
新規アプリをDocker環境で開発するやり方はたくさんあるけど、既存アプリをDocker環境で構築するやり方は全然見当たらず、わりと苦労しました。
振り返ってみると新規アプリでの構築の仕方とあまり変わらないはずなのですが、色々エラー出て苦労したのでまとめておきます。個々のコマンドの意味もできるだけ記載しました。
単なる環境構築だけでなく、DBコンテナのsequel proによる可視化やAPPコンテナでのbinding.pryの仕方、bundle install後にbuildし直さなくてもよい設定にする方法などもまとめました。同じく既存のRailsアプリをDocker上で構築したい人の参考になれば幸いです。
内容について間違っていたら教えていただけると嬉しいです。
開発環境
- Ruby: 2.5.1
- Rails: 5.2.4
- MySQL: 5.6
- MacOS
前提
- Docker for Macはインストール済み
- Dockerについての基礎知識
- 今回は開発環境のみ
対象読者
作成した既存のアプリをDocker上で構築したい人
目次
- コンテナ起動までの大まかな流れ
- 実際の作業
- 番外編:sequel proによるDBコンテナの可視化
- 開発する上でのDockerコマンド
- おまけ
- railsコンテナ上でbinding.pryをする方法
- railsコンテナ上でbundle installした時に、変更内容をコンテナ上に反映させる方法
コンテナ起動までの大まかな流れ
DockerfileにてRubyのベースイメージをもとにイメージを作成する
↓
作成したイメージをもとにdocker-compose.ymlでappコンテナを作成すると同時に、DBのコンテナのイメージを作成し、これらのコンテナを連携させる
↓
database.ymlを修正してappコンテナからdbコンテナへ接続できるように設定する実際の作業
- Dockerfileとdocker-compose.ymlの作成
- Dockerfileの記載
- docker-compose.ymlの記載
- database.ymlの変更
1. Dockerfileとdocker-compose.ymlの作成
まず、開発しているアプリで、Dockerfileとdocker-compose.ymlを以下のように作成します。
アプリ名 |- app |- bin |- config #略 |- vendor - .gitignore - config.rb - Dockerfile #追加 - docker-compose.yml #追加 - Gemfile - Gemfile.lock #略2. Dockerfileの記載
続いてDockerfileの中身を書いていきます。
myprojectのところはコンテナ起動の際に作成するディレクトリ名なので、何でも大丈夫です
ただし、それ以降の記述でも随時書き換えてくださいDockerfileFROM ruby:2.5.1 RUN apt-get update && \ apt-get install -y mysql-client nodejs vim --no-install-recommends && \ rm -rf /var/lib/apt/lists/* RUN mkdir /myproject WORKDIR /myproject ADD Gemfile /myproject/Gemfile ADD Gemfile.lock /myproject/Gemfile.lock RUN gem install bundler RUN bundle install ADD . /myproject詳細な説明は省きますが、ざっと説明すると
- ruby2.5.1をベースイメージとする
- コンテナ内で必要なコマンドをインストール
- myprojectというディレクトリを作成して基点にする
- Gemfileをコンテナ上にコピーした後、bundle install
- ローカルのディレクトリ、ファイルをコンテナ上にコピー
という感じかと思います。
3. docker-compose.ymlの記載
Rubyのコンテナは作成できるようになりました。
続いてこれをもとにアプリケーションのコンテナとデータベースのコンテナを作成し、それらのコンテナをリンクさせるための作業をしていきます。
docker-compose.ymlを以下のように記載します。
*mysqlは8.0以上だと認証方法が異なるようなので注意
https://qiita.com/yensaki/items/9e453b7320ca2d0461c7docker-compose.ymlversion: '2' services: db: image: mysql:5.6 volumes: - mysql-data:/var/lib/mysql #データの永続化のために必要 ports: - "4306:3306" #両方3306でもok。詳細は下の「番外編:DBをsequel proで可視化したい」へ app: tty: true #コンテナ上でbinding.pryするために必要 stdin_open: true #コンテナ上でbinding.pryするために必要 build: . command: bundle exec rails s -p 3000 -b '0.0.0.0' volumes: - .:/myproject #ローカルのディレクトリをコンテナ上にマウント - bundle:/usr/local/bundle #bundle installした後buildし直さなくてよくなる links: - db volumes: mysql-data: bundle: #bundle installした後buildし直さなくてよくなる要点は以下
- versionは2でも3でもいいと思いますが、使えるコマンドが違ってくるみたいです。
- servicesのところにdbとappがありますが、これらがそれぞれコンテナになります。
- appのコンテナのlinksにdbがあり、これによってappコンテナとdbコンテナが連携できるようになります
- 一番下のvolumesには永続化させたいデータを記載
- appコンテナ上でvolumes: - .:/myprojectとすることで、ローカルのディレクトリをマウントしている。
- dbコンテナ上でvolumes: -mysql-data:/var/lib/mysqlとすることでデータベースで変更されたデータを永続化。この記述がないと、コンテナを壊した時に変更したデータが消えてしまいます。
- bundle installとbinding.pryのための記述は本記事、最後のおまけを参照
4. database.ymlの変更
これまでの作業でappコンテナとdbコンテナを連携させる設定をしました。
最後にappコンテナからdbコンテナに接続するために、接続設定をします。
database.ymlの中身はおそらく初期設定のままだとこんな感じの記述になってるかと思います。config/database.yml変更前default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: socket: /tmp/mysql.sockこのままだとsocket通信でDB接続をするので、せっかく作成したdbコンテナが意味なくなってしまいます。
作成したdbコンテナに接続するために以下のように変更します。config/database.yml変更後default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password:password host: db #変更(docker-compose.ymlのservice名を記載)usernameやpasswordは環境変数で設定している記事もいくつか見ますが、開発環境なら気にしなくてもいいかなと思いました。
本番環境ではちゃんと環境変数を設定します。以上で準備ができました。
あとはターミナルで該当アプリのディレクトリまで移動して、docker-compose upと入力するだけです
(Gemfile.lockの中身を削除して空にしないとエラーになるかもしれません)
localhost:3000で接続確認して終了です!今回はMySQLを使いましたが、他のDBでも可能なはずです(未検証)
*ポート番号は変わるはずなのでご注意ください番外編:DBをsequel proで可視化したい
今まで開発していた時、DBをsequel proで可視化していて便利だったのですが、docker上のDBも可視化したい!
という方に向けて以下に方法を記載しますdocker-compose.ymlで以下のように記載していました
Ruby:docker-compose.yml
db:
ports:
- "4306:3306"
これはホストが4306で接続した時にコンテナ上では3306に置き換えますという意味です。
準備はこれでokなのでsequel proで接続します。
sequel proを開いて、標準タブに切り替えます。
そこで以下の内容を入力します名前: 任意、変えなくてもok ホスト: 127.0.0.1 ユーザー名: root (database.ymlに記載のユーザー名) パスワード: password (database.ymlにパスワード) データベース: 空でok ポート: 4306(docker-compose.ymlのportsに記載した左側)ホストの127.0.0.1は自分自身を表すIPアドレス
これで見れるようになるはず!
その他開発する上で必要なコマンド
docker上でrails g controllerやrails db:migrateなどを行う時は以下のようにコンテナを通して入力します
appの部分はdocker-compose.ymlで作成したコンテナ名のappのことですdocker-compose run --rm app 入力したいコマンド(例: rails db:migrate)その他必要なコマンドは以下の記事参照
https://qiita.com/gold-kou/items/44860fbda1a34a001fc1
全てのコンテナやイメージを削除する場合はこちら#全てのコンテナ停止 docker stop $(docker ps -q) #全てのコンテナ削除 docker rm $(docker ps -q -a) #全てのイメージ削除 docker rmi $(docker images -q)おまけ
bundle installしたい時
開発していく中で、gemを追加してbundle installしたい場面が出てくると思います
そのままやってもできるんですが、追加したgem内容がコンテナ内に反映されないため、イメージをbuildし直さなくてはなりません。
いくつか解決方法はあるみたいですが、今回はvolumeでマウントするという方法で解決しました。docker-compose.ymlapp: volumes: - bundle:/usr/local/bundle #中略 volumes: bundle:参考記事
https://qiita.com/neko-neko/items/abe912eba9c113fd527ebindin.pryしたい時
binding.pryをするためには以下の記述を追加します
docker-compose.ymlapp: tty: true stdin_open: trueおわりに
dockerについて全くわからないところから環境構築するのはかなり大変でした
自分なりにまとめられて良かった
同じような状況の人の参考になれば幸いです間違ってる場所があれば指摘していただけると幸いです
本番環境でDockerを使うのも苦労したので、そのうち開発環境との違いなどもまとめようかなと思います
参考記事
Dockerについての概要と色々なTIPSを知りたい場合は下記リンクがおすすめ
https://qiita.com/gold-kou/items/44860fbda1a34a001fc1実際の作業で参考にした記事
https://qiita.com/azul915/items/5b7063cbc80192343fc0
https://qiita.com/Nishi53454367/items/aee4cf0c346bc115be99
- 投稿日:2020-01-10T13:03:46+09:00
Rails Datetime型のカラムの値を日付(mm/dd/yyyy)単位で集計する方法
はじめに
gem chatkickを使用して、値を日付(mm/dd/yyyy)ごとにカウントしようとした際に、ハマってしまったのでその備忘録になります。
概要
やりたかったことは簡単グラフ表示 gem 'charkick'を使用して、テーブルの値を日付単位でカウントして表示したかった。
ちなみに、charkick の一次情報はこちら。
また、charkickを使っての日付単位での集計にはgem 'groupdate'を使うことがベストプラクティスらしい導入
以下の記事を参考にcharkickを導入しました
ー Railsメモ(29) : Chartkickで簡単にグラフを描画するここから本題
ぶつかった問題
エラー発生
ActiveRecord::Base.default_timezone must be :utc to use Groupdate
どうやらtimezoneがUTCでないといけないらしい。アプリのtimezoneをtokyoにしていたが、あくまでtokyoのままカウントしたい。
application.rbclass Application < Rails::Application ## 途中略 config.time_zone = 'Tokyo' end対応策
groupdateのメソッドgroup_by_day を使うとエラーの回避ができず、しょうがなくあきらめることに。
ちなみにエラーが出ていたコードは以下。
- 対応前
user_controller.rbclass UsersController < ApplicationController ## 途中略 def show @data2 = Message.where(user_id: current_user.id).group_by_day(:created_at).count ## 途中略 end end
- 対応後
user_controller.rbclass UsersController < ApplicationController ## 途中略 def show # messageテーブル値の取得 data2 = Message.where(user_id: current_user.id) # 配列の宣言 c = [] # data2の値を日付(mm/dd/yyyy)形式で取得し配列に格納 data2.each do |d| c << d.created_at.strftime('%Y%m%d').to_s end # 配列に含まれている重複している値を数える @data2 = c.each_with_object(Hash.new(0)){|v,o| o[v]+=1} @today = Date.today @lastmonth_today = @today.prev_day(14) end endもっといい書き方ができるかもしれませんが、とりあえずこれで日付単位でのカウントができた。
ちなみに表示側のHTML以下
show.html.haml.rb.learning-curve # 途中略 .learning-curve_graph = line_chart @data2, width: "100%", height: "300px",min: 0, max: 20, xmin: @lastmonth_today, xmax: @today,label: "Value",xtitle: "日付",ytitle: "登録件数"結果
無事に日付単位でのカウントに成功し、グラフの表示ができた。
- 投稿日:2020-01-10T12:14:49+09:00
【翻訳】Bundler 3アップグレードガイド
はじめに
Rubyのパッケージ管理ツールであるBundlerはバージョン 3で後方互換性が失われる様々な変更点が導入される予定になっています。
そして、バージョン 3への移行を容易にするため、バージョン 2.1ではバージョン 3で使えなくなる機能を使うと警告が出ます。これらの内容については公式リポジトリのアップグレードガイドで詳細が説明されています。
https://github.com/rubygems/bundler/blob/master/UPGRADING.md
この記事は上記のアップグレードガイドの日本語訳です。
翻訳したアップグレードガイドの版について
この記事で翻訳したのは2019年10月3日に更新された以下の版です。
(翻訳時点のBundlerの最新バージョンは2.1.4)https://github.com/rubygems/bundler/blob/e8f261882397f7e8a6c74760f8fe1a4955c9e980/UPGRADING.md
今後更新される可能性もあるため、必要に応じて最新の版を参照するようにしてください。
https://github.com/rubygems/bundler/blob/master/UPGRADING.md
おことわり
普段あまり使わない機能については、正しく翻訳できているかどうかあまり自信がありません。
もし明らかにおかしな内容を見つけたら、コメント欄や編集リクエスト等で報告してください?ラインセンスについて
この記事はBundlerと同様にMITライセンスで公開します。
This document is under the MIT License.
それでは以下がアップグレードガイドの翻訳になります。
【翻訳】アップグレードについて
Bundler 3について
以下で説明するのはBundler 3で導入される変更点のまとめです。なぜその変更を入れるのか、そして非推奨化のプロセスがどのようになるのかについても説明しています。非推奨になる変更点はBundler 2.1のリリース時にすべてデフォルトで表示されます。
もし非推奨警告の対応は後回しにして警告を非表示にしたい場合は、設定によってそのように変更できます。その場合は
BUNDLE_SILENCE_DEPRECATIONSという環境変数を"true"に設定するか、bundle configコマンドを使って設定してください。グローバルに設定を変更したい場合はbundle config set silence_deprecations trueコマンドを使い、ローカルの設定だけ変更したい場合はbundle config set --local silence_deprecations trueコマンドを使います。これ以降、本ドキュメントではこの3つの設定方法がすべて有効である前提で執筆していきますが、記述するのはbundle config set <option> <value>の方法だけにします。全般的な話として、これらの変更点は「新規Bundlerユーザー」のユーザー体験を改善するために行われます。ここでいう新規Bundlerユーザーとは、毎回特に決まった使い方をしておらず、これまでの使用体験から「Bundlerにはこう動いてほしい」という明確な意思も持っていないユーザーのことを指します。今回の変更点は長年Bundlerを使ってきたユーザーをイライラさせるものだと思います。その点は私たちも十分理解しています。ですので、私たちはこのプロセスをできる限りスムーズなものにしたつもりです。
非推奨化警告は、「CLIの非推奨警告」と「DSLの非推奨警告」と「その他の非推奨警告」という3つのグループに分けられます。それぞれについて今から見ていきましょう。
CLIの非推奨警告
CLIではBundlerを動かすためのコマンドとオプションの集合を定義しています。このコマンドとオプションの集合に対して、私たちはさまざまな変更を導入する予定です。
- 一度設定するとそれ以後の呼び出しでその設定が記憶される
bundle install用のフラグが非推奨となりました。具体的には、
bundle installに渡す--clean,--deployment,--frozen,--no-cache,--no-prune,--path,--shebang,--system,--without,--withオプションのことになります。記憶されるCLIオプションはこれまで混乱とバグレポートの温床になってきました。これは初心者に限った話ではなく、経験豊富なユーザーにとっても同様です。CLIツールは全く同じ呼び出し方なのに異なる振る舞いをするべきではありません。そうなってもよいのは、そのように明示的に設定されている場合「だけ」です。それが設定の存在理由であり、ユーザーが気づかないところでこっそりと設定が変更されるべきではありません。
この仕様変更によって影響を受けるのは、この機能に依存する「よくあるワークフロー」です。たとえば、本番環境で
bundle install --without development:testというコマンドを実行すると、このフラグがアプリケーションの設定ファイルに保存され、それ以降はbundleコマンドを実行してもdevelopmentとtestのgemをありがたく無視してくれます。Bundler 3以降はこの魔法が使えなくなります。同じ挙動を維持するためには明示的な設定が必要です。設定を入れる方法は、環境変数、アプリケーションの設定、マシン全体の設定のいずれかです。たとえば、bundle config set without development testというコマンドを使います。こうしたフラグの削除は類似のコマンドにも適用されます。たとえば、
bundle check --pathがそうです。
bundle installとbundle updateに渡す--forceフラグは、--redownloadにリネームされます。これは単なるフラグのリネームです。リネームするのは実際の挙動をよりわかりやすくするためです。このフラグは強制的にすべてのgemを再ダウンロード(redownload)するだけで、それ以外の何かを強制(force)するわけではありません。
bundle vizコマンドが削除され、プラグインとして切り出されます。これはBundlerで唯一外部依存が発生するコマンドです。依存先はOS(
graphvizパッケージ)とgem(ruby-graphvizgem)です。この依存性を排除することで開発がより容易になります。また、この機能はBundlerチームによって公式に保守されるBundlerプラグインになります。ユーザーもこのプラグインを参照すれば、独自のプラグインを作るのに役立ちます。このプラグインは従来のコアコマンドと同じコードになっていますが、唯一異なる点としてコマンド名がbundle graphに変わっています(でも、この方がずっとわかりやすいはずです)。この新しいプラグインのインストール方法や使い方についてはプラグインのリポジトリを参照してください。
bundle consoleコマンドが削除され、bin/consoleに置きかわります。時間の経過とともに
bundle consoleコマンドは保守するのが難しくなってきました。これは各ユーザーが自分独自の変更点を追加しようとしてくるためです。保守を容易にし、細々した議論を減らすため、bundle consoleコマンドをbin/consoleスクリプトに置き換えます。このスクリプトはgem開発時にbundle gemコマンドで作成され、ユーザーは自分が好きなようにこのコマンドを修正することができます。
bundle updateコマンドを実行しても、すべてのgemがアップデートされなくなります(--allオプションが必要になります)。Bundlerチームはこのコマンドを使ってすべてのgemを一気にアップデートするのは、このコマンドの主要なユースケースになるべきではないと判断しました。それよりも1回につき1つのgemをアップグレードする(または関連するgemのグループをアップグレードする)方がよいと考えます。すべてのgemを一気にアップグレードすることもできますが、その場合は
--allフラグが必要になります。
bundle installコマンドに--binstubsフラグを渡せなくなります。
--binstubsオプションはbundle installコマンドから削除され、bundle binstubsコマンドに置きかえられました。--binstubsオプションはプロジェクト内のgemに存在するすべての実行可能ファイル(executables)に対してbinスタブを作成します。しかし、これはほとんど役に立ちませんでした。なぜなら大半のユーザーが使うのはbinスタブの一部だけだからです。加えて、これを使うと、使われることのない大量のファイルがソース管理システムに追加されます。こうした理由により、binスタブは個別に作成され、個別にバージョン管理されるようになります。
bundle configコマンドに新しいサブコマンドが追加されました。従来のインターフェースは混乱の原因になっていたと思います。なぜなら様々なフラグと引数の組み合わせによって実行されるオペレーションを推測しなければならなかったからです。これに代わり、私たちは
list,get,set,unsetというサブコマンドを導入しました。このサブコマンドの導入により、configコマンドが以前よりもきっと扱いやすくなったはずです。従来のインターフェースは非推奨になりましたが、警告メッセージと一緒に新しいコマンドの提案を行います。
bundle injectコマンドが非推奨になり、bundle addコマンドに置きかえられました。新しいコマンド名の方がきっとユーザーのメンタルモデルにフィットし、より広いユースケースをサポートすると思います。
bundle injectコマンドでサポートされていたインターフェースはbundle addコマンドでもまったく同じように動作します。ですので、新しいコマンドに移行するのは簡単なはずです。ヘルパーの非推奨警告
- 右のメソッドは非推奨になりました。
Bundler.clean_env,Bundler.with_clean_env,Bundler.clean_system,Bundler.clean_execこれらのヘルパーメソッドはすべて、水面下で
Bundler.clean_envメソッドを使っています。これにより実行されるブロック内ではBundlerに関連する環境は削除されます。ユーザーからたくさんのレポートを受け取った結果、ユーザーは通常この挙動を望まず、代わりにカレントプロセスが開始する前と同じBundler環境を使いたがっていることに私たちは気づきました。こうして生まれたのが、
Bundler.with_original_env,Bundler.original_system,Bundler.original_execという3つのメソッドです。これらのメソッドはすべて新しいBundler.original_envメソッドを水面下で使います。とはいえ、特定の状況下では従来の
Bundler.clean_envの挙動が望ましいこともあるでしょう。たとえば、Railsジェネレータのテストをする場合は、bundlerのいない環境が欲しくなると思います。このため、私たちは従来の挙動を新しく、よりわかりやすい名前で維持できるようにしました。"clean"という単語はあまりにもあいまいだからです。というわけで、私たちはBundler.unbundled_env,Bundler.with_unbundled_env,Bundler.unbundled_system,Bundler.unbundled_execという4つのメソッドを新たに追加しています。
Bundler.environmentが非推奨になり、Bundler.loadに置き換えられました。どれくらいの人がこれを直接使っているのかはわかりませんが、私たちは
Bundler::Environmentクラスを削除しました。このクラスはBundler.environmentによってインスタンス化されるクラスです。削除した理由はBundler::Runtimeが同じ役割のクラスだと気づいたからです。移行期間中、Bundler.environmentはBundler.loadに委譲されます。このメソッドはBundler::Environmentへの参照を保持しています。DSLの非推奨警告
Bundler DSLで発生する下記の非推奨警告は、Bundler 3で予定されている厳格なソース固定(strict source pinning)への準備を意味します。Bundler 3ではすべての依存ライブラリのsourceは厳格に定義されます。
- 複数定義されたグローバルなGemfileのsourceはサポートされなくなります。
こんなふうに書く代わりに・・・
source "https://main_source" source "https://another_source" gem "dependency1" gem "dependency2"このように書いてください。
source "https://main_source" gem "dependency1" source "https://another_source" do gem "dependency2" end
- グローバルな
pathとgitのソース(source)はサポートされなくなります。こんなふうに書く代わりに・・・
path "/my/path/with/gems" git "https://my_git_repo_with_gems" gem "dependency1" gem "dependency2"このように書いてください。
gem "dependency1", path: "/my/path/with/gems" gem "dependency2", git: "https://my_git_repo_with_gems"もしくは各ソースに対して複数のgemがあり、もっとDRYに書きたい場合はブロック記法を使ってください。
path "/my/path/with/gems" do # gem "dependency1" # ... # gem "dependencyn" end git "https://my_git_repo_with_gems" do # gem "dependency1" # ... # gem "dependencyn" endその他の非推奨警告
vladとcapistrano用のデプロイヘルパー(Deployment helper)は削除されました。
vladツールは何年も活動が止まっているので自然に非推奨となりました。一方でcapistrano3はcapistrano-bundlergemによってBundlerに統合されています。Capistrano 3のユーザーはきっとこれを使っているはずです。なんらかの理由でCapistrano 2を使い続けている場合は、遠慮無くBundler 2のbundler/deployment.rbファイルからCapistrano用のタスクをコピーして、ご自身のアプリケーションに追加してください。基本的に私たちはあらゆるデプロイメントシステムの統合機能を保守したくありません。この機能を削除したのはこうした理由からです。
(翻訳は以上)
- 投稿日:2020-01-10T11:36:03+09:00
【Rails】config/localesに新規追加したファイルが読み込まれない【I18n】
はじめに
タイトルの通りです。
Railsで多言語化対応をする際にconfig/localesにymlファイルを追加していくと思いますが、それが読み込まれない場合の対応策です。結論
早速結論なのですが、超シンプルで、
Railsサーバを再起動するです。。。
既存ファイルに訳文を追加する場合はRailsサーバの再起動は不要ですが、新規ファイルを追加した場合はRailsサーバの再起動が必要です。ちなみに少しこの記事の内容と外れるかもしれませんが、RailsガイドにRailsの初期化プロセスについて記載があったので参考としてリンクを貼っておきます。
Railsの初期化プロセスおわりに
単純なミスで恥ずかしいのですが、めっちゃハマって辛かったので記事にしてみました。(typoやインデント、ディレクトリ構成等見直しまくりました。。。)
案外検索しても出てこず、他ごとしてたら急に「これ、サーバ再起動案件じゃね。。。?」と気が付いた次第です。はい。
- 投稿日:2020-01-10T10:49:13+09:00
#Rails + #rspec + #VCR で SECRET_KEY など秘密の環境変数・情報をフィルタリングする設定
例
spec/support/vcr_config.rbVCR.configure do |config| config.filter_sensitive_data('<SECRET_KEY>') { ENV['SOME_SECRET_KEY'] } endRef
VCR で外部 API へのリクエストをダンプするときに機密情報をマスクしたい - Qiita
https://qiita.com/gotchane/items/c2c29c0063bd44246510Original by Github issue
- 投稿日:2020-01-10T01:57:51+09:00
index.html+mobile.haml なviewをrspecする
before_action do request.variant = :mobile if ua.match(/Android/) endとかするとviewを切り替えられる
ActionPack Variantsという機能1で
view specを書く方法# spec/views/users/index.html+mobile.haml_spec.rb RSpec.describe 'users/index.html+mobile.haml', type: :view do it do render end endとやると 「
html+mobile何ていうフォーマットねーよ」 と怒られる
なのでrenderの呼び出しで正しい情報を与える# spec/views/users/index.html+mobile.haml_spec.rb RSpec.describe 'users/index.html+mobile.haml', type: :view do it do render template: 'users/index.html.haml', variants: 'mobile' end endこれで正しくmobileのテンプレートをレンダーできる
(確認は適当に= 1 / 0とか追記すれば死んでくれる)しかし当然めんどくさい
renderの中身を見ると_default_render_optionsというのをdescribeの文字列から作っているようだ
なのでこいつをいじってしまう# spec/support/variants_support.rb module VariantsSupport def _default_render_options render_options = super if render_options[:formats].first formats, variants = render_options[:formats].first.to_s.split('+') render_options[:formats] = [formats.to_sym] if formats render_options[:variants] = [variants.to_sym] if variants end render_options end end RSpec::Rails::ViewExampleGroup::ExampleMethods.prepend(VariantsSupport)これで
renderするだけでよくなったなおこの現象は
rspec 3.9までの挙動で現在ベータの4.0では修正されている
本家の修正も_default_render_optionsなので大正解っぽい
早くリリースしてくれ
名前初めて知った ↩
- 投稿日:2020-01-10T00:17:21+09:00
テーブルの結合ができない。。Mysql2::Error: Column 'updated_at' in order clause is ambiguous
はじめに
Railsでeager_loadしようとしたときに、下記のようなエラーが出て解消に時間がかかったのでまとめました。
Mysql2::Error: Column 'updated_at' in order clause is ambiguousやりたいこと
user の updated_at を指定した期間のものだけ抽出するために、テーブルの結合をしたい。
なぜeager_loadできないのか
これを実行すると、
user.rbUser.where("updated_at >= '2020/01/01' and updated_at <= '2020/01/31'").eager_load(:post)MySQLのエラーが出る。
Mysql2::Error: Column 'updated_at' in order clause is ambiguousなぜか。
updated_at は User にも Post にも存在しているため、どちらの updated_at なのか判別がつかない。
そこで、incidents.updated_at とすることで、User の updated_at なのか Post の updated_at なのかを明確化する。
下のように書き換えることで、エラーを解消することができました。
user.rb# incidents.updated_at とすることで、user User.where("incidents.updated_at >= '2020/01/01' and incidents.updated_at <= '2020/01/31'").eager_load(:post)まとめ
updated_at は、どのテーブルでも持っている値となるので、こういう値でjoinするときは気をつけるようにします。
裏話としては、railsでデバックをしていていたのですが、eager_loadを実行したらSQL文しか表示されずエラーが表示されていませんでした。
.to_sで文字列化することで、上記のMysql2::Errorが出てなんとかデバックすることができました。
エラーが隠れてる??自分の確認方法が悪かっただけかもしれませんが、、
時間はかかりましたが、解決できてよかったです。参考
- includes,joins,eager_load,preloadの違いを噛み砕いて説明する
- Mysql2::Error: Column 'created_at' in order clause is ambiguousの対策












