20190816のRubyに関する記事は19件です。

RSpec でインスタンスメソッドがスタブできないときはインスタンスをモックする必要があるよという話

はじめに

下記のようなコードを書いたとします。

instance_stub.rb
class Human
  def meet
    greeting = Greeting.new
    greeting.hello
  end
end

class Greeting
  def hello
    'Hello!'
  end
end

上記のコードに対して、Human#meet をテストしたいのですが、Greeting#hello は実際には呼び出したくなかったとします。

そのため、Greeting#hello をスタブする以下のようなテストコードを書いたとします。わかりやすいように、スタブした際の返り値を変更して、返り値が正しく変更されているかどうかを検証しています。

spec/instance_stub_spec.rb
require_relative '../instance_stub'

describe Human do
  context 'meet' do
    it 'returns a stub message instead of a real message' do
      greeting = Greeting.new

      allow(greeting).to receive(:hello).and_return('Hello from stub!')

      expect(Human.new.meet).to eq('Hello from stub!')
    end
  end
end

このコードは正しく動作しません。以下のようなエラーが発生します。

Failures:

  1) Human meet returns a stub message instead of a real message
     Failure/Error: expect(Human.new.meet).to eq('Hello from stub!')

       expected: "Hello from stub!"
            got: "Hello!"

       (compared using ==)
     # ./spec/instance_stub_spec_miss.rb:10:in `block (3 levels) in <top (required)>'

テスト中は Greeting#hello を呼び出したくないのでスタブしようとしていたのに、スタブできていません (Greeting#hello が実際に実行されてしまっています)。

この原因と解決法について説明します。

TL;DR

テストコード内で生成したインスタンスと実際のコード内で生成したインスタンスは別のオブジェクトであるため、スタブしたいインスタンスメソッドを持つクラスのインスタンスをテストコード内で生成しても正しくスタブすることはできない。

正しくスタブするためには、new をスタブする必要がある。

spec/instance_stub_spec.rb
require_relative '../instance_stub'

describe Human do
  context 'meet' do
    it 'returns a stub message instead of a real message' do
-      greeting = Greeting.new
+      greeting_mock = instance_double(Greeting)

-      allow(greeting).to receive(:hello).and_return('Hello from stub!')
+      allow(Greeting).to receive(:new).and_return(greeting_mock)
+      allow(greeting_mock).to receive(:hello).and_return('Hello from stub!')

      expect(Human.new.meet).to eq('Hello from stub!')
    end
  end
end

原因

まず原因に関してですが、行番号を付与して先ほどのテストコードを再掲します。

spec/instance_stub_spec.rb
 1  require_relative '../instance_stub'
 2
 3  describe Human do
 4    context 'meet' do
 5      it 'returns a stub message instead of a real message' do
 6        greeting = Greeting.new
 7
 8        allow(greeting).to receive(:hello).and_return('Hello from stub!')
 9
10        expect(Human.new.meet).to eq('Hello from stub!')
11      end
12    end
13  end

6 行目で Greeting クラスのインスタンスを生成しています。8 行目で Greeting インスタンスの hello メソッドをスタブして 'Hello from stub!' を返すようにしています。

一見するとこれで Greeting#hello がスタブされるように見えますが、先ほど紹介したように、これだと正しくスタブされていません。

理由は、このテストコード内 (spec/instance_stub_spec.rb) で生成した Greeting インスタンスと、実際のコード内 (instance_stub.rb) で生成した Greeting インスタンスは、どちらも Greeting インスタンスですが、オブジェクトが別だからです。

これを説明するには、以下のプログラムを IRB で実行するとわかりやすいでしょう。

irb(main):001:0> class Foo; end
=> nil
irb(main):002:0> Foo.new == Foo.new
=> false
irb(main):003:0> Foo.new
=> #<Foo:0x00007faa9aafee88>
irb(main):004:0> Foo.new
=> #<Foo:0x00007faa99b1fa58>

中身が空の Foo クラスを作って、2 つの Foo インスタンスを比較していますが、false が返ってきます。

もう少し考察してみましょう。Foo インスタンスを生成した際の返り値を見てみると、インスタンスを生成するたびにオブジェクトの ID が変わっていることがわかります (ちなみに、オブジェクトの ID は Foo.new.object_id で取得することができます)。

つまり、全く同じ Foo.new を実行しても、実行するたびに別のオブジェクトが生成されるため、一致しないのです。

実際のコードとテストコードを再掲します。

instance_stub.rb
 1  class Human
 2    def meet
 3      greeting = Greeting.new
 4      greeting.hello
 5    end
 6  end
 7
 8  class Greeting
 9    def hello
10      'Hello!'
11    end
12  end
spec/instance_stub_spec.rb
 1  require_relative '../instance_stub'
 2
 3  describe Human do
 4    context 'meet' do
 5      it 'returns a stub message instead of a real message' do
 6        greeting = Greeting.new
 7
 8        allow(greeting).to receive(:hello).and_return('Hello from stub!')
 9
10        expect(Human.new.meet).to eq('Hello from stub!')
11      end
12    end
13  end

先ほどの話を踏まえて考えると、instance_stub.rb の 3 行目の Greeting.newspec/instance_stub_spec.rb の 6 行目の Greeting.new は、見た目は同じに見えてもオブジェクトが異なります。

そのため、spec/instance_stub_spec.rb の 8 行目は、もし 6 行目で生成した Greeting インスタンスと同じオブジェクトであればスタブできますが、実際には instance_stub.rb の 3 行目で生成した Greeting インスタンスとは別のオブジェクトなのでスタブできていないということになります。

解決法

ではどのようにすれば良いかというと、Greeting インスタンスを生成する際の new メソッドをスタブすれば期待した通りにスタブできます。

spec/instance_stub_spec.rb
require_relative '../instance_stub'

describe Human do
  context 'meet' do
    it 'returns a stub message instead of a real message' do
      greeting_mock = instance_double(Greeting)

      allow(Greeting).to receive(:new).and_return(greeting_mock)
      allow(greeting_mock).to receive(:hello).and_return('Hello from stub!')

      expect(Human.new.meet).to eq('Hello from stub!')
    end
  end
end

先ほどのテストコードでは Greeting インスタンスを直接生成していましたが、今回は Greeting クラスの new をスタブして、greeting_mock という名前のモックを返すようにしています。

そしてこの greeting_mockhello というメソッドをスタブして、'Hello from stub!' を返すようにしています。

このテストコードを実行すると期待した通りに動作します。実際のコードで Greeting インスタンスを生成した際にモックオブジェクトにすげ替えられて、そのモックオブジェクトに対して hello メソッドが呼ばれるため、スタブされる際の返り値である 'Hello from stub!' が返却されます。

new をスタブしなくても良い例

ここまでの内容を理解したところで、自分は混乱してしまいました。それは『使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」』を読んだときです。

この記事で紹介されているコード例を引用させていただきます。

# 注:本当に動かす場合はtwitter gemが必要です
require 'twitter'

class WeatherBot
  def tweet_forecast
    twitter_client.update '今日は晴れです'
  end

  def twitter_client
    Twitter::REST::Client.new
  end
end
it 'エラーなく予報をツイートすること' do
  # Twitter clientのモックを作る
  twitter_client_mock = double('Twitter client')
  # updateメソッドが呼びだせるようにする
  allow(twitter_client_mock).to receive(:update)

  weather_bot = WeatherBot.new
  # twitter_clientメソッドが呼ばれたら上で作ったモックを返すように実装を書き換える
  allow(weather_bot).to receive(:twitter_client).and_return(twitter_client_mock)

  expect{ weather_bot.tweet_forecast }.not_to raise_error
end

上記の実際のコードでは twitter_client メソッド内で Twitter::REST::Client クラスのインスタンスを生成しています。そして、テストコードでは Twitter::REST::Clientnew メソッドをスタブしていません。

上記のテストコードは正しく動作するのですが、new メソッドをスタブしていないのになぜ正しくスタブできているのだろうと疑問に思いました。

しかし、落ち着いて考えれば正しくスタブできていることがわかります。Twitter::REST::Client クラスのインスタンスを生成しているだけの twitter_client メソッドを丸ごとスタブしているからです。

このテストコードでは WeatherBot クラスの tweet_forecast メソッドで例外が発生しないことを検証していますが、このメソッド内で呼び出されている twitter_client メソッドを丸ごとスタブしているため、twitter_client メソッドの中身の Twitter::REST::Client.new は実行されていないことになります。

そのため、正しくスタブできているというわけです。

インスタンスを生成するメソッドを別で用意して丸ごとスタブしてみる

先ほどの instance_stub.rb において、上記の項目で取り上げさせていただいた Twitter クライアントのコード風に、インスタンスを生成するだけのメソッドを作って書き換えてみましょう。すると以下のようなコードになります。

method_stub.rb
class Human
  def meet
    greeting_instance.hello
  end

  def greeting_instance
    Greeting.new
  end
end

class Greeting
  def hello
    'Hello!'
  end
end

以前は Human#meetGreeting インスタンスを生成していましたが、Greeting インスタンスを生成するだけの greeting_instance メソッドを用意してそれを呼び出すように変更しました。

上記のコードにおいて、greeting_instance メソッドを丸ごとスタブして Greeting#hello の返り値を変更するようなテストコードを書くと以下のようになります。

spec/method_stub_spec.rb
require_relative '../method_stub'

describe Human do
  context 'meet' do
    it 'returns a stub message instead of a real message' do
      greeting_mock = instance_double(Greeting)

      human = Human.new
      allow(human).to receive(:greeting_instance).and_return(greeting_mock)
      allow(greeting_mock).to receive(:hello).and_return('Hello from stub!')

      expect(human.meet).to eq('Hello from stub!')
    end
  end
end

先ほどの Twitter クライアントのテストコードと同様に、インスタンスを生成するだけの greeting_instance メソッドを丸ごとスタブして、モックオブジェクトを返すようにしています。

そのモックオブジェクトに対して hello メソッドが実行された際に、スタブして 'Hello from stub!' を返すようにしています。

greeting_instance メソッドを丸ごとスタブしているため、Greeting.new は実際には実行されません。そのため、上記のテストコードも正しくスタブできていることになります。

もちろん、Greeting.new の部分をスタブした場合でも期待した通りに動作します。つまり、method_stub.rb のコードに対して、spec/instance_stub_spec.rb のテストコードを実行しても正しく動作するわけです (require_relative でロードするファイルの対象を変更する必要はあります)。

method_stub.rb
class Human
  def meet
    greeting_instance.hello
  end

  def greeting_instance
    Greeting.new
  end
end

class Greeting
  def hello
    'Hello!'
  end
end
spec/instance_stub_spec.rb
require_relative '../method_stub'

describe Human do
  context 'meet' do
    it 'returns a stub message instead of a real message' do
      greeting_mock = instance_double(Greeting)

      allow(Greeting).to receive(:new).and_return(greeting_mock)
      allow(greeting_mock).to receive(:hello).and_return('Hello from stub!')

      expect(Human.new.meet).to eq('Hello from stub!')
    end
  end
end

この場合は、greeting_instance メソッドをスタブする代わりに、その中身である Greeting.new をスタブしていることになります。どちらの場合でもインスタンスの生成部分をスタブしていることになるので、正しく動作します。

まとめ

スタブしたいインスタンスメソッドを持つクラスのインスタンスをテストコード内で生成しても、実際のコード内のインスタンスとは別のオブジェクトなので、インスタンスメソッドを正しくスタブできないよというお話でした。

ところで、この記事ではモックオブジェクトを作る際に instance_double を使用しました。doubleinstance_double の違いについての記事も書きましたので、興味があれば併せてご覧ください。

RSpec における double / spy / instance_double / class_double のそれぞれの違いについて

参考にしたサイト

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

引数の基礎

引数がとても苦手でしたので、復習のためにまとめておきます。

rubyではメソッド内で定義していないものを使うことはできません。そのため、以下の記述はエラーになります。

ruby
def hoge
  puts huga
end

huga = "眠い"

hoge    #エラーになる

hogeメソッドはhugaをputsしたいわけなのですが、hugaはhogeメソッドの中で定義されたものではありません。もちろんhugaの定義をhogeの中ですればOKです。が、それだと引数の説明にならないのでこのまま引数を使ってエラーを解決します。

引数には本引数と仮引数があリます。

ruby
def hoge(仮引数)
  puts huga
end

huga = "眠い"

hoge(本引数)

本引数は、メソッドを呼ぶ方に書きます。仮引数は呼ばれたメソッドの方に書きます。

実際に本引数と仮引数を入れてみます。

ruby
def hoge(huga)
  puts huga
end

huga = "眠い"

hoge(huga)

今回のhogeメソッドはputs hugaをしたいわけですが、hugaがメソッド外で定義されているせいでエラーが起きていましたので、メソッドを呼び出す方の本引数にhugaを入れて、仮引数の方にもhugaを入れました。これによって最終行の引数の全てにhugaが入った状態になります。なのでエラーが出ずにターミナルには「眠い」と表示されます。

ここまでは、本引数と仮引数を同じ名前にしましたが、名前が異なっていても使えます。

ruby
def hoge(kotoba)
  puts kotoba
end

huga = "眠い"

hoge(huga)

本引数 : huga
仮引数 : kotoba

hogeメソッド内のputsの右側が「kotoba」に変わりました。これは、メソッドの中で引数として渡ってきたものを使うためには仮引数と同じ名前でないと使えないからです。例えば以下のようにするとエラーになります。

ruby
def hoge(kotoba)
  puts huga
end

huga = "眠い"

hoge(huga)    #エラーになる

仮引数はkotobaなのにputsの右側がhugaになってます。やっぱりhugaはhogeメソッドの中で当然ながらこれもダメです。

ruby
def hoge(huga)
  puts kotoba
end

huga = "眠い"

hoge(huga)    #エラーになる

相変わらずputsの右側のkotobaはhogeメソッドで定義されていません。

1.メソッドで使いたいメソッド外部の変数名など本引数の名前が一致していること
2.仮引数とメソッド内において仮引数で渡されたものを使いたいところの名前が一致していること

上記2点が守れていれば、本引数と仮引数の名前が異なっていても問題なく動きます。

ruby
def hoge(name2)
  puts name2
end

name1 = "眠い"

hoge(name1)

name1同士は同じ名前
name2同士も同じ名前

こうなっていればOKです。

追記:
1.本引数は実引数ということの方が多いようです。

2.本引数の中にはローカル変数だけでなく式を書くこともできます。なので何か変数名をいれなくても仮引数を書いたメソッドで使いたいものを本引数の部分に式として書いても使えます。

hoge.rb
def hoge(name)
  puts "私の名前は#{name}です"
end

puts hoge("ヤマダ")

ターミナル
ruby hoge.rb
私の名前はヤマダです

本引数の部分には変数名などではなく文字列をそのままいれていますが、問題なく結果が出ています。

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

#ruby の Timecop.freeze で Date を渡すと日付がずれるので Time.local を渡した方が良いかも

Passing Date with # ruby's Timecop.freeze will cause the date to shift, so it may be better to pass Time.local

Timecop.freeze Date.new(2019, 01, 01)
# => 2018-12-31 15:00:00 +0000

Date.today
# => Mon, 31 Dec 2018

Timecop.freeze ::Time.local(2019, 01, 01)
# => 2019-01-01 00:00:00 +0000

Date.today
# => Tue, 01 Jan 2019

Original by Github issue

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

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

現役ポールダンサー初めてのwebサービスをRuby on railsで作りました

qiita初投稿です!
私の本業はポールダンス、エアルリアルダンスをメインとするパフォーマンス業です。(時々インストラクター的なこともやってます)
そんな私がこの度Rubyonrailsでwebサービスを初めて作って公開しました。
なぜダンサーがプログラミングを?わざわざ、、、?などなどはこちらのブログに書いてますが、最初は単純にキャリアチェンジを考えていたところからプログラミング学習は始まりました。そこからやはり自分でサービスを作ってみよう!ということになって作ったサービスがこちらです。
https://poletricks-world.herokuapp.com/
デザインや機能共にまだまだ修正すべきところはありますし、目新しい機能というものはありませんが
機能、デザイン、エラーで困ったところなどを綴ります。
スクリーンショット 2019-07-14 19.23.09.png

アイディア

練習するときに見ることができるアプリがあればいいな(インスタだと、検索が面倒、技で検索できない、達成度が感じれられない、動画止められない)
・動画が止められて
・達成感が味わえて
・わかりやすい動画
・且つ無料
私が練習を始めた頃に欲しいなと思った機能です。これらを網羅したアプリを作りたかったのです。
プロトタイプを作ることを始めました。

開発環境

Rails 5.2
ruby 2.4.0
cloud9(途中からAWSのcloud9に移行)

プロトタイプを作る

プロトタイプは結構大まかでもいいのかなーと思っていましたが、私のメンターさんはできるだけ細かくデザインして、その機能をあとは乗せていくだけの方がいいよ、とのことで
イラレを使って作成していきました。
ちなみにイラストレーターを使ったのは今年のお正月が初めてです。ここは独学でアドビのイラストレーター初心者の動画をひたすら見ながら練習したり、ググったりして覚えました。すごいたくさんのことは全然できませんが
プロトタイプを作るくらいまでならサクサクできるように1ヶ月ほどでなりました。

大まかなページ展開

・トップページ
・サインイン、ログインページ
・動画一覧ページ
・動画詳細ページ
・ブログページ
サインイン、のところはgemのdeviseを使いました。
動画は最初、AWSを使う予定でしたが、こんがらがるのでまずはYoutubeにアップロードして動画リンクを入れていくことにしました。動画一覧にはyoutubeのサムネイル表示がされるようにしています。

Topページ

ナビゲーションバーのなかにリンク(ブログとコンタクト)、ログインログアウト、サインアップページ
ヘッダー画像とアカウント作成ボタン
動画例
使い方
アバウト
プロフィール
フッター(リンク、SNS)という構成です。

動画一覧ページ

ユーザーができた技数が表示される(マスターできた数/存在する技数 の計算式)
動画リンク(サムネイル+マスターボタン+カテゴリーラベル+動画リンク)
ここの動画は全てadminの私がnewで追加していくように実装しました。(newにアクセスすれば動画を自分で足すことも可能だけど今の所私のみ、動画をアップできるようにする)
スクリーンショット 2019-07-14 19.26.22.png
ブックマークの色が変わるところはAjax
検索機能はgemのransackを使いました。

動画詳細ページ

動画のyoutube
コメント欄(削除可能)←技のポイントなどを書き込めたら面白いなあと思い。
関連動画表示(ここはマスターボタンなどは乗せないで、カテゴリーで紐づかせて表示されるようにしました)
動画自体はループで表示されるようにした方がいいか、迷いどころなのでテストユーザーのフィードバックで都度変更していこうと思います。

Bookmarkページ

スクリーンショット 2019-08-16 20.13.55.png

ここはかなり時間がかかりました。動画一覧ページでブックマークしたものをユーザーごとのマイページとしてブックマークされるようにしました。masterボタンをキャンセルすると動画一覧ページも色がかわります。
同じくajaxを使い、masterした技のボタンがmaster→cancelと変更される様にしました。

主につまづいたところ

つまづいたことが多すぎてもうわからないくらいですが

デザインがうまくいかない(現時点でもまだ修正中!!)

Bootstrapを使ってますが、使ったら使ったで自由度がない部分もあり、折り合いが難しいです。
ヘッダーの画像は最初イラレのみで私の写真を切りはりしたのですが、汚く見えてしまったので
Photosopで加工→イラレでサイズ、バックの修正などを行いました。多分もっといい方法はあると思いますが現時点ではこれが一番でした。

herokuデプロイできない問題

テックアカデミーをやっていた時に使ったことがあるので余裕や!と思っていたけれど、
ふつーにエラーでなかなかデプロイできませんでした。
そもそもですが、別のブランチをmasterブランチにマージしていなかったため、まずはそこがエラーが出ていたようです。
とりあえずメンターに相談したらherokuの設定をする順番が間違っていたみたい。

Herokuでよくエラーが出た時の解決方法

何度かテストユーザーさんに使ってもらう上で、cloud9上で問題なく表示され、動作できたのでherokuにアップするとよく
We're sorry, but something went wrong.
表示が出ていました。
大体が以下のことで解決です。
・git commit してないのでする
・heroku db:migrationする
・heroku restartする
でほぼほぼ解決しました、一回表示するべきコードが消えていた時があって、その時はコードを付け足し直してエラーは解決しました。

AWSのcloud9

途中から以前のcloud9からAWSのcloud9に移行しました。
移行自体はスムーズに行ったのですが、、、
ある日急に容量がいっぱいであるというエラーが出て、ターミナルが使えなくなりました。
スクリーンショット 2019-08-16 20.06.05.png

githubにコードはギリギリまであげていたので、再度git cloneして新たなワークスペースで作成。
そしてなんとかまたできるようになったもののまたある程度コードを書くと警告が出て、
またもやターミナルが使えなくなり、サポートに問い合わせるも具体的なことはわかりかねるよでした。
次にAWSでもう一度ワークスペースを作る際、変更したのはEC2インスタンスです。(ここがキモでした)今までmicroを使っていたのですがこれをsmallに変更
サーバーはubuntuです。
これで無事AWSに新たなワークスペースが作ることができました。
そのあと、AWSのアクセスキー、シークレットキーを登録しエラーと格闘しながら無事にまたheroku上で公開する事ができました。

テストユーザーに利用してもらう

わたしの周りはインストラクターやショーにすでに出ている人たちだったのでポールダンスを習ってそこまで間もない生徒さんが必要でした。
友人の経営しているスタジオの生徒さんに協力してもらいフィードバックをいただくことになりました。
特に自分で作ってると裏側がわかるので導線などは、当たり前にここからログインするよね?
探しにいくよね?という前提で作っていたので、
動画ページに移動の方法に迷った、削除ボタンの位置がわかりにくい、戻り方がわからない、、
などなど導線をわかりやすくする工夫が必要になりました。
あとは重要なチェックボタンを押したあと、取り消しボタンがないこと、取り消しボタンがないため、同じボタンを押すと、Master数がどんどん同じ技で計上されてしまうようになっていたのでそこを修正する必要がありました。

まとめ

細かいエラーなどはまた別途ブログで書いていこうと思いますが
6か月弱かけてなんとかここまで作る事ができました。
プロの方からするとはっきり言ってかなり単純なwebサービスとなっているように思われるかもしれませんが、個人的にはかなり頑張りました。

自分で初めてプロダクトを作って見て、何度も投げ出しそうになったけど(というかまだデザイン終わってない)形にできて本当によかったし、もっと色々手を出して見たくなりました。
引き続き改良を試みつつ、技術のアップデートしたいです!
https://poletricks-world.herokuapp.com/
※2019年8月現在、エラー復旧中にAWSのcloud9のターミナルが使えなくなり、諸々修正が不可になりgit cloneするもバージョンコンフリクトなどで解消せず、、ローカルでぽちぽち作り直してます。一難去ってまた一難。。とりあえずこのurlとは別で新たにappを作っていこうと思います。

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

Rails ActiveRecordの結合時のクエリの内容について

参考

以下のページを参考にさせていただきました。ありがとうございます。
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い
【Rails】テーブル結合

データ構造

companiesテーブル

id name
1 A社
2 B社

servicesテーブル

id company_id name
1 1 Aアプリ1
2 1 Aアプリ2
3 1 Aアプリ3
4 2 Bの野望

パターン1 all(pluckなし)

companies = Company.all
companies.each do |company|
    company.services
end
SELECT `companies`.* FROM `companies`
SELECT `services`.* FROM `services` WHERE `services`.`company_id` IN (1,2)

パターン2 : all

companies = Company.all
companies.each do |company|
    model.services.pluck(:name).join(',')
end
SELECT `companies`.* FROM `companies`
SELECT `services`.`name` FROM `services` WHERE `services`.`company_id` = 1
SELECT `services`.`name` FROM `services` WHERE `services`.`company_id` = 2

パターン3 : join

companies = Company.joins(:services)
companies.each do |company|
    model.services.pluck(:name).join(',')
end
SELECT `companies`.* FROM `companies` INNER JOIN `services` ON `services`.`company_id` = `companies`.`id`
SELECT `services`.`name` FROM `services` WHERE `services`.`company_id` = 1
SELECT `services`.`name` FROM `services` WHERE `services`.`company_id` = 1
SELECT `services`.`name` FROM `services` WHERE `services`.`company_id` = 1
SELECT `services`.`name` FROM `services` WHERE `services`.`company_id` = 2

パターン4 : preload

companies = Company.preload(:services)
companies.each do |company|
    model.services.pluck(:name).join(',')
end
SELECT `companies`.* FROM `companies`
SELECT `services`.* FROM `services` WHERE `services`.`company_id` IN (1,2)

パターン5 : includes

companies = Company.includes(:services)
companies.each do |company|
    model.services.pluck(:name).join(',')
end
SELECT `companies`.* FROM `companies`
SELECT `services`.* FROM `services` WHERE `services`.`company_id` IN (1,2)

パターン6 : eager_load

companies = Company.includes(:services)
companies.each do |company|
    model.services.pluck(:name).join(',')
end
SELECT **省略** FROM `companies` LEFT OUTER JOIN `services` ON `services`.`company_id` = `companies`.`id`
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Railsでプロジェクト管理WEBアプリを開発

はじめに

UdemyでRuby on Railsのコースを学習したので、そこで学んだ知識を生かしてWEBアプリを開発しました。

ソースコード

GitHubで公開しています
https://github.com/Ryota7101/milestone.git

主な機能 

  • miliaによるマルチテナント機能
  • タスク機能
  • タスクへのカテゴリ付け
  • ファイルアップロード機能
  • プレミアム会員機能
  • 管理者機能

アプリ詳細

 サインアップ

スクリーンショット 2019-08-16 15.52.29.png

サインアップ時にフリープランかプレミアムプランかを選択できます。
フリープランの場合はカード情報入力欄が表示されません。
スクリーンショット 2019-08-16 15.56.47.png

プレミアムプランに切り替えると、クレジットカード情報入力欄が表示されます。
※情報を登録して処理する機能を設定してないので、ここで入力した情報は保存されません。
スクリーンショット 2019-08-16 16.00.33.png

プロジェクト作成

スクリーンショット 2019-08-16 16.02.46.png

プロジェクトを作成すると、プロジェクト一覧に表示されます。
スクリーンショット 2019-08-16 16.04.00.png

プロジェクト詳細ページ
スクリーンショット 2019-08-16 16.04.59.png

ファイルアップロード機能

テキストファイルや画像をアップできます。
※ただし、クラウドなど保存先を設定していないので、現状ではどこにもファイルは保存されません。
スクリーンショット 2019-08-16 16.09.37.png

プロジェクトページにアップしたファイルが表示されます。
スクリーンショット 2019-08-16 16.10.12.png

タスク

タスク作成時にカテゴリ(後述)も指定できます。
スクリーンショット 2019-08-16 16.15.46.png

スクリーンショット 2019-08-16 16.16.22.png

作成したタスクがプロジェクトページに表示されます。
スクリーンショット 2019-08-16 16.16.51.png

管理者機能

Organization作成者は自動的に管理者として登録されます。
管理者がログインしてる場合は、ナビゲーションバーに
* メンバー追加
* プラン変更
* タスクのカテゴリ管理
のボタンが表示されます。

スクリーンショット 2019-08-16 16.17.30.png

タスクのカテゴリ

タスクのカテゴリ管理(作成や削除)は管理者のみが行えます。
タスクを作成したり、タスクにカテゴリを設定するのは非管理者でも行えます。

スクリーンショット 2019-08-16 16.18.17.png

カテゴリを作成すると、カテゴリ一覧ページに表示されます。
使用する場合は、タスク作成時にチェックを入れます。
スクリーンショット 2019-08-16 16.18.44.png

プラン変更

プランを途中で変更できます
スクリーンショット 2019-08-16 16.19.46.png

メンバーを招待する

管理者はメンバーをプロジェクトに招待できます。
招待したい人の氏名やメールアドレスを入力すると、リンク付きのメールが相手に送信されます。
スクリーンショット 2019-08-16 16.21.21.png

届いたメールのリンクをクリックすると、パスワード設定画面に移動するので、パスワードを入力します
スクリーンショット 2019-08-16 16.21.57.png

管理者がプロジェクトページを見ると、招待した相手の情報が表示されるので、Addボタンを押すと招待が完了します。
スクリーンショット 2019-08-16 16.26.27.png

招待が完了すると、メンバー欄にメンバーの情報が表示されます。
スクリーンショット 2019-08-16 16.27.01.png

招待された相手がログインすると、管理者が作成したプロジェクトやファイル、タスクなどが表示されます。
※招待されたメンバーは管理者でないので、ナビゲーションバーにカテゴリ管理などのボタンは表示されません。
スクリーンショット 2019-08-16 16.28.00.png

スクリーンショット 2019-08-16 16.29.23.png

別の組織を作成

試しに別の組織でサインアップして、これまで作成した情報が表示されないことを確認します。
スクリーンショット 2019-08-16 16.30.59.png

これまでに作成した情報が表示されず、初期画面となっているため、組織ごとにデータが分かれてることがわかります。
スクリーンショット 2019-08-16 16.32.01.png

終わりに

  • Udemyのコースの内容が2015年ごろのもので、その通りにコードを描いても動かないことが多く、苦戦しました。(コースではRails4、著者は5で作成)

  • 相変わらずコード書いて実際にweb上で動かして確認して・・とやってしまい、テストはおざなりになってしまった。。

  • 実戦でRails使いたいです

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

Ruby pn Railsでプロジェクト管理WEBアプリを開発

はじめに

UdemyでRuby on Railsのコースを学習したので、そこで学んだ知識を生かしてWEBアプリを開発しました。

ソースコード

GitHubで公開しています
https://github.com/Ryota7101/milestone.git

主な機能 

  • miliaによるマルチテナント機能
  • タスク機能
  • タスクへのカテゴリ付け
  • ファイルアップロード機能
  • プレミアム会員機能
  • 管理者機能

アプリ詳細

 サインアップ

スクリーンショット 2019-08-16 15.52.29.png

サインアップ時にフリープランかプレミアムプランかを選択できます。
フリープランの場合はカード情報入力欄が表示されません。
スクリーンショット 2019-08-16 15.56.47.png

プレミアムプランに切り替えると、クレジットカード情報入力欄が表示されます。
※情報を登録して処理する機能を設定してないので、ここで入力した情報は保存されません。
スクリーンショット 2019-08-16 16.00.33.png

プロジェクト作成

スクリーンショット 2019-08-16 16.02.46.png

プロジェクトを作成すると、プロジェクト一覧に表示されます。
スクリーンショット 2019-08-16 16.04.00.png

プロジェクト詳細ページ
スクリーンショット 2019-08-16 16.04.59.png

ファイルアップロード機能

テキストファイルや画像をアップできます。
※ただし、クラウドなど保存先を設定していないので、現状ではどこにもファイルは保存されません。
スクリーンショット 2019-08-16 16.09.37.png

プロジェクトページにアップしたファイルが表示されます。
スクリーンショット 2019-08-16 16.10.12.png

タスク

タスク作成時にカテゴリ(後述)も指定できます。
スクリーンショット 2019-08-16 16.15.46.png

スクリーンショット 2019-08-16 16.16.22.png

作成したタスクがプロジェクトページに表示されます。
スクリーンショット 2019-08-16 16.16.51.png

管理者機能

Organization作成者は自動的に管理者として登録されます。
管理者がログインしてる場合は、ナビゲーションバーに
* メンバー追加
* プラン変更
* タスクのカテゴリ管理
のボタンが表示されます。

スクリーンショット 2019-08-16 16.17.30.png

タスクのカテゴリ

タスクのカテゴリ管理(作成や削除)は管理者のみが行えます。
タスクを作成したり、タスクにカテゴリを設定するのは非管理者でも行えます。

スクリーンショット 2019-08-16 16.18.17.png

カテゴリを作成すると、カテゴリ一覧ページに表示されます。
使用する場合は、タスク作成時にチェックを入れます。
スクリーンショット 2019-08-16 16.18.44.png

プラン変更

プランを途中で変更できます
スクリーンショット 2019-08-16 16.19.46.png

メンバーを招待する

管理者はメンバーをプロジェクトに招待できます。
招待したい人の氏名やメールアドレスを入力すると、リンク付きのメールが相手に送信されます。
スクリーンショット 2019-08-16 16.21.21.png

届いたメールのリンクをクリックすると、パスワード設定画面に移動するので、パスワードを入力します
スクリーンショット 2019-08-16 16.21.57.png

管理者がプロジェクトページを見ると、招待した相手の情報が表示されるので、Addボタンを押すと招待が完了します。
スクリーンショット 2019-08-16 16.26.27.png

招待が完了すると、メンバー欄にメンバーの情報が表示されます。
スクリーンショット 2019-08-16 16.27.01.png

招待された相手がログインすると、管理者が作成したプロジェクトやファイル、タスクなどが表示されます。
※招待されたメンバーは管理者でないので、ナビゲーションバーにカテゴリ管理などのボタンは表示されません。
スクリーンショット 2019-08-16 16.28.00.png

スクリーンショット 2019-08-16 16.29.23.png

別の組織を作成

試しに別の組織でサインアップして、これまで作成した情報が表示されないことを確認します。
スクリーンショット 2019-08-16 16.30.59.png

これまでに作成した情報が表示されず、初期画面となっているため、組織ごとにデータが分かれてることがわかります。
スクリーンショット 2019-08-16 16.32.01.png

終わりに

  • Udemyのコースの内容が2015年ごろのもので、その通りにコードを描いても動かないことが多く、苦戦しました。(コースではRails4、著者は5で作成)

  • 相変わらずコード書いて実際にweb上で動かして確認して・・とやってしまい、テストはおざなりになってしまった。。

  • 実戦でRails使いたいです

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

【AWS】開発環境では動くが本番だと�動かない事例集

Qiita初投稿になります。
何か不備や誤りがあったらご指摘をいただけるととてもありがたいです!

経緯

スクールでの課題や個人アプリの開発でAWSを使っており、デプロイも双方担当をさせていただいておりました。
その中で、苦戦をした大きな比率を占めるのが、開発環境だと動くのに何故か本番環境だと動かない事例です。
本番で動かないと実際のサービスだと何も意味がないにも関わらず、このパターンが結構ありましたので、これから対処をする方がどういう方法や考え方で解いていけばいいのかをまとめます。
デプロイで苦戦をすると本当にイライラしてしまうので、イライラを少しでも沈めるのに貢献をできたらなと思います。

開発環境(個人アプリもスクールの課題も同一です)
言語:Ruby 2.5.1
フレームワーク:Rails 5.2.3
データベース:MySQL5.6
ライブラリ:JQuery
自動デプロイ:capistrano
Webサーバー(本番):nginx
APサーバー(本番):unicorn
Web・APサーバー(開発):puma
今回は双方ともにEC2、S3を用いております。

前提とエラーの基本の見方

EC2インスタンスの再起動が1番今まで救われたケースが多いです。まずは試してみましょう。(MySQLとnginxの再起動も必要です)

そして、エラーを解決をする基本はエラーログを見て忠実に動くことです。これは本番環境でも開発環境でも一緒です。
エラーが出る場合、開発環境だとターミナルやビューに出てきます。ただ、本番環境だと出てこないため、本番環境だと別でコマンドで見ていきます。
エラーログの本番環境の場所はlinuxコマンドでログインをして確認をします。注意をするのは自動デプロイの前後でエラーが吐き出される場所が異なることです。自動デプロイをしたにも関わらず元のままの位置でファイルを開いても意味がないです。

#手動デプロイ後
/var/www/アプリ名/log/production.log
#capistranoでの自動デプロイ後
/var/www/アプリ名/current/log/production.log

logにまで移動をしたらcat等のコマンドでエラーログを見ることができます。

①pumaとの性能の違いで、拡張子がついていなかった時

ActionView::Template::Error (The asset "bannar_image" is not present in the asset pipeline.):
※本番環境のエラーログです。
これが一番はじめのエラーでした。こちらに関しては上記のエラー文を見ると、bannar_imageが間違っているのが何となく分かると思います。
このbannar_imageという記述があったのが、image_tagになるので、image_tagに関して調べてみました。

index.erb
#エラーが起きたコードのイメージ
<%= image_tag 'bannar_image' %>
#他のサイトで見た見本のコード
<%= image_tag 'flower.png' %>

実際にビューファイルを比べてみると何か違和感が、、、上のコードは実は拡張子がなかったのです。
そのため、本番環境では動かなかったのです。

ちなみにこの時はそれまで自動デプロイが初めてできた瞬間に出たエラーだったため、自動デプロイのcapistranoのエラーだと勘違いをしてしまっておりました。
自動デプロイのエラーばかり見ていたので、何もエラーログがないのに原因不明のエラーが起こっていると勘違いして、だいぶハマりました。
デプロイがどういう流れになっていてどういう仕組みになっているのかを理解をしていないとこういうところで非常に苦戦をします。

というかpumaは何故拡張子がないのに動くのか、、、

②application.jsにJQueryが2つあった時

こちらはそこまで難しくないエラーです。お互いに干渉してしまうため、エラーになるようです。
今出せないのですが、エラーログも出ていて、そんなに複雑なエラーログではありませんでした。

application.js
=require rails-ujs
=require jquery-ujs

下のコードはヴァージョンが古い時に使っていたようでして、Rails5.1以降は上のコードを使うみたいです。
これはスクールの発表で先輩グループの発表の際に触れていたので、すぐに分かりました。
①もだけどそもそもpumaの時点でエラー起きてくれないのかな。。。

③データベース関連で開発環境とmigrationの状況が異なってしまった時

ActionView::Template::Error (Unknown database 'freemarket_sample_54c_production'):

チーム開発でmearge済みのmigrationファイルを変更をしてその後に再度meargeをしたり、
rake db:reset等を繰り返しているといつか起きてしまうかと思います。
私たちのチームではどうしようもなくなってしまい、結局一から本番環境のデータベースを構築をし直したのですが、これは本来あまりやっていいことではないかと思っています。
もちろん開発の初期段階ですので正直困りはしなかったのですが、実際のサービスだと既に重要な顧客データが多数入っているためです。
こちらは他に何かやり方あれば教えていただけると非常に嬉しいです。

ちなみにcapistrano導入後はデータベースをcreateしたり、dropをさせるときもエラーを見るのと同じくcurrent上でコマンドは打ちます。
rake:db:seedも同様で、capistrano導入後はディレクトリの位置が変更をされるためです。

④credentials.yml関連

ActiveSupport::MessageEncryptor::InvalidMessage

このエラーは序盤結構多かったです。こちらに関しては他に詳しい記事も多数あるかと思いますので割愛します。
credentials.ymlに関しては最初は結構苦戦をしましたが、慣れるとAPIkeyの管理がすごく簡単でした。
怖がるものではないかと思います。

⑤データベース内に数値が入っていなかった時

ActionView::Template::Error (undefined method `id' for nil:NilClass):

すごく単純なようで起きがちなエラーです。
特にテーブルを増やしてカラムに新しく外部キーを増やすと起こりがちです。
私はデータベースをSQLで直接いじっていましたが、Sequelpro等で本番でも見れるようにすることができるので、入れたら楽に設定をできると思います。
※ベーシック認証をしてからだと思うのですが、途中からSequelproは使えなくなりました。おそらく弾かれてしまったのだと思います。もしかするとベーシック認証を使っていてもできるのかもしれませんが、私は調べきれませんでした。

⑥JQueryで発火をしなかった※データが入っていない時

エラーログは出ません。
インクリメンタルサーチをしようとしたのですが、本番環境のデータで名前を検索をする際に、そもそも該当の名前のデータが入っていませんでした。。。

これは完全に単なる勘違いです。即解決できましたが、全くエラーログが出てこないので原因が不明になるので少しの時間、不安になりました。
これも上記の⑤と一緒でSequelproとか入れたらわかりやすそうです。

まとめ

基本はエラーログをしっかりと見ていけば何とかなりました。ただ、データベース関連は視覚化ができていなかったので、苦労をしました。
これに関してはエラーログの構造等を理解しだしてから、格段に理解が深まったので、細かいWebアプリの仕組みに関しては理解をしといたほうがいいなと感じました。
今回挙げた事例に関しては正直かなり簡単なほうだと思っています。これから先も出てくる事例は多いと思うので、また何かあれば記事に挙げていきます。

参考記事

https://www.javadrive.jp/rails/template/index11.html
https://www.bokukoko.info/entry/2017/10/27/231129
https://qiita.com/scivola/items/cc06ddbfd94d3118f693

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

【AWS】開発環境では動くが本番だと動かない事例集

Qiita初投稿になります。
何か不備や誤りがあったらご指摘をいただけるととてもありがたいです!

経緯

スクールでの課題や個人アプリの開発でAWSを使っており、デプロイも双方担当をさせていただいておりました。
その中で、苦戦をした大きな比率を占めるのが、開発環境だと動くのに何故か本番環境だと動かない事例です。
本番で動かないと実際のサービスだと何も意味がないにも関わらず、このパターンが結構ありましたので、これから対処をする方がどういう方法や考え方で解いていけばいいのかをまとめます。
デプロイで苦戦をすると本当にイライラしてしまうので、イライラを少しでも沈めるのに貢献をできたらなと思います。

開発環境(個人アプリもスクールの課題も同一です)
言語:Ruby 2.5.1
フレームワーク:Rails 5.2.3
データベース:MySQL5.6
ライブラリ:JQuery
自動デプロイ:capistrano
Webサーバー(本番):nginx
APサーバー(本番):unicorn
Web・APサーバー(開発):puma
今回は双方ともにEC2、S3を用いております。

前提とエラーの基本の見方

EC2インスタンスの再起動が1番今まで救われたケースが多いです。まずは試してみましょう。(MySQLとnginxの再起動も必要です)

そして、エラーを解決をする基本はエラーログを見て忠実に動くことです。これは本番環境でも開発環境でも一緒です。
エラーが出る場合、開発環境だとターミナルやビューに出てきます。ただ、本番環境だと出てこないため、本番環境だと別でコマンドで見ていきます。
エラーログの本番環境の場所はlinuxコマンドでログインをして確認をします。注意をするのは自動デプロイの前後でエラーが吐き出される場所が異なることです。自動デプロイをしたにも関わらず元のままの位置でファイルを開いても意味がないです。

#手動デプロイ後
/var/www/アプリ名/log/production.log
#capistranoでの自動デプロイ後
/var/www/アプリ名/current/log/production.log

logにまで移動をしたらcat等のコマンドでエラーログを見ることができます。

①pumaとの性能の違いで、拡張子がついていなかった時

ActionView::Template::Error (The asset "bannar_image" is not present in the asset pipeline.):
※本番環境のエラーログです。
これが一番はじめのエラーでした。こちらに関しては上記のエラー文を見ると、bannar_imageが間違っているのが何となく分かると思います。
このbannar_imageという記述があったのが、image_tagになるので、image_tagに関して調べてみました。

index.erb
#エラーが起きたコードのイメージ
<%= image_tag 'bannar_image' %>
#他のサイトで見た見本のコード
<%= image_tag 'flower.png' %>

実際にビューファイルを比べてみると何か違和感が、、、上のコードは実は拡張子がなかったのです。
そのため、本番環境では動かなかったのです。

ちなみにこの時はそれまで自動デプロイが初めてできた瞬間に出たエラーだったため、自動デプロイのcapistranoのエラーだと勘違いをしてしまっておりました。
自動デプロイのエラーばかり見ていたので、何もエラーログがないのに原因不明のエラーが起こっていると勘違いして、だいぶハマりました。
デプロイがどういう流れになっていてどういう仕組みになっているのかを理解をしていないとこういうところで非常に苦戦をします。

というかpumaは何故拡張子がないのに動くのか、、、

②application.jsにJQueryが2つあった時

こちらはそこまで難しくないエラーです。お互いに干渉してしまうため、エラーになるようです。
今出せないのですが、エラーログも出ていて、そんなに複雑なエラーログではありませんでした。

application.js
=require rails-ujs
=require jquery-ujs

下のコードはヴァージョンが古い時に使っていたようでして、Rails5.1以降は上のコードを使うみたいです。
これはスクールの発表で先輩グループの発表の際に触れていたので、すぐに分かりました。
①もだけどそもそもpumaの時点でエラー起きてくれないのかな。。。

③データベース関連で開発環境とmigrationの状況が異なってしまった時

ActionView::Template::Error (Unknown database 'freemarket_sample_54c_production'):

チーム開発でmearge済みのmigrationファイルを変更をしてその後に再度meargeをしたり、
rake db:reset等を繰り返しているといつか起きてしまうかと思います。
私たちのチームではどうしようもなくなってしまい、結局一から本番環境のデータベースを構築をし直したのですが、これは本来あまりやっていいことではないかと思っています。
もちろん開発の初期段階ですので正直困りはしなかったのですが、実際のサービスだと既に重要な顧客データが多数入っているためです。
こちらは他に何かやり方あれば教えていただけると非常に嬉しいです。

ちなみにcapistrano導入後はデータベースをcreateしたり、dropをさせるときもエラーを見るのと同じくcurrent上でコマンドは打ちます。
rake:db:seedも同様で、capistrano導入後はディレクトリの位置が変更をされるためです。

④credentials.yml関連

ActiveSupport::MessageEncryptor::InvalidMessage

このエラーは序盤結構多かったです。こちらに関しては他に詳しい記事も多数あるかと思いますので割愛します。
credentials.ymlに関しては最初は結構苦戦をしましたが、慣れるとAPIkeyの管理がすごく簡単でした。
怖がるものではないかと思います。

⑤データベース内に数値が入っていなかった時

ActionView::Template::Error (undefined method `id' for nil:NilClass):

すごく単純なようで起きがちなエラーです。
特にテーブルを増やしてカラムに新しく外部キーを増やすと起こりがちです。
私はデータベースをSQLで直接いじっていましたが、Sequelpro等で本番でも見れるようにすることができるので、入れたら楽に設定をできると思います。
※ベーシック認証をしてからだと思うのですが、途中からSequelproは使えなくなりました。おそらく弾かれてしまったのだと思います。もしかするとベーシック認証を使っていてもできるのかもしれませんが、私は調べきれませんでした。

⑥JQueryで発火をしなかった※データが入っていない時

エラーログは出ません。
インクリメンタルサーチをしようとしたのですが、本番環境のデータで名前を検索をする際に、そもそも該当の名前のデータが入っていませんでした。。。

これは完全に単なる勘違いです。即解決できましたが、全くエラーログが出てこないので原因が不明になるので少しの時間、不安になりました。
これも上記の⑤と一緒でSequelproとか入れたらわかりやすそうです。

まとめ

基本はエラーログをしっかりと見ていけば何とかなりました。ただ、データベース関連は視覚化ができていなかったので、苦労をしました。
これに関してはエラーログの構造等を理解しだしてから、格段に理解が深まったので、細かいWebアプリの仕組みに関しては理解をしといたほうがいいなと感じました。
今回挙げた事例に関しては正直かなり簡単なほうだと思っています。これから先も出てくる事例は多いと思うので、また何かあれば記事に挙げていきます。

参考記事

https://www.javadrive.jp/rails/template/index11.html
https://www.bokukoko.info/entry/2017/10/27/231129
https://qiita.com/scivola/items/cc06ddbfd94d3118f693

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

識別子

■ローカル変数

先頭:英小文字又は_
構成文字:英数字又は_
スコープ:最初に代入式が使用された位置から、その代入を含むブロックまたはメソッド定義の終わりまで。
初期化前に参照した際の挙動:参照箇所より前に代入文が記述してあるが実行されなかった場合はnil
              代入文が記述されていない場合は例外発生

sample_nil.rb
#ローカル変数aへの代入文があるが実行されないため結果はnilとなる
def method(i)
    if i > 0 then
        p "pass"
        a = "hello"
    end
    p a
end
method(-1)
sample_nil.rb
#ローカル変数aへの代入文がないため例外(NameError)となる
def method(i)
    p a
end
method(-1)

■グローバル変数

先頭:$
構成文字:英数字又は_
スコープ:どこからも参照可能
初期化前に参照した際の挙動:nil

sample_nil_global.rb
#グローバル変数$aへの代入文がないためnilとなる
$b = 1
def method
    p $a
end
method

■クラス変数

先頭:@@
構成文字:英数字又は_
スコープ:そのクラスの全インスタンスから参照可能
初期化前に参照した際の挙動:例外発生

■インスタンス変数

先頭:@
構成文字:英数字又は_
スコープ:そのインスタンス内で参照可能
初期化前に参照した際の挙動:nil

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #10 リメンバーミー機能編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#9 永続セッション, cookie編

こんなことが分かる

  • リメンバーミー機能の実装方法

一緒に勉強していこう:bow:
注意:リメンバーミー機能は前回(#9)の導入を済ませた上で機能します。

今回の流れ

  1. ログイン画面にリメンバーミーのチェックボックスを表示する
  2. リメンバーミー機能を実装する

チェックボックスを表示する

まずはログイン画面にチェックボックスを表示する。
クラス名をつける際、Bootstrap4と3では異なるので注意する。
スタイリングはいい感じだったのでほぼそのまま。

app/views/sessions/new.html.erb
<% provide(:title, "ログイン") %>
<div class="container form-container login-container">
  <div class="row">
    <div class="col">
      <div class="form-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="form-title">ログイン</h1>
      <%= form_with(scope: :session, url: login_path, local: true) do |form| %>

        <div class="form-group">
          <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
        </div>

        <div class="form-group">
          <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
        </div>

        <div class="form-group form-check">
          <%= form.check_box :remember_me, class: 'form-check-input' %>
          <%= form.label :remember_me, '次から保存(ログイン省略)', class: 'form-check-label' %>
        </div>

        <div class="form-group">
          <%= form.submit "ログイン", class: "btn btn-info btn-lg form-submit" %>
        </div>
      <% end %>

      <p class="form-go-to-signup-or-login">新しくはじめる方は<%= link_to "こちら", signup_path %></p>
    </div>
  </div>
</div>
/app/assets/stylesheets/application.scss.erb
// 中略
.form-submit {
  width: 100%;
  // margin-top: 1rem; // 削除
}

lantern_lantern_checkbox.png
(header省略)

リメンバーミー機能を実装する

チェックボックスはparams[:session][:remember_me]に保存される。
それぞれオン→'1'、オフ→'0'。
だから実装は超簡単。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
# 中略
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'メールアドレスかパスワードが正しくありません'
      render 'new'
    end
  end
# 中略

これでリメンバーミー機能の実装は完了。

今日は短いけどここまで。

前回:#9 永続セッション, cookie編

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

#Mysql2::Error: Can't connect to MySQL server onについて

起こったこと

備忘録です。低レベルですが、同じエラーを起こしてしまった人のために。
MacOSのアプデを自動でしているため、再起動に入ってしまった。
アプデ後、bundle startをしてもうまく起動しない。
いつもならターミナルがbashからrubyに変わるのに跳ねられてしまう。
色々調べていると、次のエラー文がみられた。

Mysql2::Error: Can't connect to MySQL server on なんちゃら

あんまり英語が得意ではないが、MysqlがCan't connectなので接続で来てないらしい。
さてここで重要なのは、僕が初心者すぎてMysqlなんぞやってレベルであること。

とりあえずググってみた

スクリーンショット 2019-08-16 13.37.36.png

解決策は[ $mysql.server start ]

スクリーンショットの検索結果は、残念ながら解決策よりも高等な解決策を示されていた。
以下のURLを参考にしていただきたいです。
https://qiita.com/itooww/items/13055c8bb1d226ee5844

原因は再起動によるMySQLの活動停止

当たり前だが、再起動とか電源切るとアプリケーションは止まる。
ただ、エンジニアになって自分がどういう物を使っているかをイメージできないことも多い。
バグだとか、壊れたわけではないが原因がわからないと解決の仕様が無い。。。

こうならないための対策

MySQLの自動起動です。これでいちいち立ち上げなくてもよくなります!
https://qiita.com/rinkun/items/c1649bcbe9a79bf2b07e

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

【備忘録】Rails5でモデル名を変更する方法

前提条件

OS:Mac OS X
Rails:5.2.3
groupモデルをroomモデルに変更します。

モデル名の変更方法

最初に結論から。
そのあとそれぞれのコマンドの解説をしています。
※置換で一気に変更する際は、migrationファイルなど変更したくないファイルには気をつけてください。

1. 変更するモデルに対応するDBのテーブル名を変更

20190815230119_rename_groups.rb
  def change
    # :groupsが変更前
    rename_table :groups, :rooms
  end

変更内容をDBに反映

ターミナル
   $ rails db:migrate

2.モデルに関連するファイル名を全て変更

ターミナル
   # find 対象ディレクトリ | xargs rename -s 変更前のファイル名 変更後のファイル名
   $ find ./ | xargs rename -s group room
   $ find ./ | xargs rename -s Group Room

■ 実行結果の例 ■
groups_controller.rb
    ↓
rooms_controller.rb

3.モデルが使用されてるファイルの中身を全て変更

ターミナル
   # find 対象ディレクトリ -name "検索する文字列" | LC_ALL=C xargs sed -i -e "s/変換前/変換後/g"
   $ find ./ -name "*room*" | LC_ALL=C xargs sed -i -e "s/group/room/g"
   $ find ./ -name "*room*" | LC_ALL=C xargs sed -i -e "s/Group/Room/g"

■ 実行結果の例 ■
class Admin::GroupsController < ApplicationController
           ↓
class Admin::RoomsController < ApplicationController

4.バックアップとしてできたファイルを削除

なぜかファイル名の末尾に-eとつくバックアップのファイルが生成されたため削除する。

ターミナル
   # find 対象ディレクトリ -name "検索する文字列" | xargs rm
   $ find ./ -name "*.*-e" | xargs rm

■ 実行結果 ■
下記のようなファイル名の末尾に-eとつくファイルが削除された。
groups_controller.rb-e

使用したコマンドの解説

findコマンド

ファイルやディレクトリを検索できる。

オプション コマンドの説明
-name ファイル名を指定して検索できる

xargsコマンド

前に実行したコマンドの結果を引き継げる。

ターミナル
   # find 対象ディレクトリ | xargs rename -s 変更前のファイル名 変更後のファイル名
   $ find ./ | xargs rename -s group room

上記の例だと、「find ./」で自分がいる階層のファイルとディレクトリを検索している。
xargsを使ってその検索結果を引き継ぎ、引き継いだ検索結果の中からファイル名がgroupの
ものをroomに変更している。

sedコマンド

ファイルの中身のテキストを置換できる。

オプション コマンドの説明
-i ファイルを上書きする
s 正規表現で置換処理をする
g マッチした全ての文字列を置換する

-eコマンドはちょっとよくわかんない。
ご存知の方、ご教授いただけると助かります。

LC_ALL=C

出力する言語や時刻などのフォーマットを設定する。
LC_ALL=Cはデフォルトの言語(英語)で出力される。

デフォルトの言語を日本語にしているせいかこれつけないとillegal byte sequence
っていうエラーがでるからつけてます。

終わりに

モデル名の変更に時間がかかったので自分用のメモとしてこの記事を書きました。
皆様のモデル名の変更するときの参考になれば幸いです。

なぜバックアップのファイルが出力されたのか謎。

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

Database Cleanerの使い方(学習時のメモ)

Database Cleanerとは

Database CleanerとはRubyのデータを削除するためのgemで提供されているパッケージ
今回はRspecテスト実行時に作成したテストデータをテスト終了後にデータベースから削除するために利用

setup方法

Gemfileに追記する 
(テスト環境でのみ利用できればよかったため以下のような記載に)

group :test do
  gem 'database_cleaner'
end

そしてターミナルで以下を実行

$ bundle install

使い方

RSpecの場合のコードのサンプルは以下

spec_helper.rb
RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end

end

解説

実行順序

before(:suite)
before(:all)
before(:each)
after(:each)
after(:all)
after(:suite)

実行タイミング

:suite
=>rspecコマンド実行時にテスト全体の前または後に呼び出される
:all
=> 各contextの前または後に呼び出される
:each
=>各exampleの前または後に呼び出される

Strategies

データベースのCleanする方法のことをStrategieと呼び、以下の3種類が準備されている

Transaction
=>一連の処理をTransactionという単位で囲み、その始点の状態にデータを戻すことで、データベースをCleanする
Truncation
=>指定したテーブルに格納されているデータをすべて削除する"TRANCATE TABLE 文"を使用して、データベースをCleanする
 テーブルをいったん削除して改めてテーブルを作成する
Deletion
=>テーブルはそのままにレコードデータを削除する"DELETE 文"を使用して、データベースをCleanする

データ削除対象テーブルの指定

:truncationのstrategyを使用するときは、
以下のように実行対象(または実行除外対象)のテーブルを指定できる

DatabaseCleaner.strategy = :truncation, {:only => %w[widgets dogs some_other_table]}
DatabaseCleaner.strategy = :truncation, {:except => %w[widgets]}

補足情報

rails_helper.rbファイルに以下のコードが書かれている場合、rspecのテスト時は
before(:each)でtransactionを張ってafter(:each)でrollbackする処理を実行してくれている。

rails_helper.rb
RSpec.configure do |config|
  config.use_transactional_fixtures = true
end

参考文献

https://github.com/DatabaseCleaner/database_cleaner
https://qiita.com/shoichiimamura/items/25942acc1d1bd78ef9c3

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

Database Cleanerの使い方

Database Cleanerとは

Database CleanerとはRubyのデータを削除するためのgemで提供されているパッケージ
今回はRspecテスト実行時に作成したテストデータをテスト終了後にデータベースから削除するために利用

setup方法

Gemfileに追記する 
(テスト環境でのみ利用できればよかったため以下のような記載に)

group :test do
  gem 'database_cleaner'
end

そしてターミナルで以下を実行

$ bundle install

使い方

RSpecの場合のコードのサンプルは以下

spec_helper.rb
RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end

end

解説

実行順序

before(:suite)
before(:all)
before(:each)
after(:each)
after(:all)
after(:suite)

実行タイミング

:suite
=>rspecコマンド実行時にテスト全体の前または後に呼び出される
:all
=> 各contextの前または後に呼び出される
:each
=>各exampleの前または後に呼び出される

Strategies

データベースのCleanする方法のことをStrategieと呼び、以下の3種類が準備されている

Transaction
=>一連の処理をTransactionという単位で囲み、その始点の状態にデータを戻すことで、データベースをCleanする
Truncation
=>指定したテーブルに格納されているデータをすべて削除する"TRANCATE TABLE 文"を使用して、データベースをCleanする
 テーブルをいったん削除して改めてテーブルを作成する
Deletion
=>テーブルはそのままにレコードデータを削除する"DELETE 文"を使用して、データベースをCleanする

データ削除対象テーブルの指定

:truncationのstrategyを使用するときは、
以下のように実行対象(または実行除外対象)のテーブルを指定できる

DatabaseCleaner.strategy = :truncation, {:only => %w[widgets dogs some_other_table]}
DatabaseCleaner.strategy = :truncation, {:except => %w[widgets]}

補足情報

rails_helper.rbファイルに以下のコードが書かれている場合、rspecのテスト時は
before(:each)でtransactionを張ってafter(:each)でrollbackする処理を実行してくれている。

rails_helper.rb
RSpec.configure do |config|
  config.use_transactional_fixtures = true
end

参考文献

https://github.com/DatabaseCleaner/database_cleaner
https://qiita.com/shoichiimamura/items/25942acc1d1bd78ef9c3

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

Pay.jpのキー

Ruby 2.5.1
Rails 5.2.3

スクールの課題(チーム開発で)デプロイ担当となってしまった私は、Pay.jpのAPIのキーがないというエラーに直面しましたので、少しでも参考になればとやったことを書いておきます。

Pay.jp利用のための公開鍵、秘密鍵の登録

local環境ではdotenv-railsを利用して環境変数を管理していましたが、本番でAPIキーがないことを示すエラーが出ていました。環境変数の読み込みがうまくいっていないということですね。

何はともあれ.bash_profileに書き込む

ローカル、リモート共にキーの情報をbashに直書き!

$ vim ~/.bash_profile

APIの秘密鍵、公開鍵の情報をコピー&ペースト。

$ source ~/.bash_profile

有効化するには上記コマンドが必要。記述が変だとエラーが出るので注意しましょう。

結局失敗。

.env.developmentを作成しなかったためでは?

envのメリットは環境ごとに扱う変数の管理ができること。

.env.development
.env.test
.env.developmen
と種類がある。

私のチームでは.env.developmentしかなかった。よって、prodductionを作成。

①作成 失敗
②touch コマンドで本番(EC2)current以下に作成 失敗

リモートの/env.environment以下に書き込む

ターミナルで以下の操作

$ sudo vim /etc/environment

環境変数を有効にするため一旦ログアウト、サイドリモートに接続し直す。

$ env | grep キーの名前

これで値が取れているか確認できます。

私の場合はさらにunicornを再起動したらうまくいきました。

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #9 永続セッション, cookie編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい
  • ポートフォリオを作成しながら勉強したい

前回:#8 ログイン/ログアウト, FactroyBot編
次回:#10 リメンバーミー機能編

こんなことが分かる

  • 永続化するメソッドが知れる
  • cookieについて分かる
  • attr_accessorについて分かる
  • クラスメソッドとインスタンスメソッドの違いが分かる

一緒に勉強しませう:bow:

今回の流れ

  1. 永続化の手順をざっくり解説
  2. 各単語やメソッドを確認
  3. 永続化を実装
  4. バグを除去

こういう流れでやって行きます。

どうやってログインを永続化させる?

ログインを永続化させるリメンバーミー機能。
(リメンバーミー機能の実装は次回)
流れ的にはこうらしい(以下Tutorial 9.1 Remember me 機能を引用)

  1. 記憶トークンにはランダムな文字列を生成して用いる
  2. ブラウザのcookiesにトークンを保存するときには、有効期限を設定する
  3. トークンはハッシュ値に変換してからデータベースに保存する
  4. ブラウザのcookiesに保存するユーザーIDは暗号化しておく
  5. 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する

なんのこっちゃ。
それによく分からない言葉やメソッドやらがいっぱいある。

  • cookie
  • remember_digest
  • remember_token
  • User.new_token
  • User.digest
  • remember
  • attr_accessor ← これはRubyの話だけど

これも1つずつ解説しよう。

永続化の仕組みをざっくり解説

これから以下の工程で永続化を行います。
超ざっくりにまとめるとこんな感じ。

  1. サーバに記憶トークンとIDの暗号作ってもらう
  2. パソコンに保存する
  3. 次来た時保存した暗号をサーバと照らし合わせる
  4. OKだったら勝手にログインする

クッキー(cookie)とは?

クッキーはパソコンとサーバの橋渡しです。
...質素すぎますね。

クッキーちゃんというキャラを想像してみよう(強引)
:cookie: ...ここにいました。

:cookie:はサーバと私たちのパソコンを繋ぐメモ係です。
私たちが「次もログインしたままがいいなあ、サーバさんに伝えといてくれる?」
とお願いすると、:cookie:は「分かった!」と言ってくれました。

しかし:cookie:はか弱いです。
サーバに重要な情報を渡す途中で盗まれたりしたら大変!
なので名前をつけた情報(remember_token)を暗号(remember_digest)にしちゃいました。

これで安全!:cookie:はサーバさんに情報を伝えるおつかいに行ったのでした。

分かりやすい解説↓
クッキー(cookie)とは?初心者でも分かるように図解

remember_token / remember_digestとは?

remember_tokenを暗号化 → remember_digestに代入
こういう関係性。

User.new_token / User.digest / rememberとは?

User.new_token → remember_tokenを作るメソッド
User.digest → remember_tokenとかの値を暗号にするメソッド
remember → 暗号をremember_digestに代入するメソッド

User.digestと関連する解説↓
ハッシュ値 (hash value)とは

remember_digestに保存するまでを実装する

ここまでくると各メソッドがどういう風に動くかが見えてくる。

  1. User.new_tokenメソッドでremember_tokenを作る
  2. User.digestメソッドでremember_tokenを暗号化する
  3. rememberメソッドで暗号化したremember_tokenをremember_digestに保存する

では早速実装しよう。
その前に1つ。
User.digestやUser.new_tokenはこんな風に書き換えできる。

class << self
  def digest(string)
  end

  def new_token
  end
end

これを踏まえた上で実装する。
まずUserモデルにremember_digest属性を追加。

bash
$ rails generate migration add_remember_digest_to_users remember_digest:string
$ rails db:migrate

その後user.rbにメソッドを書く。

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remeber_token  
  # 中略

  class << self
    def digest(string)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                    BCrypt::Engine.cost
      BCrypt::Password.create(string, cost: cost)
    end

    def new_token
      SecureRandom.urlsafe_base64
    end
  end

  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

attr_accessorは何をしているのか

勘のいい方は分かるかもしれませんが、
本来remember_tokenなんて属性はUserモデルにありません。
(あるのはさっきmigrationで作ったremember_digestだけ)

だからremember_token属性に代入できるわけがない。
それをいい感じにやってくれるのがattr_accessor。

attr_accessor :remember_token

こうするとremember_token属性を仮で作ってくれる。
だから存在しないはずのremeber_tokenに代入することができる。

分かりやすい解説↓
Rubyのattr_accessorって何?[和訳]
Railsから入った人へ【attr_accessor】って?
永続cookiesでガチセッションするRailsチュートリアル9章

どうしてUser.digestやUser.new_tokenにはUser.がつくのか

これも素朴な疑問です。
これらはクラスメソッドと呼ばれるもので、今まで定義してきたインスタンスメソッドとは少々異なります。

  • クラスメソッド → クラスオブジェクトからでしか呼び出せない
  • インスタンスメソッド → インスタンスオブジェクトからでしか呼び出せない

例えばよくこんなことをしますよね。

user = User.new

ここでのuserはインスタンスオブジェクト。
この時、↓できるのがインスタンスメソッド。

user.hoge

今のができず、↓するのがクラスメソッド。

User.hoge

どうしてTutorialでは使い分けているのか

User.digestやUser.new_tokenはユーザーオブジェクトが不要です。
つまり暗号化するだけ、トークンを作るだけの処理にユーザ情報は不要
必要なのは作ってからremember_digestで固有のユーザに代入するときだけ。
(だからrememberはインスタンスメソッドです)

それを明示的に示すためにクラスメソッドを使用したのです。

クラスメソッドはクラスそのものの変更や参照する役割にも使用される。
そのような場合にもクラスメソッドを使用する余地がありそう。

分かりやすい解説↓
【Ruby】クラスメソッドとインスタンスメソッドについてザクッと分かりやすく説明してみる
Rubyのクラスメソッドとインスタンスメソッドの例

クッキーに保存する

続いてIDとトークンをクッキーに保存する。
クッキーを保存するcookiesメソッドがあるので簡単。

クッキーの保存期間を20年にするpermanentメソッド、
IDは暗号化していないので暗号化するsignedメソッドも使用する。

cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token

そしてSessions内で保存までを一気に行うメソッドがあると便利。
だからここまで作ってきたメソッドをrememberメソッドとしてSessionsヘルパーに組み込もう。

app/helpers/sessions_helper.rb
module SessionsHelper

  def log_in(user)
  # 中略

  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  def current_user
    # 中略

さっき作ったusers.rbに作ったrememberメソッドとは違う。
メソッド内に入っているuser.rememberこそが、user.rbのrememberメソッドです。

たった今作ったSessionsヘルパーのrememberメソッドの方は、

  • remember_tokenとremember_digestと作成
  • IDとremember_tokenとクッキーに保存

これを一気に行うものなのですね。

クッキーのremember_tokenとUserモデル属性のremember_digestを照らし合わせる

じゃあSessionsコントローラに実装しよう。

def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    log_in user
    remember user
    redirect_to user
  else
    # 中略
  end
end

これでひとまず新規ユーザ作成したらクッキーに保存されるようになった。
ただこのままだとcurrent_user(#8参照)がクッキーを参照しない。

そのためには、

  • 一時セッションの場合
  • 永続セッションの場合

これらを分けた上で処理を書く必要がある。

簡潔にまとめていただいている記事があるので、
35歳だけどRailsチュートリアルやってみた。[第4版 9章 9.1 Remember me 機能 まとめ&解答例]から内容をお借りします。

永続セッションの場合
session[:user_id]が存在すれば、一時セッションからユーザーを取得
cookies[:user_id]が存在すれば、永続セッションからユーザーを取得

if (user_id = session[:user_id])
  # 一時セッションからユーザーを取り出す
elsif (user_id = cookies[:user_id])
  # 永続セッションからユーザーを取り出す
  if # ユーザーが存在し、永続セッションの中の記憶トークンがDBの値と一致する
    # ログイン処理
  end
end

というわけで処理を書いていくわけだけど、

  • 記憶トークンがデータベース内の記憶ダイジェストの値が一致するか

このメソッドがない。
BCryptを参考にして書こう。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

このメソッドの、
* 引数remember_tokenはあくまでただのローカル変数
* 引数remember_digestはUserモデルの属性(始めにmigrationで生成したもの)

なので混同しないよう注意が必要。

準備ができたのでcurrent_userを編集する。

app/helpers/sessions_helper.rb
module SessionsHelper
# 中略
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
# 中略

これで永続セッションのログインが完了になる。

永続セッションからログアウトする

今の状態だと、ログアウトが正しく機能しない。
log_outメソッドが一時セッションにしか対応していないからだ。
実装の手順はこう。

  1. Userモデルにremember_digestを破棄するメソッドを定義
  2. Sessionsヘルパーに実際のログアウトを行うメソッドを定義

というわけで実装しよう。

remember_digestを破棄するメソッドを定義する

remember_digestにnilを代入することでログイン情報を破棄できる。
それをforgetメソッドとして定義。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  def forget
    update_attribute(:remember_digest, nil)
  end
end

実際にログアウトを行うメソッドを定義

Sessionsヘルパーのlog_outメソッドに永続セッションを破棄する処理を追加。
その処理にforgetメソッドを新たに定義する。

app/helpers/sessions_helper.rb
module SessionsHelper
# 中略
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
end

Sessionsヘルパーのforgetメソッドもまた、Userモデルのforgetメソッドと異なるので注意。
(Userモデルのforgetメソッドは、Sessionsヘルパーのforgetメソッドの中のuser.forget)

これで一応ログアウトが可能になる。

細かなバグ修正

現状2つのバグが残っている。

  • 複数のタブで開いたサイトからログアウトが二重に行われる時のエラー
  • 複数のブラウザのうち1つはログアウト、もう1つはログアウトせず終了後再び画面を開く時のエラー

それぞれ対応しよう。

複数タブのエラー対処

エラーの理由:
1度目のログアウトでcurrent_userがnilなのにも関わらず、log_outでforget(current_user)しようと試みるから。

よって、ログインしているか確認した上でログアウトするように変更する。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  # 中略
  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

複数ブラウザのエラー対処

エラーの理由:
1つ目のブラウザでremember_digestをnilしたのにも関わらず、もう1つのブラウザではクッキーが残っているので、authenticated?に例外が起こるから。

よってauthenticated?内でremember_digestがnilなら即座に認証を終了させる。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
  # 中略

rubyの慣習的に、1文で済むif文は処理の後に書く。
よってreturn falseを先に、ifはその後。

これでようやくバグも取り除いた。

次回はリメンバーミー機能を実装

Tutorialは9.2 [Remember me] チェックボックス直前まできた。
次はリメンバーミー機能を実装する。
Tutorialの解説記事になっている?同じ工程だから仕方ない!!
(そのうちポートフォリオ感出てくることを信じて...:relaxed:

前回:#8 ログイン/ログアウト, FactroyBot編
次回:#10 リメンバーミー機能編

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

蟻本の区間スケジューリング問題をrubyで解いてみる(記録用)

問題

image.png

入力例

5
1 2 4 6 8
3 5 7 9 10

回答例

count=gets.to_i
starts=gets.chomp.split(" ").map(&:to_i)
ends=gets.chomp.split(" ").map(&:to_i)
se=[]
kijun=0
answer=0
starts.map.with_index{|item,index|
  se[index]=[item,ends[index]]
}
se.sort_by! { |_, b| b }
se.map {|item|
  if kijun<item[0] then
    kijun=item[1]
    answer=answer+1
  end    
}
puts answer


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

各桁を一桁になるまで掛け算する回数の大きい数をRubyで探す

twitterで話題になってた算数のお題のやつですが、安直に探すとせいぜい9回程度の数(数千万~数億)しか見つけられません。(11回の数を見つけた小学生凄い!)

素因数に注目する

さてどうしたものかと考えると、"各桁の数字を掛ける"という事は1桁の数を掛けていくという事、つまり途中経過は全て1桁の素因数のみを持つ合成数になっているはず(表1)なので、これを探して行くのが速い気がします。

(表1) bruteforceで見つけた9回の数 26888999 を処理して行く様子

数値 素因数分解
0 26888999 51413 * 523
1 4478976 2^11 * 3^7
2 338688 2^8 * 3^3 * 7^2
3 27648 2^10 * 3^3
4 2688 2^7 * 3 * 7
5 768 2^8 * 3
6 336 2^4 * 3 * 7
7 54 2 * 3^3
8 20 2^2 * 5
9 0 0

1桁の素数は2,3,5,7の4つですが、5が混じると一瞬で積が0に収束してしまいがちなので、2,3,7の合成数を探す事にしましょう。
探せる範囲で最大回数となる合成数を見つけ出したら、その合成数の1桁の因数を並べてやる事で最大回数+1の数も見つけられる事になります。

実装

という訳で以下Rubyで書いた探索コード

# 回数算出
def count(n)
  c = 0
  while n >= 10
    m = 1
    while n > 0 && m > 0
      m *= n % 10
      n /= 10
    end
    n = m
    c += 1
  end
  c
end

# 2,3,7の合成数を探索
def search(e2, e3, e7)
  max = 0
  nums = []
  e7.times{|k|
    e3.times{|j|
      e2.times{|i|
        n = (2**i)*(3**j)*(7**k)
        c = count(n)
        if c == max
          nums << [n, i, j, k]
        elsif c > max
          max = c
          nums = [[n, i, j, k]]
        end
      }
    }
  }

  # 一番回数が多かった2,3,7の合成数から回数+1となる数を生成
  nums.sort.each{|n, c2, c3, c7|
    puts "#{max}: #{n} = 2^#{c2} * 3^#{c3} * 7^#{c7}"
    m = '2'*c2 + '3'*c3 + '7'*c7
    m = m.gsub(/222/,'8').gsub(/33/,'9').chars.sort.join.sub(/23/,'6').sub(/22/,'4')
    puts "#{max+1}: #{m}"
  }
end

require 'benchmark'
puts Benchmark::CAPTION, Benchmark.measure {
  # 2^29 * 3^29 * 7^9 までの合成数を探索 (てきとう)
  search(30, 30, 10)
}

結果

10: 4996238671872 = 2^19 * 3^4 * 7^6
11: 277777788888899
10: 937638166841712 = 2^4 * 3^20 * 7^5
11: 27777789999999999
      user     system      total        real
  0.031000   0.000000   0.031000 (  0.037331)

10回となる2,3,7の合成数がサクっと2つ見つかり、11回となる数を2つ生成できました。
(なお探索範囲を2^199 * 3^199 * 7^199まで広げてみても11回以上になる合成数は見つからず、10回の合成数もこの2つ以外は見つかりませんでした。)

他の解法

Pythonで幅優先探索 by @yutasth

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