20200110のRailsに関する記事は29件です。

Dockerで環境構築する際、ソースコードの変更が即時反映されない場合の対処法

記事の目的

docker-composeで環境構築する際に起こったRailsのソースコードを修正しても即時反映されない問題に対しての対処法を共有すること。

対処法

1.development.rbを編集

開発環境用の設定ファイルである config/environments/development.rb を編集。
「 ~ 」は元々の記述を省略しているものとする。
この中にconfig.reload_classes_only_on_change = falseという記述を追加する。

development.rb
Rails.application.configure do

~
config.reload_classes_only_on_change = false

end

2.railsコンテナを再起動

設定を記載しただけでは反映されないので、最後にRailsを再起動する。

ターミナル上で、

$ docker-compose restart 

と入力する。

以上でソースコードの変更が即時反映される。

終わりに

Dockerで環境構築をした際に、もう設定しておくと良い。

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

#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
end
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
end
dupulicated_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
end
dupulicated_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

https://github.com/YumaInaura/YumaInaura/issues/2929

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

#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
end

I 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
end
dupulicated_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
end
dupulicated_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

https://github.com/YumaInaura/YumaInaura/issues/2930

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

【第8章】Railsチュートリアル 5.1(第4版) 基本的なログイン機構

大まかな流れの自己整理が目的(コード外文章中のSession(s)の表記などはスルー状態)のため、内容そのものに不足・誤り等あれば追記&訂正していきますのでご指摘頂けますと幸いです:bow_tone1:
なお、筆者は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.rb
get    '/login',   to: 'sessions#new'
post   '/login',   to: 'sessions#create'
delete '/logout',  to: 'sessions#destroy'

サーバ起動して、「/login」にアクセスするとnewアクションのview画面が出る。

スクリーンショット 2020-01-05 14.35.59.png

Sessionsコントローラのテストで名前付きルートを使うようにする( 「get sessions_new_url」 → 「get login_path」へ変更)。テストしてGREEN。

sessions_controller_test.rb
require '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.rb
  def new
    @user = User.new
    # => form_for @user
  end

ただSessionではモデル作ってないからできない。
POSTリクエストを「/login」に送ってcreateアクションが動くメソッドが欲しいので、sessionsのコントローラ、ビュー画面に下記を追加する。

sessions_controller.rb
class SessionsController < ApplicationController
  # GET /login
  def new
    # POST /login => create action
  end

  # POST /login
  def create
  end
end

 
なお、form_forの引数には@userを入れなくてもシンボル(:session)で対応可能であり、引数にurlを加えればok

app/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メソッド

 
スクリーンショット 2020-01-05 16.56.52.png

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発生(メソッドが見つからない)

スクリーンショット 2020-01-05 17.51.08.png

ここで真っ先に思いつく(筆者のかろじての発想)のが「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の表示時間が長い(思ったより生き延びている..?)問題がある。

スクリーンショット 2020-01-05 20.23.46.png

原因として、flashの生存期間としては、次のリクエストが来るまで
前回がredirectに対して今回はrender(リクエストを発行するのでなく、「このテンプレートを描画してね」)、つまり、失敗してflash登場後に次のリクエストが来るまで残り続ける
①表示(0リウエスト目)、②リロード(1リクエスト目)、③リロード(2リクエスト目)

すぐに直さず回帰バグを防ぐため、統合テストを行う。

$ rails generate integration_test users_login
users_login_test.rb
require '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.rb
module 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.rb
  def 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メソッドが見つからない。

スクリーンショット 2020-01-09 17.34.41.png

ヘルパーはもともとviewで使われる。ただ、今回はコントローラでヘルパーを使いたいので、module(ヘルパー側)と対応するincludeを追記する。

sessions_controller.rb
class SessionsController < ApplicationController
  #moduleと対応させる
  include SessionsHelper

# 省略

  
Bootstrapの機能を使うため、jsに追加する。

app/assets/javascripts/application.js
//= require jquery
//= require bootstrap

もう一度リロードするとログイン状態になり、右上の項目が変わる(プルダウンの起動もok)。

スクリーンショット 2020-01-09 18.06.04.png

レイアウトの変更をテストする

fixture(テストを実行、成功させるための状態や前提条件の集合)をYAMLで用意する。

YAMLファイル

YAMLファイルとは、
構造化されたデータを表現するためのデータ形式のひとつ(HTML,XMLなどいろんなファイルの書き方のひとつ)。
配列(先頭に「-」)、ハッシュ(キー:値)、スカラー(文字列、数値、真偽知など)で構成される。

< 主な用途 >
・各種設定ファイル
・データ保存用 (シリアライゼーション)
・データ交換用フォーマット
・ログファイル

<参考>
YAMLとは|「分かりそう」で「分からない」でも「分かった」気に ...
プログラマーのための YAML 入門 (初級編)
【Ruby入門】YAMLの使い方をわかりやすく解説!

 
name,email,password_digest(あとでBCryptでハッシュ化)を含むYAMLを作成する。ラベルはmichael

test/fixtures/users.yml
michael:
  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.rb
class 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.rb
class 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.rb
class 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

スクリーンショット 2020-01-10 19.45.38.png

「Log out」クリックして、

スクリーンショット 2020-01-10 19.49.37.png

無事ホーム(root)に戻ったのでok!

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

[Rails]form_withでutf8=✓を消す方法

調べても中々なかったのでメモ

form_withでutf8=✓を消す方法

= form_with(url: hoge_path, method: :get, local: true, skip_enforcing_utf8: true)

skip_enforcing_utf8: trueを指定する。

form_for / form_tagだと

enforce_utf8: false

参考情報

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

[Rails]Deviseの有効期限設定する方法

最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!

URLの有効期限

設定ファイル config/initializers/devise.rb を編集

config/initializers/devise.rb
Devise.setup do |config|
  # 省略
  config.confirm_within = 1.minutes
  # 省略
end

この記述内容はアカウントを登録してから
・1分以内なら確認化
・1分以降なら確認不可
になる設定ができる!

参考記事

Deviseの有効期限設定をテストする
【Rails】deviseでURL認証付きのメールを送信してみる

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

#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
# => nil

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2927

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

#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
# => nil

Pattern2 - 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
# => nil

Pattern2 - 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
# => nil

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2928

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

【rails】Ajaxを使ったフォローボタンの実装で躓いたところ

はじめに

初学者がポートフォリオ作成の際に躓いたことをメモします。
今回は躓いた部分だけ記載します。全体を知りたい方は下記のリンクがわかりやすいです。
https://railstutorial.jp/chapters/following_users?version=4.2#sec-a_working_follow_button_the_standard_way
https://qiita.com/Kaisyou/items/e918b77465e3f55c97a2

Ajaxとは

『Ajaxとは、あるWebページを表示した状態のまま、別のページや再読込などを伴わずにWebサーバ側と通信を行い、動的に表示内容を変更する手法。ページ上でプログラムを実行できるプログラミング言語JavaScriptの拡張機能を用いる。』

簡単に言うと、『javascriptによって必要な部分だけを更新する技術。その為ページ全体を再読み込みする必要がなく、素早いレスポンスが可能になる』と言ったところでしょうか?

前提

今回はユーザー一覧画面(users/index)でフォローボタンを表示します。

users_controller.rb
def index
    @q = User.ransack(params[:q])
    @users = @q.result(distinct: true)
  end
users/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: object
users/_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.rb
class 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.rb
class 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

end

remote: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に置き換えている。また@userusers/unfollow内でobjectとして参照している。

最後に

誰かのお役にたてれば幸いです。
間違えている部分があればコメントお願いいたします。

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

アイデアは早めに形にした方がいいぞ!2020

アイデアは早めに形にした方がいいぞ!2020

似たような内容の記事は探せば5万と出てくると思います。
私もインターネットをクロールしているときに見たことがある気がします。

ですがそういった歴史上の出来事がすっかり頭から抜け落ちていた私は、どちらかといえば経験から学ぶタイプ、つまり愚者側の人間だったためこのような事態になってしまいました。

今後このようなことがないよう戒めのために記録を残しておくことにします。
読み物と思って見ていただければ感激です。

:bookmark_tabs: TL;DR

アイデアは早い段階で形にしたほうがいいぞ!

:house: 前書き

令和時代になった今、AWSをまともに触っていないのは自分だけなのではないかと思い、何かしらのWebアプリケーションをデプロイしてみようと思った9月の秋が始まりである。

:wrench:何を作った?

動画の切り抜きをシェアするWebアプリを開発した。
個人的にめちゃくちゃ欲しかった。

スクリーンショット 2020-01-08 13.16.11.png

https://cutters.work/home

隙間時間を有効に
CUTTERSは、動画や生放送を見たいが時間がない人のためのツールです。
他人が投稿したおすすめの動画ポイントをシェアして、盛り上がりましょう。

仕組みとしては単純で、youtubeの動画はパラメータを指定してやることで一定の範囲内を再生することができるため、これを利用したものになる。

入力値として動画のURL切り抜きの開始時間切り抜きの終了時間を指定してやることで下記のようなクエリを作成する。

切り抜きのiframe要素
<iframe 
  src="https://www.youtube.com/embed<%= @uri.path %>?start=<%= @cutter.start_time %>&end=<%= @cutter.end_time %>" 
>
</iframe>

これをiframeなどの要素で表示することで切り抜きを簡単に投稿、シェアすることができるというサービスである。

スクリーンショット 2020-01-08 14.51.56.png

今回はAWSに慣れるということに重きを置いていたので、Webアプリケーションを楽にデプロイできるという認識のもとElastic BeanstalkというAWSのサービスを利用することにした。

また、慣れているという理由からRuby on Railsを使用して開発をすることに決めた。
仕組み自体は難しいものでもなく、EC2を利用したことはある

短い時間で公開ができそうだな。

:computer: 開発環境

  • Mac book pro
  • Runy on Rails 6.0.2
  • ruby 2.6.3
  • Docker

:poop: 問題が発生する

だが、アプリの公開は順調に進まなかった(今では後悔ばかりだ)

問題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デザイン初学者の私を過去へ置き去りにした:walking_tone3:

AWSを学ぶことに重きを置いていたとはいえ先をこされてしまったのは純粋に悔しかった。
主機能さえ作ってしまえば細かいところはあとで足していけば良かったのである。

物づくりのモチベーションが落ちるのは、自分の作っているものに価値がない時がついたときだと知った。

:bow_tone1: 結論

主機能が出来次第デプロイし、徐々に改善していくのがいいかも

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

アイデアは早めに形にした方がいいぞ!2020

アイデアは早めに形にした方がいいぞ!2020

似たような内容の記事は探せば5万と出てくると思います。
私もインターネットをクロールしているときに見たことがある気がします。

ですがそういった歴史上の出来事がすっかり頭から抜け落ちていた私は、どちらかといえば経験から学ぶタイプ、つまり愚者側の人間だったためこのような事態になってしまいました。

今後このようなことがないよう戒めのために記録を残しておくことにします。
読み物と思って見ていただければ感激です。

:bookmark_tabs: TL;DR

アイデアは早い段階で形にしたほうがいいぞ!

:house: 前書き

令和時代になった今、AWSをまともに触っていないのは自分だけなのではないかと思い、何かしらのWebアプリケーションをデプロイしてみようと思った9月の秋が始まりである。

:wrench:何を作った?

動画の切り抜きをシェアするWebアプリを開発した。
個人的にめちゃくちゃ欲しかった。

スクリーンショット 2020-01-08 13.16.11.png

https://cutters.work/home

隙間時間を有効に
CUTTERSは、動画や生放送を見たいが時間がない人のためのツールです。
他人が投稿したおすすめの動画ポイントをシェアして、盛り上がりましょう。

仕組みとしては単純で、youtubeの動画はパラメータを指定してやることで一定の範囲内を再生することができるため、これを利用したものになる。

入力値として動画のURL切り抜きの開始時間切り抜きの終了時間を指定してやることで下記のようなクエリを作成する。

切り抜きのiframe要素
<iframe 
  src="https://www.youtube.com/embed<%= @uri.path %>?start=<%= @cutter.start_time %>&end=<%= @cutter.end_time %>" 
>
</iframe>

これをiframeなどの要素で表示することで切り抜きを簡単に投稿、シェアすることができるというサービスである。

スクリーンショット 2020-01-08 14.51.56.png

今回はAWSに慣れるということに重きを置いていたので、Webアプリケーションを楽にデプロイできるという認識のもとElastic BeanstalkというAWSのサービスを利用することにした。

また、慣れているという理由からRuby on Railsを使用して開発をすることに決めた。
仕組み自体は難しいものでもなく、EC2を利用したことはある

短い時間で公開ができそうだな。

:computer: 開発環境

  • Mac book pro
  • Runy on Rails 6.0.2
  • ruby 2.6.3
  • Docker

:poop: 問題が発生する

だが、アプリの公開は順調に進まなかった(今では後悔ばかりだ)

問題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デザイン初学者の私を過去へ置き去りにした:walking_tone3:

AWSを学ぶことに重きを置いていたとはいえ先をこされてしまったのは純粋に悔しかった。
主機能さえ作ってしまえば細かいところはあとで足していけば良かったのである。

物づくりのモチベーションが落ちるのは、自分の作っているものに価値がない時がついたときだと知った。

:bow_tone1: 結論

主機能が出来次第デプロイし、徐々に改善していくのがいいかも

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

【Rails】アカウント有効化

  • あらかじめDBには有効化されていない状態のレコードがある
  • 顧客ID、メールアドレス、電話番号を入力すると有効化メールが送信される
  • リンクを踏んで有効化、ユーザー情報更新、ログイン

という流れを実装したのでまとめます。

前提

  • customersテーブルにはactivation_digestactivated_atの2つのカラムが必要(無い場合は追加)

Model

  • attr_accessoractivation_token属性を有効化
  • トークンとダイジェストを生成、代入するメソッドを作成
  • 引数にトークンを取って、ダイジェストと比較するメソッドを作成
app/models/customer.rb
class 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
end

Mailer

インスタンス変数を引数にとり、メールアドレスやトークンを抽出して送信します。
なお、本番環境のメール送信部分は未完成です。サーバーのログからメールの文面を確認してます。
【Rails】メールを送信する(ローカル)

app/mailers/customer_mailer.rb
class CustomerMailer < ApplicationMailer
  def account_activation(customer)
    @customer = customer
    mail to: customer.email, subject: 'アカウント有効化'
  end
end
app/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.rb
class 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
end

View

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.rb
resources :account_activations, only: %i[new create edit update]
get '/account_activations/sent', to: 'account_activations#sent'

問題点

Modelでパスワードに入れる値に制限をかけたら、editアクションで@customerを取得することができなくなってしまったので、仕方なくコントローラーでバリデーションの処理を実装しました。

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

【Rails】アカウントをアクティベート

  • あらかじめDBには有効化されていない状態のレコードがある
  • 顧客ID、メールアドレス、電話番号を入力すると有効化メールが送信される
  • リンクを踏んで有効化、ユーザー情報更新、ログイン

という流れを実装したのでまとめます。

前提

  • customersテーブルにはactivation_digestactivated_atの2つのカラムが必要(無い場合は追加)

Model

  • attr_accessoractivation_token属性を有効化
  • トークンとダイジェストを生成、代入するメソッドを作成
  • 引数にトークンを取って、ダイジェストと比較するメソッドを作成
app/models/customer.rb
class 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
end

Mailer

インスタンス変数を引数にとり、メールアドレスやトークンを抽出して送信します。
なお、本番環境のメール送信部分は未完成です。サーバーのログからメールの文面を確認してます。
【Rails】メールを送信する(ローカル)

app/mailers/customer_mailer.rb
class CustomerMailer < ApplicationMailer
  def account_activation(customer)
    @customer = customer
    mail to: customer.email, subject: 'アカウント有効化'
  end
end
app/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.rb
class 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
end

View

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.rb
resources :account_activations, only: %i[new create edit update]
get '/account_activations/sent', to: 'account_activations#sent'

問題点

Modelでパスワードに入れる値に制限をかけたら、editアクションで@customerを取得することができなくなってしまったので、仕方なくコントローラーでバリデーションの処理を実装しました。

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

bundle installするときに "can't find gem bundler (>= 0.a) with executable bundler (Gem::GemNotFoundException)" とErrorが出る

問題が起きたらググれって偉い人が言ってた

対象のディレクトリ下でbundle installするときに起った現象のメモ
※初学者なので細かめに記載

git cloneしたアプリのgemを入れようとしたら以下のエラーが出た。

console
Traceback (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 -v
Gemfile.lockにある 'BUNDLED WITH' をbunderのversionに対応させるの方法
bundler
$ cd ~/[対象のディレクトリ]
bundler
$ vim Gemfile.lock

ファイルの中身が見れるようになるので、
G(Shift+g)で最終行へ
x.x.xを対応のversionに書き換えます。

Gemfile.lock
BUNDLED WITH
   x.x.x

書き換え方
i → インサートモードへ(書き換えができるようになる)
esc → インサートモードの終了
:wq!を入力 → 保存して強制終了

これでbundle -v したり bundle install して、
問題なければヨシッ!

まとめ

いろんな記事を読んで模索した結果なので、これが最善の解説策かはわかりません。
なのでこれだけでなくいろんな記事を読んで、
ここは一つの参考にしていただければと思います。

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

モデルのテストの記述方法を細かくみてみた

下記のようなテスト文を、後からわかりやすいように、細かくみてみた。

spec/models.rb/message_spec.rb
require '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.rb
require '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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでカンマ区切り+小数点以下表示で数値を出力する方法

目的

整数部をカンマ区切りで表示し、小数点以下を表示したい

コード

number_with_precision(123456789, precision: 2, delimiter: ',')
#=> "123,456,789.00"

参考リンク

ruby-on-rails — 大きな数字にカンマを追加するためのRailsトリックはありますか?
ActionView::Helpers::NumberHelper

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

【Rails】has_secure_passwordメソッド

has_secure_passwordメソッドについてのメモです。

準備

usersテーブルにpassword_digestカラムを追加しておく。

bcryptをインストールする。

Gemfile
gem 'bcrypt'
ターミナル
$ bundle install

モデルにhas_secure_passwordを記載する。

app/models/user.rb
class User < ApplicationRecord
  has_secure_password
end

機能

  • 暗号化されたパスワードがpassword_digestカラムに保存される。
  • 仮想属性password_confirmationを使用して、入力ミスを減らすためにパスワードの確認入力をさせることができる。
  • authenticateメソッドを使用して、メールアドレスとパスワードに基いてユーザー認証を行うことができる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリケーションのメモリが膨れ上がる問題

次のグラフは、Herokuで運用している、とあるRailsアプリケーションのMemory Usageです。1日1回再起動がかかったあと、スワップが発生するまでメモリ使用量が増え続けます。特に大きなデータをメモリ上に置き続けているわけではありません。

memory-2.png

Passengerの作者Hongli Lai氏が、Rubyのメモリが膨れ上がる問題について研究して記事を書いています(2019年3月)。

日本語での概略は、次の記事の真ん中あたりで読めます。

Hongli Lai氏が見つけたのは、Rubyはメモリを正しく解放しているが、Cのライブラリ(glibc)のmalloc周りがなかなかメモリを回収してくれない、ということです。メモリ使用量よりパフォーマンスを優先しているせい、との見立てです。

メモリ使用量が膨れ上がるのを防ぐ方法は、3つあります。1と2は以前から知られた方法で、3はHongli Lai氏が発見した方法です。Hongli Lai氏の説が正しければ、3が本質的な解決方法ということになります。

  1. mallocの代わりにjemallocを使ってRubyをコンパイルする。
  2. 環境変数 MALLOC_ARENA_MAX=2 を指定する。
  3. ガベージコレクションの後でmalloc_trimを呼ぶ。

Hongli Lai氏は、この件について一緒に研究してくれるよう呼びかけています。

RubyのBug trackerでも報告されていますが、話は進んでいないもよう。

Rubyに上記の3のパッチを当てるプロジェクトが公開されています。残念ながらHeroku用は開発が進んでいないようです。

Herokuでは、上記の2の環境変数MALLOC_ARENA_MAX=2はデフォルトになりました。ただし、2019年9月24日より前に作られたアプリケーションでは自分で設定する必要があります。

MALLOC_ARENA_MAXを指定すると、レスポンスタイムが若干増えるという実験結果があります。

以上です。私自身は上記1-3のどれも実際には試していません、すいません。?‍♂️

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

rails usersテーブルの値をpasswordなしで更新する

はじめに

Userテーブルのカラムをpasswordなしで更新しようと思ったときに、更新ができずに手間取ってしまったのでその備忘録として記載する。

概要

usersテーブルにprofileというカラムを持たせていたのだが、ユーザー登録後にユーザーページからユーザー情報を更新したい。

ぶつかった課題

controllerで以下のように定義して実行した際にRollbackが起こり、値が更新されなかった。

mypage_controller.rb
   class 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.rb
   class User < ApplicationRecord
    # 途中略
     validates :password, length: { minimum: 6 }
     validates :password_confirmation, presence: true
     validates :password, presence: true, unless: :uid?
   end
  • 変更後

passwordのバリデーションに対して、on create を追加することで、 userの新規登録するときにのみバリデーションによる入力チェックがかかるように変更した。コード内容は以下

user.rb
   class 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 >を指定すればよいらしい。
ちなみに参考資料はこちら

もっと勉強しないとなぁ〜

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

【Rails】redirect_toで変数を渡す

redirect_toでページ遷移する先に、変数を引き渡す方法がわかったのでメモ。

送る側

redirect_to hoge_url(user_id: @customer.id)

受け取る側

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

Railsで開発した個人アプリにDockerを導入する手順と最低限の知識

Railsで開発した個人アプリに、後からDockerを導入しました。
その時知識不足なせいでハマってしまった場面があったので、自分の中で整理してアウトプットすることを目的にこの記事を書き残します。

ちなみに、以下の記事を主に参考にさせていただきました。
DockerをMacにインストールする
Docker + Rails + Puma + Nginx + MySQL

Railsアプリに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に記述する(アプリケーションフォルダ直下)

Dockerfile
FROM 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.yml
version: '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.env
MYSQL_ROOT_PASSWORD=password
MYSQL_USER=user
MYSQL_PASSWORD=password

⑦database.ymlを編集

config/database.yml
default: &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/Dockerfile
FROM 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.conf
upstream 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.rb
app_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 build

railsのコンテナを作成し、データベースの作成処理を行う

ターミナル
$ 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 のバージョン情報を表示
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存の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コンテナへ接続できるように設定する

実際の作業

  1. Dockerfileとdocker-compose.ymlの作成
  2. Dockerfileの記載
  3. docker-compose.ymlの記載
  4. 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のところはコンテナ起動の際に作成するディレクトリ名なので、何でも大丈夫です
ただし、それ以降の記述でも随時書き換えてください

Dockerfile
FROM 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/9e453b7320ca2d0461c7

docker-compose.yml
version: '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
ポート: 4306docker-compose.ymlportsに記載した左側)

ホストの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.yml
app:
  volumes:
    - bundle:/usr/local/bundle
  
 #中略

volumes:
  bundle:

参考記事
https://qiita.com/neko-neko/items/abe912eba9c113fd527e

bindin.pryしたい時

binding.pryをするためには以下の記述を追加します

docker-compose.yml
app:
  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

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

既存の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コンテナへ接続できるように設定する

実際の作業

  1. Dockerfileとdocker-compose.ymlの作成
  2. Dockerfileの記載
  3. docker-compose.ymlの記載
  4. 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のところはコンテナ起動の際に作成するディレクトリ名なので、何でも大丈夫です
ただし、それ以降の記述でも随時書き換えてください

Dockerfile
FROM 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/9e453b7320ca2d0461c7

docker-compose.yml
version: '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.yml
app:
  volumes:
    - bundle:/usr/local/bundle

   #中略

volumes:
  bundle:

参考記事
https://qiita.com/neko-neko/items/abe912eba9c113fd527e

bindin.pryしたい時

binding.pryをするためには以下の記述を追加します

docker-compose.yml
app:
  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

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

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
スクリーンショット 2020-01-10 11 57 39

どうやらtimezoneがUTCでないといけないらしい。アプリのtimezoneをtokyoにしていたが、あくまでtokyoのままカウントしたい。

application.rb
  class Application < Rails::Application
   ## 途中略
   config.time_zone = 'Tokyo'
  end

対応策

groupdateのメソッドgroup_by_day を使うとエラーの回避ができず、しょうがなくあきらめることに。
ちなみにエラーが出ていたコードは以下。

  • 対応前
user_controller.rb
  class UsersController < ApplicationController
   ## 途中略
    def show

      @data2 = Message.where(user_id: current_user.id).group_by_day(:created_at).count
      ## 途中略
     end
  end
  • 対応後
user_controller.rb
  class 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-10 12 53 15

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

【翻訳】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 installbundle updateに渡す--forceフラグは、--redownloadにリネームされます。

これは単なるフラグのリネームです。リネームするのは実際の挙動をよりわかりやすくするためです。このフラグは強制的にすべてのgemを再ダウンロード(redownload)するだけで、それ以外の何かを強制(force)するわけではありません。

  • bundle vizコマンドが削除され、プラグインとして切り出されます。

これはBundlerで唯一外部依存が発生するコマンドです。依存先はOS(graphvizパッケージ)とgem(ruby-graphviz gem)です。この依存性を排除することで開発がより容易になります。また、この機能は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.environmentBundler.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
  • グローバルなpathgitのソース(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

その他の非推奨警告

  • vladcapistrano用のデプロイヘルパー(Deployment helper)は削除されました。

vladツールは何年も活動が止まっているので自然に非推奨となりました。一方でcapistrano 3はcapistrano-bundler gemによってBundlerに統合されています。Capistrano 3のユーザーはきっとこれを使っているはずです。なんらかの理由でCapistrano 2を使い続けている場合は、遠慮無くBundler 2のbundler/deployment.rbファイルからCapistrano用のタスクをコピーして、ご自身のアプリケーションに追加してください。

基本的に私たちはあらゆるデプロイメントシステムの統合機能を保守したくありません。この機能を削除したのはこうした理由からです。

(翻訳は以上)

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

【Rails】config/localesに新規追加したファイルが読み込まれない【I18n】

はじめに

タイトルの通りです。
Railsで多言語化対応をする際にconfig/localesにymlファイルを追加していくと思いますが、それが読み込まれない場合の対応策です。

結論

早速結論なのですが、超シンプルで、Railsサーバを再起動するです。。。
既存ファイルに訳文を追加する場合はRailsサーバの再起動は不要ですが、新規ファイルを追加した場合はRailsサーバの再起動が必要です。

ちなみに少しこの記事の内容と外れるかもしれませんが、RailsガイドにRailsの初期化プロセスについて記載があったので参考としてリンクを貼っておきます。
Railsの初期化プロセス

おわりに

単純なミスで恥ずかしいのですが、めっちゃハマって辛かったので記事にしてみました。(typoやインデント、ディレクトリ構成等見直しまくりました。。。)
案外検索しても出てこず、他ごとしてたら急に「これ、サーバ再起動案件じゃね。。。?」と気が付いた次第です。はい。

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

#Rails + #rspec + #VCR で SECRET_KEY など秘密の環境変数・情報をフィルタリングする設定

spec/support/vcr_config.rb

VCR.configure do |config|
  config.filter_sensitive_data('<SECRET_KEY>') { ENV['SOME_SECRET_KEY'] }
end

Ref

VCR で外部 API へのリクエストをダンプするときに機密情報をマスクしたい - Qiita
https://qiita.com/gotchane/items/c2c29c0063bd44246510

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2926

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

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 なので大正解っぽい
早くリリースしてくれ


  1. 名前初めて知った 

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

テーブルの結合ができない。。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.rb
User.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が出てなんとかデバックすることができました。
エラーが隠れてる??自分の確認方法が悪かっただけかもしれませんが、、
時間はかかりましたが、解決できてよかったです。

参考

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