20200925のRubyに関する記事は29件です。

【Rails6.0】突然CSSが読み込まれなくなった、、【怪奇現象?】

現在個人開発でRailsアプリを作っているのですが、正常に動いていたRailsアプリのCSSが突然読み込まれなくなり(画像ファイルも)無駄に時間を食ってしまいました。
結局はっきりとした原因はわかりませんが、解決するまでの過程をここに書いていこうと思います。

開発環境

  • windows10 Pro
  • Rails: 6.0.3.2
  • ruby: 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
  • Docker for windows
  • MySQL 5.7
  • nginx:1.15.8

エラーが発生した経緯

rspecのシステムスペックでテストを書いて実行、失敗を10回ほど繰り返してlocalhost(root_path)にアクセスしたら
CSSが反映されなくなっていた。
そのときのConsoleのエラー内容↓

Failed to load resource: the server responded with a status of 500 (Internal Server Error) 

このエラー自体はよく見るやつで、タイトル通りCSSファイルが読み込まれていないというものでした。

解決手順(?)

この解決策が正しいのか正直微妙ですが、治るまでの経緯をここに書かせて頂きます。
結論から言いますと、nignx.confの内容を適当に書き換えて、dockerコンテナをビルドし直して起動しなおしてエラーを起こしてもう一度ビルドし直してコンテナ起動したら治ったという感じです。普通に再ビルドしただけでは治りませんでした。これのせいで時間がかかりましたね、、たまたまエラー起こせたんで治せましたが、、
これだとわかる人にしかわからないでの以下で詳しく説明します。

1.nginx.confの内容を書き換える

私の開発環境ではなるべく本番環境に近づけるためにWEBサーバーとしてnginxコンテナを起動させています。
そのnginx.confはその設定ファイルです↓

# プロキシ先の指定
# Nginxが受け取ったリクエストをバックエンドのpumaに送信
upstream webapp {
  # ソケット通信したいのでpuma.sockを指定
  server unix:///webapp/tmp/sockets/puma.sock;
}

server {
  listen 80;
  # ドメインもしくはIPを指定
  server_name webapp ;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  # ドキュメントルートの指定
  root /webapp/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 @webapp;
  keepalive_timeout 5;

  # リバースプロキシ関連の設定
  location @webapp {
    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://webapp;
  }
}

この設定ファイルのproxy_passを適当に書き換えてわざとエラーをおこします。(場所はどこでもいいかも)

2.dockerを再ビルドして起動する。

$ docker-compose down
$ docker-compose up -d --build

-dオプションはバックグランドで実行してくれます。
--buildオプションをつけるとビルドから起動まで一気にやってくれます。

そうすると、

$ docker-compose ps
    Name                  Command               State           Ports
----------------------------------------------------------------------------
webapp_app_1   bundle exec puma -C config ...   Up
webapp_db_1    docker-entrypoint.sh mysqld      Up       3306/tcp, 33060/tcp
webapp_web_1   /bin/sh -c /usr/sbin/nginx ...   Exit 1

エラーが起きて webapp_web_1コンテナがExitするはずです。(コンテナ名は、docker-compose.ymlの設定で変わるので各自置き換えてください。)

3.書き換えたnginx.confを元に戻して再ビルド

ここで適当に書き換えてわざとエラーを起こしたnginx.confのファイルを元に戻します。
そして2を再び実行します。
そしたらなぜか反映されるようになりました!嬉しい!!

最後まで読んでいただきありがとうございます!

正直今回の方法がどういうロジックで治ったのか?そもそも正しいのか?というのはわかりません。もし、わかるかたがいましたらコメントいただけると幸いです。

とりあえず、これをやったら治ったというのは事実です。production環境でプリコンパイル時にCSS読み込みがうまくいかないとかならまだわかるんですが、今回は突然のエラーだったので治すのに時間がかかってしまいました。同じエラーに遭遇している方に、こちらが役に立てばうれしいです!

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

RSpec - 'users validation'のテストコードをレビューしてもらった結果

はじめに

みなさん、こんにちわ!Qiitaの投稿、ruby、rails、rspec初心者の J です!
つい先日、「プロから見た僕のspecはどうなの?」というのが気になり、
RSpec - 'users validation'のテスト を質問として、投稿しました。
そして、rubyやrspecについての動画をyoutubeで配信されていて、
僕の先生である@jnchito さんにコードレビューをしてもらいました!
テストコードは、そこそこ長かったのですが、一つ一つ徹底的に指摘してもらえて、
「僕ならこう書く!」を丁寧に解説してくださいました!
おかげさまで、

「ルールに従ったテストコードがなんなのか」と迷子になり、
なかなかcommitできず、地獄を彷徨っています。

から開放されて今は天にも上ような気分です!、、レビュー頂いて本当にありがとうございます。。泣

@jnchito さんについて

有名なので、みなさん知ってるかもですが、、

めっちゃかっこいいダンディな先生です!!

という訳でこの記事は、僕自信がスペッカーになるためのアウトプットでもありますが、
まだ僕と同じくらいのスキル感の同志たちの参考になるべく、
指摘・解説してもらった内容から再度、自分自身のコードを見返してダメなところをガチガチに直してみました!
よかったら参考にしてみてください!

修正前の僕のゴミコード

前回のコードとのbefore、afterテストコードを載せるとQiitaのデータベースがパンクしてしまうかも ←(・・。)
なので、 修正前の僕のゴミコード を比較する場合は、タブを複製してどうぞ!

Specのネストイメージ

「ネストが深すぎなくていいんじゃないか?」って訳で、なるべく浅く修正!

RSpec.describe User, type: :model do
  describe 'attribute: name'
    context 'when present'
      it 'is valid'
        # ...省略
      end
    end
  end
end

上の内容は、、User > name > when present > it is valid
英訳的にはおかしな感じになりますが、どのネストも、、
「ユーザー名が存在する場合は有効です」的なニュアンスでわかりやすくネストし直しました。
これはあくまでイメージです!

修正後

Userモデル

app/models/user.rb
class User < ApplicationRecord
  before_save :downcase_email

  validates :name,
            presence: true,
            length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email,
            presence: true,
            length: { maximum: 255 },
            format: { with: VALID_EMAIL_REGEX },
            uniqueness: { case_sensitive: false }

  private

    def downcase_email
      self.email = self.email.downcase
    end
end

補足

  • VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    railstutorialより 「2つの連続したドットはマッチさせないようにする」こちらの正規表現に修正しました。
  • self.email.downcase!と、破壊的メソッドはあまり使用しない。
    将来的にいつどこで参照されるか不明なので、破壊的メソッドを使用しない方が無難。
    、、、たしかに。(ということで修正済みが以下。)
def downcase_email
  self.email = self.email.downcase
end

FactoryBot

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'Tony Stark' }
    email { 'tony@example.com' }
  end
end

補足

  • 修正前: name { 'hoge' }email { "foo@bar.com" }
    ありえない思っていたメールアドレスのドメイン部分。。あ、あれ?
    bar.com ポチッ!
    、、、@example.comを使用すると誓います。

Spec

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  let(:user) { FactoryBot.build(:user) }

  describe 'attribute: name' do
    context 'when present' do
      # nameが存在している場合は、有効であること
      it 'is valid' do
        user.name = 'Tony Stark'
        expect(user).to be_valid
      end
    end

    context 'when blank' do
      # nameが空白の場合は、無効であること
      it 'is invalid' do
        user.name = ' '
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end

    context 'when empty' do
      # nameが空の場合は、無効であること
      it 'is invalid' do
        user.name = ''
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end

    context 'when nil' do
      # nameが存在していない場合は、無効であること
      it 'is invalid' do
        user.name = nil
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end

    context 'when length is 50 characters or less' do
      # nameが50文字以下の場合は、有効であること
      it 'is valid' do
        user.name = 'a' * 50
        expect(user).to be_valid
      end
    end

    context 'when length is more than 50 characters' do
      # nameが50文字を超える場合は、無効であること
      it 'is invalid' do
        user.name = 'a' * 51
        expect(user).to be_invalid
        expect(user.errors[:name]).to include('is too long (maximum is 50 characters)')
      end
    end
  end

  describe 'attribute: email' do
    context 'when present' do
      # emailが存在している場合は、有効であること
      it 'is invalid' do
        user.email = 'tony@example.com'
        expect(user).to be_valid
      end
    end

    context 'when blank' do
      # emailが空白の場合は、無効であること
      it 'is invalid' do
        user.email = ' '
        expect(user).to be_invalid
        expect(user.errors[:email]).to include("can't be blank")
      end
    end

    context 'when empty' do
      # emailが空の場合は、無効であること
      it 'is invalid' do
        user.email = ''
        expect(user).to be_invalid
        expect(user.errors[:email]).to include("can't be blank")
      end
    end

    context 'when nil' do
      # emailが存在していない場合は、無効であること
      it 'is invalid' do
        user.email = nil
        expect(user).to be_invalid
        expect(user.errors[:email]).to include("can't be blank")
      end
    end

    context 'when length is 50 characters or less' do
      # emailが255文字以下の場合は、有効であること
      it 'is valid' do
        user.email = 'a' * 243 + '@example.com'
        expect(user).to be_valid
      end
    end

    context 'when length is more than 50 characters' do
      # emailが255文字を超える場合は、無効であること
      it 'is invalid' do
        user.email = 'a' * 244 + '@example.com'
        expect(user).to be_invalid
        expect(user.errors[:email]).to include('is too long (maximum is 255 characters)')
      end
    end

    context 'when correct format' do
      # emailの形式が正しい場合は、有効であること
      it 'is valid' do
        user.email = 'user@example.com'
        expect(user).to be_valid

        user.email = 'USER@foo.COM'
        expect(user).to be_valid

        user.email = 'A_US-ER@foo.bar.org'
        expect(user).to be_valid

        user.email = 'foo.bar@baz.jp'
        expect(user).to be_valid

        user.email = 'foo+bar@baz.cn'
        expect(user).to be_valid
      end
    end

    context 'when is incorrect format' do
      # emailの形式が正しくない場合は、無効であること
      it 'is invalid' do
        user.email = 'user@example,com'
        expect(user).to be_invalid

        user.email = 'user_at_foo.org'
        expect(user).to be_invalid

        user.email = 'user.name@example.'
        expect(user).to be_invalid

        user.email = 'foo@bar_baz.com'
        expect(user).to be_invalid

        user.email = 'foo@bar+baz.com'
        expect(user).to be_invalid
      end
    end

    context 'when already taken' do
      # 同一のemailが既に登録されている場合は、無効であること
      it 'is invalid' do
        FactoryBot.create(:user, email: 'tony@example.com')
        user.email = 'tony@example.com'
        expect(user).to be_invalid
        expect(user.errors[:email]).to include('has already been taken')
      end
    end

    context 'when case insensitive and not unipue' do
      # emailの大文字と小文字を区別せず、一意ではない場合は、無効であること
      it 'is invalid' do
        FactoryBot.create(:user, email: 'tony@example.com')
        user.email = 'TONY@EXAMPLE.COM'
        expect(user).to be_invalid
        expect(user.errors[:email]).to include('has already been taken')
      end
    end

    # emailが小文字で保存されていること
    it 'is saved in lowercase' do
      user.email = 'TONY@EXAMPLE.COM'
      user.save!
      expect(user.reload.email).to eq 'tony@example.com'
    end
  end
end

補足

  • 先生:必要ないデバックコードは消しましょう。
    僕:あ、はい!でもテストしてるときにpryは便利です!
  • 先生:FactoryBotのユーザーを見にいかないといけないのはよくないなぁ
    僕:FactoryBotでユーザーを定義している場合でも、エクスペクテーションを書く前に明確にnameに値を入れます!
  • 僕:contextitの文章を修正しました!
    • context: 'when ~' ~である時をグループ化
    • it: validパターンといinvalidパターンを検証
      イメージはネストイメージの部分の文脈のニュアンスです。
  • 先生:saveの検証まではいらないかなぁ。。
    validationが通ってsaveで落ちることは、滅多にないし、もしあったらrailsのバグか何か。
    僕:、、、なるほど!(削除済み以下。)
    修正前のコードを抜粋: expect(user.save).to be_falsey
  • 先生:ここのテストをまとめよう。
    「nameが存在していない場合は、無効であること」
    • name = ' '(スペースのみ)
    • name = ''(空)
    • name = nil(nil)
    context 'when name is blank' do
      # nameが存在しない場合は、無効であること
      it 'is invalid' do
        user.name = ' '
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")

        user.name = ''
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")

        user.name = nil
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end

僕:ここはなんとなくまとめずに書きたいと思ったのでblankemptynilで区分けしました!

  • 僕:user.valid?と聞いてあげないとエラーメッセージにmessagesが入ってこないと思ってました、、
    ということでuser.valid?と聞かずにexpectをそのまま記載!
  • 先生:正規表現のテスト、ここはいいですね!
    僕:あざっす!
  • 先生:dryを捨てて、変数に格納してあれやこれやしない!
    ロジックを組むと、そのロジックにミスが発生して思わぬ事故になる。。。
  • 先生:uniqueness: { case_sensitive: false }をテストした方がいいのでは?
    僕:(2/2)Qiitaに投稿されていたRSpecのテストコードをレビューしてみたで解説してもらったように、、
    • emailアドレスが小文字の有効なuserを保存。
    • 上で登録したuserの大文字emailをuserのemailに入れ直す。
    • userは無効であること。
    • また、エラーメッセージにhas already been takenが含まれていること。 が以下のコード。
context 'when case insensitive and not unipue' do
  # emailの大文字と小文字を区別せず、一意ではない場合は、無効であること
  it 'is invalid' do
    FactoryBot.create(:user, email: 'tony@example.com')
    user.email = 'TONY@EXAMPLE.COM'
    expect(user).to be_invalid
    expect(user.errors[:email]).to include('has already been taken')
  end
end

まとめ

もっとたくさんの指摘もらいましたが、まだプロの考えに及んでいない自分がいるので、
レビュー&解説して頂いた動画を見直して理解した部分をもっと明確に記載しようと思います!
テスト書き始めの方は、僕と同じで「どこまでテスト」していいのかわからないとなる同志も少なくなさそうです。
それぞれ意見は異なると思いますが、初心者的にズバリ!

'railsのバリデーションを信じて、自分や他の人の作ったロジックをテストしよう!'

ってことですね!
railsの検証をわざわざテストするのは、もったいないと思いました、、
また、先生も言っていた通り、「言い出したらキリがないもの」もあるということを考えれば、
テストポイントを絞る力の方が重要だと思います。
今回のバリデーションに対するテストは、ほぼ100%と言える正解のテストコードが存在すると思うので、
自分やチームメンバーが作ったロジックをテストする未来の練習にはなりそうです!

それからみなさん!

必要でない限り、テストはdryを捨てましょう!

みんながコード読みやすくてハッピーになれるし、
何より、、
TESTが間違ってたら意味ないですからね、、w

では、また!

テストのアウトプット

User
  attribute: name
    when present
      is valid
    when blank
      is invalid
    when empty
      is invalid
    when nil
      is invalid
    when length is 50 characters or less
      is valid
    when length is more than 50 characters
      is invalid
  attribute: email
    is saved in lowercase
    when present
      is invalid
    when blank
      is invalid
    when empty
      is invalid
    when nil
      is invalid
    when length is 50 characters or less
      is valid
    when length is more than 50 characters
      is invalid
    when correct format
      is valid
    when is incorrect format
      is invalid
    when already taken
      is invalid
    when case insensitive and not unipue
      is invalid
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyとGem

Gem(ジェム)

Rubyを用いた開発において使用できるツール(ライブラリ)。
ただしGemは他のGemの機能を使って成り立っているものが多く、単体では使用できないことが多い。

bundler

そこで出番はこの「bundler」
欲しいGemに関連する必要Gemをまとめてインストールしてくれます。超便利。
ちなみにbundlerもGemの一つだったりする。

Gemfile.lock

bundlerでbundle installしたときに、自動生成される情報が記録されているファイル。1つのGemのためにどのGemをどのバーションで合わせてインストールしたか確認できる。

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

クラスとモデル

クラスとは

  • rubyの概念
  • 複数のデータと処理(メソッド)をまとめて入れた箱(オブジェクト指向)。設計図のようなもの。

モデルとは

  • railsの概念 Ruby
  • クラスには違いないが、必ずモデルに対応したテーブル(データベース)を持つ
  • 例えばPostモデルにはpostsテーブル(テーブルには複数のデータが入るため名前は複数形になる) => 逆にモデル名は必ず単数形で命名すること。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(ギリ)20代の地方公務員がRailsチュートリアルに取り組みます【第14章】

前提

・Railsチュートリアルは第4版
・今回の学習は3周目(9章以降は2周目)
・著者はProgate一通りやったぐらいの初学者

基本方針

・読んだら分かることは端折る。
・意味がわからない用語は調べてまとめる(記事最下段・用語集)。
・理解できない内容を掘り下げる。
・演習はすべて取り組む。
・コードコピペは極力しない。

 
ラストじゃ!!!!!最後まで駆け抜けろ!!!!!

 
ラストを飾るBGMはこちら。
My Bloody Valentine "Loveless"
始まりにして終わりみたいな一枚。もはやオチ担当。大音量で脳汁たらしながら聴きましょう。

 

【14.1.1 データモデルの問題(および解決策) 演習】

1. 図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。
→ これって、この時点でコンソール上で実行してもnomethoderrorでいいですよね?following定義してないし。(なんで演習解答まとめにみんな載せてないんだ?)
 想像としては、id:1のユーザーがフォローしているユーザーのidの配列が返される。

 
2. 図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。
→ ユーザー2がフォローしているユーザーのid(ここでは1)を返す。後者はその配列。

 

【14.1.2 User/Relationshipの関連付け 演習】

1. コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。
→ 下記

>> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil>

>> other_user = User.second
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Ms. Jerry Hermann", email: "example-1@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$4n7IPw3AcdhW6IzNuLygIuVLA26qlTNneHXDXIqW0zp...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QNHMG3qKng0pFdQdDfGNMeFZaDiddcT0z3ovdEVcOcn...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil>

>> user.active_relationships.create(followed_id: other_user.id)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  SQL (3.4ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2020-09-23 12:37:03.376561"], ["updated_at", "2020-09-23 12:37:03.376561"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2020-09-23 12:37:03", updated_at: "2020-09-23 12:37:03">

 
2. 先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。
→ 上の解答の一番下に出てます。

 

【14.1.3 Relationshipのバリデーション 演習】

1. リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)
→ GREENでした。

 

【14.1.4 フォローしているユーザー メモと演習】

has_many :配列の名称, through: :テーブル名称, source: :配列の元の集合 …なんかややこしいな。元から呼び名で使う名称の方で定義付けしとけばよかったのでは?疑問。

1. コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。
→ 下記。うまくいってます。

>> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil>

>> other_user = User.second
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Ms. Jerry Hermann", email: "example-1@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$4n7IPw3AcdhW6IzNuLygIuVLA26qlTNneHXDXIqW0zp...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QNHMG3qKng0pFdQdDfGNMeFZaDiddcT0z3ovdEVcOcn...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil>

>> user.following?(other_user)
  User Exists (0.6ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 1], ["id", 2], ["LIMIT", 1]]
=> false

>> user.follow(other_user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (6.4ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2020-09-23 14:03:47.235565"], ["updated_at", "2020-09-23 14:03:47.235565"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ?  [["follower_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, name: "Ms. Jerry Hermann", email: "example-1@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$4n7IPw3AcdhW6IzNuLygIuVLA26qlTNneHXDXIqW0zp...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QNHMG3qKng0pFdQdDfGNMeFZaDiddcT0z3ovdEVcOcn...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil>]>

>> user.following?(other_user)
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 1], ["id", 2], ["LIMIT", 1]]
=> true

>> user.unfollow(other_user)
  Relationship Load (0.2ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2020-09-23 14:03:47", updated_at: "2020-09-23 14:03:47">

>> user.following?(other_user)
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 1], ["id", 2], ["LIMIT", 1]]
=> false

 
2. 先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。
→ 上のとおり。INSERTしたりDELETEしたり。

 

【14.1.5 フォロワー メモと演習】

あーそうか。前節でわざわざthroughとsource使ってたのは、一個のテーブルを別々の側面から扱うためか。それにしてもfollowed_idは分かりにくいけど。

1. コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?
→ 下記

>> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil>
>> other1 = User.second
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Ms. Jerry Hermann", email: "example-1@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$4n7IPw3AcdhW6IzNuLygIuVLA26qlTNneHXDXIqW0zp...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QNHMG3qKng0pFdQdDfGNMeFZaDiddcT0z3ovdEVcOcn...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil>
>> other2 = User.third
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 2]]
=> #<User id: 3, name: "Bernice Rippin", email: "example-2@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$fsftEGHfcujlrAy4h.X2VelOSKNXNDnk71MbkBPOqSA...", remember_digest: nil, admin: false, activation_digest: "$2a$10$4DlqqHWVesXipOA4xC/XAOlA70S8T6PjkH3/T4RAI7M...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil>

>> other1.follow(user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  SQL (0.1ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 2], ["followed_id", 1], ["created_at", "2020-09-23 22:04:23.011442"], ["updated_at", "2020-09-23 22:04:23.011442"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ?  [["follower_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil>]>
>> other2.follow(user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  SQL (0.1ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 3], ["followed_id", 1], ["created_at", "2020-09-23 22:04:33.583218"], ["updated_at", "2020-09-23 22:04:33.583218"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ?  [["follower_id", 3], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil>]>

>> user.followers.map(&:id)
  User Load (0.2ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> [2, 3]

 
2. 上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。
→ 下記

>> user.followers.count
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 2

 
3. user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。
→ SQLは上のとおり。配列作る手間分、時間と負荷がかかるのでは。なのでcountのみの方が良い。

>> user.followers.to_a.count
=> 2

 

【14.2.1 フォローのサンプルデータ 演習】

1. コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。
→ 3~48なんで期待どおり。

>> User.first.followers.count
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 38

 
2. 先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。
→ こちらもOK

>> User.first.following.count
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 49

 

【14.2.2 統計と[Follow]フォーム メモと演習】

メンバールーティング:RESTfulなルーティングに新たなルーティングを追加できる。memberはブロックの形をとる。ユーザーidが含まれたURLを扱う。idを指定しない場合はcollectionを使う。

1. ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では [Unfollow] ボタンが表示されているはずです。さて、/users/1 にアクセスすると、どのような結果が表示されるでしょうか?
→ プロフィールページが表示される

 
2. ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。
→ 両方ともstatsが表示されています。

 
3. Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28で示したテストに追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。
→ 下記。これ調べてみると、@user.active(またはpassive)_relationshipsって書いてるのが出てきたんですが、どっちもいけるんでしょうか?コンソールで試したところ、両方とも同じ結果が返されましたが、relationshipsの方が処理が少ないからいいのかも?

saite_layout_tesr.rb
  test "stats" do
    log_in_as(@user)
    get root_path
    assert_match @user.following.count.to_s, response.body
    assert_match @user.followers.count.to_s, response.body
  end
user_profile_test.rb
  test "profile display" do
    get user_path(@user)
    assert_template 'users/show'
    assert_select 'title', full_title(@user.name)
    assert_select 'h1', text: @user.name
    assert_select 'h1>img.gravatar'
    assert_match @user.following.count.to_s, response.body
    assert_match @user.followers.count.to_s, response.body
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination', count: 1
    @user.microposts.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end
>> user.active_relationships.count
   (0.1ms)  SELECT COUNT(*) FROM "relationships" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 49
>> user.following.count
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 49

 

【14.2.3 [Following]と[Followers]ページ 演習】

1. ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?
→ おーけーでーす

 
2. リスト 14.29のassert_selectに関連するコードをコメントアウトしてみて、テストが正しく red に変わることを確認してみましょう。
→ あれ?REDにならない?…あ、そうか。renderもコメントアウトしなきゃでした。@usersのブロックだけコメントアウトしてた。下記。

show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b><%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <%# @users.each do |user| %>
            <%#= link_to gravatar_for(user, size: 30), user %>
          <%# end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%#= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

 

【14.2.4 [Follow]ボタン(基本編) 演習】

1. ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?
→ うまくいってます!

 
2. 先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?
→ Rendering users/show.html.erb within layouts/application

 

【14.2.5 [Follow]ボタン(Ajax編) メモと演習】

Ajaxの実装:
①form_forにremote: trueを記入。
②コントローラのアクション内にrespond_toメソッドを記入。Ajaxリクエストを受け取る。
③ブラウザ側でJSが無効になっている場合用に、config/application.rbファイルにそれ用の記述を追記。
④アクション名.js.erbにjQueryを記入。
jQuery忘れたなぁ……progateで復習してくるか。

1. ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。
→ うん、動いてます。

 
2. 先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。
→ Rendering relationships/create.js.erb
  Rendering relationships/destroy.js.erb

 

【14.2.6 フォローをテストする メモと演習】

xhr: true を入れるだけでAjaxの動作をテストできる。

1. リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?
→ format.html { redirect_to @user } のところでunknownformatが出ました。

 
2. リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。
→ どこまで削除すればいいのか分からないけど、文法が変にならないように削除すればGREENです。
 JSが無効なときに表示するフォーマットがないからでしょうか。(先ほどの演習というのは、演習1のことなのか、それともこの直近の削除のことをいっているのか、あいまいですね…)

 

【14.3.1 動機と計画 演習】

1. マイクロポストのidが正しく並んでいると仮定して (すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。ヒント: 13.1.4で実装したdefault_scopeを思い出してください。
→ 投稿日時が新しいもの順で、各投稿のユーザーidの配列が返される?(自分自身とフォローしているユーザーのもののみ)

 

【14.3.2 フィードを初めて実装する 演習】

1. リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
→ ("user_id IN (?) OR user_id = ?", following_ids, id) の構成が分かれば解けますね。先の(?)に対応するのがfollowing_ids、後の?に対応するのが、idです。ということで、それぞれセットで消せば望んだエラーが返ってきます。今回は後者のセットを削除すると、# 自分自身の投稿を確認 のコメントアウト以下3行のFAILが返ってきます。

user.rb
  def feed
    Micropost.where("user_id IN (?)", following_ids)
  end

 
2. リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
→ 1の逆。前者のセットを消します。# フォローしているユーザーの投稿を確認 以下の3行のFAILが返ってきます。

 
3. リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。
→ これって、要は全マイクロポストってことですよね。ということで下記。FAILは最後の# フォローしていないユーザーの投稿を確認 です。

user.rb
  def feed
    Micropost.all
  end

 

【14.3.3 サブセレクト 演習】

1. Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。
→下記。assert_matchにお約束のresponse.body、となればマイクロポストの中身が表示されているかどうかをテスト。

following_test.rb
  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(micropost.content), response.body
    end
  end

 
2. リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。
→ うわ、テストしたらえげつない量のエラー文出てきた。sorryで検索すると、「Your words made sense, but your sarcastic tone did not.」=「あなたの言葉は理にかなっていますが、あなたの皮肉な口調はそうではありませんでした。」 ジョークみたいなこと言われてますが、つまりエスケープしないと表示できないものがあるよってことでしょうか。

 

第14章まとめ

・モデルにhas_manyを設定してテーブルの要素を柔軟に名付けて扱える。
・ルーティングはネストして、リソースに新たなものを追加できる。
・jQueryを使ってフォームにAjaxを採用。
・必要に応じてSQLも使える。
・チュートリアルはチュートリアルにすぎない。やっとスタートラインに立てたぐらいだと心得よ(自分への戒め)。

 
 2周半完走です!!ありやした!!!

 ちょうど9月1日から始めて、今日が25日。なんとか目標としていた1ヶ月以内に終わりました。チュートリアルで学んだ内容はだいぶ頭に入ったと思いますが、つどつど疑問点を調べていると、まだまだ知らないこと99%あるんじゃないかって感じます。ゆえにまとめの最後の一文というわけです。というかまだスタートラインに立つためにアップしてるぐらいの感覚です。オレはようやくのぼりはじめたばかりだからな、このはてしなく遠い男坂をよ…

 さて、 Railsチュートリアルについては一旦ここで終了です。次のステージはスクールに通うことです。ここまでやってきて、正直スクールうんぬんよりも、いかに自分で調べられるか、自走力・独学力が大事だなあと実感しました。が、記事タイトルどおり、自分には悠長なことを言っていられる時間はないのです。世の中に挑んでいくために、ぎゅっと集中して背水の陣で学習に臨む必要があるのです。
 というわけで次の一手は、スクールを利用しての学習です。どっちにしろ自分で調べて考えてコードを書いていくことに変わりはないと思いますが、講師に自分のコードをみてもらい必要なことを教えてもらえるのはきっと価値があるし、時間短縮に繋がると考えています。金にモノをいわせる大人のやり方です。たいむいずまねーです。

 とかグダグダ書いてるヒマがあったら勉強しろって話!それではまた!
 記事書くのは好きなので、いつか有益な記事が書けるように精進します!!

 
⇦ 第13章はこちら
学習にあたっての前提・著者ステータスはこちら
 

なんとなくイメージを掴む用語集

・Ajax
 Asynchronous JavaScript + XML の略。ページを移動する・読み込み直すといったことをせずに、ページの内容をいろいろ動かしたりする技術。

・非同期
 データを転送する際に、送信側と受信側のタイミングの一致(同期)を気にせずにデータをやり取りすること。

・XML(Extensible Markup Language)
 拡張できるマークアップ言語。書き方のルールの一つ。主にデータのやりとりや管理を簡単にする目的で使われる。

・DOM(Document Object Model)
 ホバリングしている黒いやつじゃないよ。プログラムからHTML等のWebページを自由に操作するための仕組みで、階層構造(ツリー構造)をとる。

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

ワンタイムパスワードをサーバー側とクライアント側で作るサンプル

ワンタイムパスワードのロジックを作ってみた

2020/09/25 現在では何かと銀行がクラックされまくっているが、いずれもワンタイムパスワードのような認証が入ってない。もちろんワンタイムパスワードも「そのワンタイムの間に」盗まれればアウトなんだけど、常時通信内容を奪うのは盗む方も大変だ。

ただ、ワンタイムパスワードの仕組みが、Google Authenticator などの外部の仕組みに依存するのは、別の意味でリスク(急に仕様が変わるなど)。

なので、自前でどこまでできるかを検証。

どのようにして動く?

以下の要素を組み合わせて、ダイジェスト(sha256)を生成し、ダイジェストから数値6桁を取り出してます。

  • ユーザ毎に違う秘密鍵(クライアントとサーバーで同じ秘密鍵を持ちます)
  • タイムスタンプ(今回のサンプルでは60秒で割った整数部を使います)
  • ソルト(サーバー側で保持する長い文字列)

サンプルソース

端末側

onetime.js
async function digestMessage(message) {
  const encoder = new TextEncoder();
  const data = encoder.encode(message);
  const hash = crypto.subtle.digest('SHA-256', data);
  return hash;
}

function buf2hex(buffer) { // buffer is an ArrayBuffer
  return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}

async function onetimePass(secret_key){
  const ts_min = parseInt( Date.now() / 60000 ).toString();
  const buffer = await digestMessage(secret_key + '_and_sault' + ts_min);
  const hexString = '0x' + await buf2hex(buffer) ;
  const digitString = BigInt(hexString).toString(10);
  return digitString.slice(-6);
}
onetime.html
<html>
<script type="text/javascript" src="./onetime.js"></script>
<body>
<script>
(async () => {
document.write(await onetimePass('secret_key'));
})();
</script>
</body>
</html>

サーバー側

onetime.rb
require 'digest/sha1'

def onetime_pass(secret_key, next_min = false)
  # タイムスタンプを秒から分に丸める
  ts_min = Time.now.to_i / 60
  ts_min += 1 if next_min # 次の分までカバーしたい場合(チェックを受ける側)
  text = "#{secret_key}_and_sault"
  Digest::SHA256.hexdigest("#{text}#{ts_min}").to_i(16).to_s[-6, 6]
end

# ちょうど一分をまたがることを想定して、一分後のパスワードも取得できるようにしてる
puts onetime_pass('secret_key')
puts onetime_pass('secret_key', true)

実行結果

フロント側とサーバー側で同じ6桁ができてることがわかる。
(サーバー側のニ行目は、1分後のパスワード)

image.png

心残り部分

  • 久しぶりにJS書いたら何実行してもPromiseばかり帰ってくる。試行錯誤で, async, await を書きまくったけど、あんまり自身がない。
  • RFCにもちゃんとワンタイムパスワードのロジックはあるらしいが読まずに「きっとこういうことだろう」で済ませてる
  • 端末側はJSで書いちゃったけど、肝心の「秘密鍵を渡す方法」がこの記事では触れられてない。QRコードで渡すのが良さそうだけど、QRコードを読んで端末側のローカストレージに秘密鍵を保存するというプログラムの方がよほど上記のソースよりも長くなりそう。そういうことまで考えると、 Google Authenticator を使い、 Google Authenticator のサーバー側のライブラリを使っちゃうので解決か。
  • sha256でダイジェストを作ったけど、10進数にして最後の6桁を取り出してる時点で、衝突確率は増える。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RuboCopでこんなエラーが出た。Assignment Branch Condition size for search is too high.

なにこのエラー

Assignment Branch Condition size for search is too high. [<10, 21, 5> 23.79/20] 

コミットをする前にこんなエラーが出た。
調べてみると、なげーよ、幅取りすぎだろみたいなエラーらしい。

RuboCopとは。簡単に

RuboCopのGithub
https://github.com/rubocop-hq/rubocop/tree/v0.28.0

このRubCopはコードを検査してくれるものらしくて、コードが長いとか、インデントが変だとかいろんなことを教えてくれる。勝手に直してくれる(不自然なインデントを修正)ときもあれば、警告文だけだして、自分でどうにかしてくれみたいな時もある。ただ、このRuboCopからの指摘も全てが正しい訳ではないっぽい。

 今回のエラーに関して

Assignment Branch Condition size for search is too high. [<10, 21, 5> 23.79/20] 

最初に述べたように、なげーよ的な意味らしい。

実際に指摘を受けたコード

  def search
    if params[:city_id]
      pagy, offices = pagy Office.where(city_id: params[:city_id])
      pagy_headers_merge(pagy)
    elsif params[:keyword]
      keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?)
      pagy, offices = pagy_array([])
      pagy_headers_merge(pagy)
      keywords.each do |keyword|
        offices += Office.where('name LIKE (?) OR
                                 address LIKE (?) OR
                                 near_station LIKE (?) OR
                                 introduction LIKE (?) OR
                                 company LIKE (?)',
                                "%#{keyword}%",
                                "%#{keyword}%",
                                "%#{keyword}%",
                                "%#{keyword}%",
                                "%#{keyword}%")
      end
    else
      pagy, offices = pagy(Office.all)
      pagy_headers_merge(pagy)
    end
    render json: offices, each_serializer: OfficeIndexSerializer, include: '**'
  end

自分でも書いていて、長いなと思った。縦に長い。ただ、今の知識ではどうやってコードをより簡潔なものにすればいいか思いつかなかった。だれか教えてください。

それで結局どうしたか・・・

.rubocop.ymlRuboCopの設定を変更した。
エラー文を見てみると、、、、

Assignment Branch Condition size for search is too high. [<10, 21, 5> 23.79/20] 

[<10, 21, 5> 23.79/20]この部分が、点数を表しているっぽい。これでみると、これだと『MAXスコアが20なのにお前のは23.79だよ』ってことらしく、これをどうにかするしかないと思った。

それで、.rubocop.yml内にある設定を変更した。

どう変更したかというと、、、

Metrics/AbcSize:
  # The ABC size is a calculated magnitude, so this number can be a Fixnum or
  # a Float.
  Max: 15 

このMaxの部分を 25に設定を変更した。そしてコミットすると、RubCopからの指摘を受けずにすんだ。

おわりに

設定変えられるのかー程度に、この記事では感じていただければ、幸いです。

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

gem Pagy(ページネーション)を配列に適用させる書き方

railsのAPIモードでアプリケーションを作る際、ページネーションでPagyというgemを使用しました。
その際、記述の仕方に迷ったのでメモとしてここに残しておきます。

配列に適用させる記述

pagy, offices = pagy_array([])

ふつうに適用させる方法

$ pagy, offices = pagy Office.where(city_id: params[:city_id])
pagy, offices = pagy(Office.all)

HTTPヘッダーに関する記述

pagy_headers_merge(pagy)

これを上の記述の下に追記すると、HTTPヘッダーにページネーションの情報を持たせることができる。

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

【Rails】devise, devise_token_authで、ユーザー作成ログインの初期設定

新規ユーザーモデルを作成する時

deviseの設定

$ rails g devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
routes.rb
Rails.application.routes.draw do

  devise_for :users, :controllers => {
    :registrations => 'users/registrations',
    :sessions => 'users/sessions'
  }

  devise_scope :user do
    get "sign_in", :to => "users/sessions#new"
    get "sign_out", :to => "users/sessions#destroy"
  end
end

devise_for :モデル名認証に必要なルーティングを自動で設定

devise_token_authの設定

$ rails g devise_token_auth:install User auth
      create  config/initializers/devise_token_auth.rb
      insert  app/controllers/application_controller.rb
        gsub  config/routes.rb
      create  db/migrate/20200919181950_devise_token_auth_create_users.rb
      create  app/models/user.rb
$ rails db:migrate

User...モデル名

auth...認証ルーティングをマウントするパス

routes.rb
Rails.application.routes.draw do
  mount_devise_token_auth_for 'User', controllers: {
    registrations: 'users'
  }
end

deviseコントローラ

$ rails g devise:controllers users
      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb

すでにUserモデルある場合

マイグレーション内容をいじる

class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[6.0]
  def change


    change_table(:users) do |t|
      ## Required
      t.string :provider, :null => false, :default => "email"
      t.string :uid, :null => false, :default => ""


      ## Database authenticatable
      t.string :encrypted_password, :null => false, :default => ""


      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
      t.boolean  :allow_password_change, :default => false


      ## Rememberable
      t.datetime :remember_created_at


      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip


      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable


      ## Lockable
      # t.integer  :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      ## User Info
      # t.string :name
      t.string :nickname
      t.string :image
      # t.string :email


      ## Tokens
      t.text :tokens


      # t.timestamps
    end


    add_index :users, :email,                unique: true
    add_index :users, [:uid, :provider],     unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,       unique: true
  end
end

注意すべきことは、2つ

  1. change_table(:users)にすること。(元は、create_table
  2. 既存のカラムはコメントアウトすること
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DBから特定のカラムの数値を取り出したい(自分メモ)

Aテーブル:user
Bテーブル:user_info

目的:
Bテーブルの特定カラム(ここではuser_main_id)の数値を返したい

やり方:
用意されているのは、Aテーブルの@user_sessionのみ。

  1. Aテーブルの@user_sessionの中のidを探し出す。
  2. Aテーブルのidと、Bテーブルのuser_idの数値を一致させる
  3. Bテーブルの中でidで一致した後、目的のカラム(user_main_id)を取得する
Ruby
API表示名: user.find_by(user_id: @user_session.record.id).user_main_id 

@user_sessionの中身が何かが重要!

参考URL
https://qiita.com/tsuchinoko_run/items/f3926caaec461cfa1ca3

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

【ruby】ストロングパラメーター【require】【permit】【merge】使用方法 役割

はじめに

 この記事は、学んだことを復習するために投稿者が理解できる言葉まで落とし込んで書いております。

1.役割

 ストロングパラメーターとは、指定したキーを持つ、パラメーターのみ受け取るように制限をかけることができるものです。

2.使用する理由

 受け取るパラメーターのデータを制限することで、意図しないデータの保存。更新を避けるため。
今回は、商品出品を行うフリマアプリを想定した例で紹介します。

3.基本構成と使用方法

・基本構成は以下の通りです。

# ・・・中略
 def new
    @item = Item.new
  end

 def create
   @item = Item.new(item_params)
 end

 private

 def item_params
   params.require(:item).permit(:name, :explanation, :category_id, :status_id, :delivery_fee_id, :shipping_region_id, :shipping_day_id, :selling_price, :image).merge(user_id: current_user.id)
 end

使用方法

・ストロングパラメーターは、プライベートメゾットとして定義する。
・下記のように、item_paramsとすることで、可読性が向上する。createアクションで保存した後に、データを更新するupdateアクションでも同様の箇所の変更を加えるので、記述をまとめられます!!

 def create
   @item = Item.new(item_params) ←ここです
 end

4.制限する値

4-1.require(モデル名)

# 使用例
params.require(:モデル名)

params.require(:item)

# itemモデルに入っている全てのデータが、保存される。
  指定したモデルの中でも、カラム名で保存したい値を制限したいときに、permitを使用する。

4-2.permit(:キー1,キー2...)

# 使用例
params.permit(:キー1, :キー2....)

params.require(:item).permit(:name, :explanation, :category_id, :status_id, :delivery_fee_id, :shipping_region_id, :shipping_day_id, :selling_price, :image)

# 指定したモデルの中でも、保存したい値を制限したいときに、permitでカラム名を制限する。

4-3.merge(:結合させたい外部キー等)

# 使用例:商品を出品したユーザーの情報を紐付けたい場合
params.merge(user_id: current_user.id)

params.require(:item).permit(:name, :explanation, :category_id, :status_id, :delivery_fee_id, :shipping_region_id, :shipping_day_id, :selling_price, :image).merge(user_id: current_user.id)

注意

current_user.idを使用できるのは、deviseを使っているからです。
参考になる記事がありましたので、共有させていただきます。

Rails deviseで使えるようになるヘルパーメソッド一覧
https://qiita.com/tobita0000/items/866de191635e6d74e392

5.最後に

指摘事項などありましたら、勉強のために教えていただけると幸いです。

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

Pay.JP ローカルではできたのに本番環境では上手く動作しない時の解決法

1週間ほど悩んで解決した
Pay.PJがローカル環境ではしっかり動作するのに本番環境では決済ができない原因を忘れないうちに書いておきます。
Pay.JPの決済がうまくできない原因はエラー文が出ないのでかなり時間がかかりました…

動作環境
ruby 2.6.5
Rails 6.0.3.2
heroku 7.42.13

エラーの原因

エラーが起こっていた原因は簡単にまとめると
Herokuに設定した環境変数が本番環境に反映されていなかった
ことが原因です

どういうことか説明していきます。
ターミナルにて
heroku config:set PAYJP_PUBLIC_KEY='ご自身のテスト公開鍵'

heroku config:set PAYJP_SECRET_KEY='ご自身のテスト秘密鍵'
を入力されるかと思います。
しっかり入力できているかどうかは
heroku config
で確認していただければ確認できます。
確認するとしっかり入っているんです。
入っているんです!!
これに騙されました…
Herokuの環境変数の中身を変えましたがこれまだ適応できていないのです。。

Heroku環境変数の適応の仕方

結論から申します
git push heroku master
を入力してくださいこれでGitHubのマスターがプッシュされて環境変数が適応され本番環境でもしっかりと決済が行われます。
しかし、中には
Everything up-to-date
と、すでに最新の状態ですと帰ってくる方もいらっしゃると思います。自分もそうでした。
git commit --allow-empty -m "空のcommit"
そんなときはこれを入力してGitHubのマスターをもう一度最新の状態にして
git push heroku master
してみてください!
きっと解決できると思います

ご高覧ありがとうございました。

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

Rubyを用いたじゃんけんプログラムとそのテスト(test-unit)

はじめに

Rubyでじゃんけんのプログラムを製作しました。
その際、勝ち負けの判定が正しいかをtest-unitを用いて確認しました。

目次

1.じゃんけんメソッドを書く
2.テストコードを書く
3.じゃんけんプログラムを書く
4.プログラムを実行
5.テストを実行

1.じゃんけんメソッドを書く

初めにじゃんけんのメソッドをクラスにまとめます。
ここでは、標準入出力などは行わずただ戻り値を返すだけです。
このメソッドは部品としてテストコードとじゃんけんプログラムのコードに使用します。

janken_judge.rb
class Janken
  def self.judge(my_hand, enemy_hand)
    hands = {"g" => "グー", "c" => "チョキ", "p" => "パー"}        
    if my_hand == enemy_hand
      "あなたは#{hands[my_hand]}、私は#{hands[enemy_hand]}、引き分けです。"        
    elsif (my_hand == "g" && enemy_hand == "c") || (my_hand == "c" && enemy_hand == "p") || (my_hand == "p" && enemy_hand == "g")
      "あなたは#{hands[my_hand]}、私は#{hands[enemy_hand]}、あなたの勝ちです。"        
    else
      "あなたは#{hands[my_hand]}、私は#{hands[enemy_hand]}、あなたの負けです。"        
    end
  end
end

2.テストコードを書く

test-unitを用いて全てのパターンで判定が正しいか確認するテストコードを書いていきます。

test_janken.rb
# 'test/unit'とテストするメソッド部品をrequire 
require 'test/unit'
require_relative 'janken_judge.rb'

# Test::Unit::TestCaseを継承したclassを作成。名前がtest_で始まるメソッドを作成。
class TestJanken < Test::Unit::TestCase
  def test_janken
# テストを書く。assert_equal 期待される値A, テスト対象B、A = Bであればテスト通過。今回は全ての手の組み合わせと、勝ち負けの結果を記入。
    assert_equal 'あなたはグー、私はグー、引き分けです。', Janken.judge("g", "g")
    assert_equal 'あなたはチョキ、私はチョキ、引き分けです。', Janken.judge("c", "c")
    assert_equal 'あなたはパー、私はパー、引き分けです。', Janken.judge("p", "p")
    assert_equal 'あなたはグー、私はチョキ、あなたの勝ちです。', Janken.judge("g", "c")
    assert_equal 'あなたはチョキ、私はパー、あなたの勝ちです。', Janken.judge("c", "p")
    assert_equal 'あなたはパー、私はグー、あなたの勝ちです。', Janken.judge("p", "g")
    assert_equal 'あなたはグー、私はパー、あなたの負けです。', Janken.judge("g", "p")
    assert_equal 'あなたはチョキ、私はグー、あなたの負けです。', Janken.judge("c", "g")
    assert_equal 'あなたはパー、私はチョキ、あなたの負けです。', Janken.judge("p", "c")
  end
end

1.まずは'test/unit'と先に書いた'janken_jedge.rb'をrequireします。

2.Test::Unit::TestCaseを継承したclassを作成します。
名前がtest_で始まるメソッドを作成します。ここでは、test_jankenとしました。

3.作成したtest_jankenメソッドにテストを書いていきます。
assert_equalを用いて、期待される値とテスト対象の値が一致するかで判定しています。
今回は全ての手の組み合わせと、勝ち負けの結果で判定を行いました。

3.じゃんけんプログラムを書く

最後に実際に実行するメインロジックであるじゃんけんプログラムを書いていきます。
このプログラムを実行すると、標準入力の待ち状態になって、ユーザーは g,c,pのいずれかを入力します。コンピュータはランダムにグー、チョキ、パーを出し、勝ち負けを判定して、 以下のように出力されます。
例) あなたはチョキ、私はパー、あなたの勝ちです。

janken.rb
# 先に作成したメソッド部品をrequire
require_relative 'janken_judge.rb'

# ユーザーはg,c,pいずれかの手を入力
my_hand = gets.chomp
# コンピュータはランダムな手を出す。
enemy_hand = ["g","c","p"].sample

# じゃんけんの判定が出力される。
puts Janken.judge(my_hand, enemy_hand)

4.プログラムを実行

それでは実行してみます。

% ruby janken.rb 
g
あなたはグー、私はパー、あなたの負けです。

5.テストを実行

上の結果より、じゃんけんプログラムは正しく動いているように思えます。全パターンで正しく判定できているのかを確認するためにテストを実行してみましょう。

% ruby test_janken.rb 
Loaded suite test_janken
Started
.
Finished in 0.001524 seconds.
--------------------------------------------------------------------------------
1 tests, 9 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
--------------------------------------------------------------------------------

9 assertions, 0 failures, 100% passedと出ましたので、全パターンでテストを通過したことが確認できました。

それでは間違ったコードを書いていた場合も試してみます。janken_judge.rbのelseの後の行の、「あなたの負けです。」を「あなたの勝ちです。」に変えてから、テストを実行してみます。

 % ruby test_janken.rb
Loaded suite test_janken
Started
F
================================================================================
     12:     assert_equal 'あなたはグー、私はチョキ、あなたの勝ちです。', Janken.judge("g", "c")
     13:     assert_equal 'あなたはチョキ、私はパー、あなたの勝ちです。', Janken.judge("c", "p")
     14:     assert_equal 'あなたはパー、私はグー、あなたの勝ちです。', Janken.judge("p", "g")
  => 15:     assert_equal 'あなたはグー、私はパー、あなたの負けです。', Janken.judge("g", "p")
     16:     assert_equal 'あなたはチョキ、私はグー、あなたの負けです。', Janken.judge("c", "g")
     17:     assert_equal 'あなたはパー、私はチョキ、あなたの負けです。', Janken.judge("p", "c")
     18:   end
test_janken.rb:15:in `test_janken'
<"あなたはグー、私はパー、あなたの負けです。"> expected but was
<"あなたはグー、私はパー、あなたの勝ちです。">

diff:
? あなたはグー、私はパー、あなたの負けです。
?                         勝ち      
Failure: test_janken(TestJanken)
================================================================================

Finished in 0.01695 seconds.
--------------------------------------------------------------------------------
1 tests, 7 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
0% passed
--------------------------------------------------------------------------------

「"負け"と出てほしいのに"勝ち"になってしまってますよ」と教えてくれました。
しっかりとテストが動いてくれていることが分かりました。

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

form_withによるname属性とid属性の自動付与

動作環境
Ruby 2.6.5
Rails 6.0.3.2

form_withを使った場合、name属性とid属性が自動で付与されていたことに気づかずにエラーをよく起こしていたので、投稿してみました。

form_withにmodelを指定し、name属性とid属性が自動で付与される具体例

new.html.erb
<%= form_with model: @hoge, local: true do |f| %>
  <%= f.text_field :fuga %>
  <%= f.submit "投稿する" %>
<% end %>

上記のように、modelを@hogeに指定し、検証ツールにてname属性とid属性を確認すると、name="hoge[fuga]"、id="hoge_fuga"となります。nameとidを指定していないのに、自動でname属性とid属性が付与されています。

先ほどのコードでは、name属性とid属性を指定しない場合は自動で付与されるという話でしたが、name属性とid属性を指定した場合はどうなるのかを見ていきましょう。

form_withを使いname属性とid属性を指定した具体例

new.html.erb
<%= form_with model: @hoge, local: true do |f| %>
  <%= f.text_field :fuga, name:"hogera", id:"piyo" %>
  <%= f.submit "投稿する" %>
<% end %>

先ほどと同様に、検証ツールにてname属性とid属性を確認すると、name="hogera"、id="piyo"となります。つまり、name属性とid属性は指定した通りになります。

次に、modelを指定するのではなく、urlを指定した場合はどうなるのかを見ていきましょう。

form_withにurlを指定し、name属性とid属性が自動で付与される具体例

new.html.erb
<%= form_with url: hoges_path, local: true do |f| %>
  <%= f.text_field :fuga %>
  <%= f.submit "投稿する" %>
<% end %>

先ほどと同様に、検証ツールにてname属性とid属性を確認すると、name="fuga"、id="fuga"となります。ここで気を付けたいのが、modelを指定した場合とname属性とid属性が異なっていることです。基本的に、controllerで行いたいactionが同じの場合、modelを指定してもurlを指定しても結果は変わらないのですが、name属性とid属性は異なります。

ちなみに、modelとurlの両方を指定すると、name属性とid属性はmodelを指定した場合と同じになります。

私はname属性とid属性は指定しないと付与されないものだと思っていたので、このことを知ったときには驚きました。また、JavaScriptを用いてname属性を取得することがあったのですが、その際に、modelを指定した時とurlを指定した時のname属性の違いに気づかずに何時間もエラーと闘ってしまうということがありました。

こういったエラーを防ぐために最初からname属性もid属性も指定してあげれば良いのでは?と考えたのですが、そもそも少しでも記述量を減らすために自動で付与しているので、自分で指定せずに自動で付与されたものを使うことが多いようです。

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

【初学者向け】Rubyにおける○○.△△(ActiveRecordメソッド、インスタンスメソッド、データ取得)

この記事を書いた背景

Rubyを学習している中で、○○.△△のパターンのコード(例: User.new)をたくさん見ました。
それぞれ使われ方が違うのではないかと思って調べたら、3つの使われ方のパターンが有りました。
ということで、今回は○○.△△のパターンをまとめました。

対象読者

Rubyを学習中の駆け出しエンジニアさん

実施環境

Ruby 2.6.5
Rails 6.0.3.3

3つのパターンがありました

①モデル(クラス)名.ActiveRecordメソッド名

モデルに対するテーブル操作で用いられます。
下の例では、『Tweet.all』、『Tweet.new』、『Tweet.find(params[:id])』の部分です。

class TweetsController < ApplicationController
  def index
    #Tweetモデル(Tweetsテーブル)の全データ取得
    @tweet = Tweet.all
  end


  def new
    #Tweetモデルのインスタンス生成
    @tweet = Tweet.new
  end

  def edit
    #Tweetモデル(Tweetsテーブル)のある1つのデータ(1レコード)取得
    @tweet = Tweet.find(params[:id])
  end
end

ActiveRecordメソッドについては、こちらの記事をご覧ください。とても分かりやすかったです。
https://qiita.com/ryokky59/items/a1d0b4e86bacbd7ef6e8

②インスタンス(変数)名.インスタンスメソッド名

インスタンスにメソッドをくっつけて使います。
インスタンスメソッドはインスタンスを生成しないと使えない。
下の例では、『number.calc(1,2)』の部分です。

class Number
  def calc(a, b)
    puts a + b
  end
end

number = Number.new()
number.calc(1,2)

③変数名.属性名

変数に格納された属性値を取得するときに用いられます。
下の例では、『user.nickname』の部分です。
Userテーブルのnicknameカラムのデータを取得します。

class UsersController < ApplicationController
  def show
    #Userモデル(Usersテーブル)のある1つのデータ(1レコード)取得し、変数userに代入
    user = User.find(params[:id])
    #変数userのnicknameカラムの情報(属性値)を取得し、インスタンス変数@nicknameに代入
    @nickname = user.nickname
  end
end

以上になります。参考になれば幸いです。

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

[Rails]simple_calendarの導入方法

はじめに

アプリ開発でカレンダーを用いた実装をしたかったので簡単にまとめました。

simple_calendarとは

simple_calendarとは、簡単にカレンダー機能を付け加えれるgemです。
月間カレンダー、週間カレンダーなど日付指定をしてカレンダーを作成することができます。
今回は月間カレンダーを用いた方法となっております。

目次

  1. simple_calendarのインストール
  2. simple_calendarのビューの生成
  3. カレンダーを表示
  4. カレンダーのレイアウトの変更
  5. おまけ

1. simple_calendarのインストール

gemファイルに以下を追記し、アプリケーションのディレクトリで「bundle install」を実行します。

gem.file
gem "simple_calendar", "~> 2.0"

2. simple_calendarのビューの生成

simple_calendarのビューファイルを生成するために、以下のコマンドを実行します。
レイアウトをカスタマイズしたいときはこのコマンドでファイルを生成することで編集ができるようになります。

ターミナル
rails g simple_calendar:views

3. カレンダーを表示

モデルの編集

カレンダーにイベントを表示させるために以下をモデルに追記します。
「date」の部分はカラム名を記述します。

model.rb
def start_time
  self.date
end

ビューファイルの編集

カレンダーを表示させるために、以下を記述します。
「events」の部分は、コントローラーで設定したインスタンス変数を置くことでデータを引っ張ってきます。
これでカレンダー上にイベントを表示させることができます。

view.html.erb
<%= month_calendar events: @all do |date, all| %>
  <%= date.day %> //カレンダー上の日程の表示の仕方
  <% all.each do |i| %>
    <div>
      <%= i.price %>
    </div>
  <% end %>
<% end %>

<%= date %>だと2020-01-01のように出力されます。
今回は日付だけを表示させたいので<%= date.day %>と記述しています。

その他ファイルの編集

simple_calendarのCSSを適用させるために、application.cssに「*= require simple_calendar」を追記します。

/app/assets/stylesheets/application.css
/*
 *= require simple_calendar #ここに追記します
 *= require_tree .
 *= require_self
 */

4. カレンダーのレイアウトの変更

以下のようにファイルを作成しCSSを自分で記述することで、カレンダーのデザインをカスタマイズすることができます。

/app/assets/stylesheets/_simple_calendar.scss
.simple-calendar {
  .day {}

  .wday-0 {}
  .wday-1 {}
  .wday-2 {}
  .wday-3 {}
  .wday-4 {}
  .wday-5 {}
  .wday-6 {}

  .today {}
  .past {}
  .future {}

  .start-date {}

  .prev-month {}
  .next-month { }
  .current-month {}

  .has-events {}
}

5. おまけ

inputタグでカレンダーを使いたいときは以下のように 「f.date_field」で表示させることができます。

view.html.erb
<%= form_with model: @income, local: true do |f| %>
  <%= f.date_field :date, id:"date" %>
<% end %>

参考リンク

https://qiita.com/isaatsu0131/items/ad1d0a6130fe4fd339d0
https://github.com/excid3/simple_calendar

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

【Rails6】Devise+SNS認証で登録&ログイン(複数連携可)

はじめに

複数のSNSと連携可能な認証をつくります。
Deviseは実装済みとして進めます。
各APIキーの取得等にも触れませんので適宜お調べ下さい。
今回はfacebookとtwitterで実装しますが他のSNSでも基本的には同じだと思います。

環境

環境
Windows10
ruby 2.6.6
Rails 6.0.3.1

下準備

gem追加
Gemfile
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-twitter'

保存後bundle installして下さい。

credentials.ymlにAPIキーを記述

Windowsの方は前回の記事を参考にしてみてください。

credentials.yml
facebook:
  api_key: pk_test_~
  secret_key: sk_test_~
twitter:
  api_key: pk_test_~
  secret_key: sk_test_~
Deviseの設定

config/initializer内。

devise.rb
#以下を追加
config.omniauth :facebook, Rails.application.credentials.facebook[:api_key], Rails.application.credentials.facebook[:secret_key], scope: 'email', info_fields: 'email,name'
config.omniauth :twitter, Rails.application.credentials.twitter[:api_key], Rails.application.credentials.twitter[:secret_key], scope: 'email', callback_url: 'https://localhost:3000/users/auth/twitter/callback'

twitterの方はコールバックURLを明示する必要があるみたいです。

Socialモデルを作成

今回はDeviseでUserモデルを作成済みとします。
Userモデルへカラムを追加してもいいですが、今回はUserモデルに紐づくSocialモデルを新たに作成します。
(UserモデルとSocialモデルは一対多の関係となります。)

console
rails g model social

Socialモデルにはuser_id、provider("facebook"や"twitter"などが入る)、uid(各SNSアカウントと紐づけるためのidが入る)を持たせます。

2020~create_socials.rb
class CreateSocials < ActiveRecord::Migration[6.0]
  def change
    create_table :socials do |t|
      t.references :user, null: false, foreign_key: true
      t.string :provider, null: false
      t.string :uid, null: false
      t.timestamps
    end
  end
end

保存したらrails db:migrateする。

関連付け
user.rb
has_many :socials, dependent: :destroy#追加
devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, 
          :omniauthable#追加
social.rb
belongs_to :user
validates :uid, uniqueness: {scope: :provider}#同一provider内で多重登録できないようにする

同じSNSアカウントが複数のユーザーに紐付かないようにします。
「{scope: :provider}」は多分なくても平気だと思いますが念のため。

実装

コールバック処理をつくる
routes.rb
devise_for :users, controllers: {
  sessions: 'users/sessions',
  password: 'users/password',
  registrations: 'users/registrations',
  omniauth_callbacks: 'users/omniauth_callbacks'#追加
  }

以下app/controllers/users/omniauth_callbacks_controller.rb内。
(rails routes等で確認し、この中のメソッドを各リダイレクト先に設定して下さい。)

omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

 def facebook
    if request.env['omniauth.auth'].info.email.blank?#Facebookでメール使用を許可しているか
      redirect_to '/users/auth/facebook?auth_type=rerequest&scope=email'
    end
    callback_from :facebook
  end

  def twitter
    callback_from :twitter
  end


  private

  def callback_from(provider)
    provider = provider.to_s

    if user_signed_in?
      @user = current_user
      User.attach_social(request.env['omniauth.auth'], @user.id)#後でattach_social作る。SNSからの情報とログイン中のUserのidを渡す。
    else
      @user = User.find_omniauth(request.env['omniauth.auth'])#後でfind_omniauth作る。SNSからの情報を渡す。
    end

    if @user.persisted?#登録済みor登録できたら
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end
end

リダイレクト元のSNSの種類を「callback_from」へ渡して切り分けます。
「callback_from」内では「SNS認証での新規ユーザー登録またはSNS認証済みユーザーのログイン」か「ログイン中ユーザーのSNS連携」かを切り分けました。
「登録後」「ログイン後」「連携後」の処理は今回は分けずに、全てユーザーページにリダイレクトさせています。

次にUser.rb内に各処理を書きます。

user.rb
 def self.find_omniauth(auth)#SNS認証での新規登録またはsnsログイン
    social = Social.where(uid: auth.uid, provider: auth.provider).first
    unless social.blank?#sns認証済み(ログイン)
      user = User.find(social.user.id)
    else#sns認証での新規登録
      temp_pass = Devise.friendly_token[0,20]#今回は取り敢えずランダムなパスワードを作ります
      user = User.create!(
        username: auth.info.name,
        email: auth.info.email,
        password: temp_pass,
        password_confirmation: temp_pass,
      )
      social = Social.create!(
        user_id: user.id,
        provider: auth.provider,
        uid: auth.uid,
      )
    end

    return user
 end

 def self.attach_social(auth, user_id)#sns連携追加時
    social = Social.create!(
      user_id: user_id,
      provider: auth.provider,
      uid: auth.uid,
    )
  end

「SNS認証での新規ユーザー登録またはSNS認証済みユーザーのログイン」時はさらに「新規登録」か「ログイン」かを切り分けます。
「新規登録」時はUserとSocialを両方作成し(同時にUserとSocialの紐づけもする)、Userを返します。
「ログイン」時はSNSから送られてきた情報(auth.uidとauth.provider)からSocialを探し、紐づくUserを返します。
「ログイン中ユーザーのSNS連携」時はSocialのみ作成します。ログイン中のUserと紐づけるため、user_idを受け取ってます。

一応これで処理は終わりです。あとは「user_facebook_omniauth_authorize_path」などでリンクを作成すれば、Userの状況に応じて新規登録やログイン、連携を行ってくれます。

おしまい

実際には連携したSNSへのリンクを作ったり、SNSでの新規登録時のパスワード再設定の仕組み、Deviseのメール認証、連携の解除ボタンなどもあったほうが良いかと思いますが、ひとまず最小構成で作ってみました。
Rails勉強中なのでスマートでない所が多々あるかと思います。もしおかしなことをしてたら教えて頂けると嬉しいです!

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

Google Cloud Vision APIをrailsに導入する際、ドキュメント通り進めてみた。

環境

・Rails 6.0.3.3
・ruby 2.7.1

参考資料

Google Cloud Vision APIドキュメント
1、https://cloud.google.com/vision/docs/libraries?hl=ja

導入手順

1、gemインストール

gemfileに記述

source "https://rubygems.org"
gem "google-cloud-vision"

そのあとに、

bundle installl

※公式では「gem install google-cloud-vision」の手段での方法を推奨されているが、私のappではうまくいきませんでした。
後ほど出てくる

require "google/cloud/vision"

という記述でエラーが発生て詰まった。(これのせいで凄く時間食った)

2、認証の設定

Google Cloud Vision APIを利用するために専用のjsonファイルを作成し、対象のrailsappに読み込ませる必要がある。
・jsonファイルを作成
▶︎ドキュメント通りの手順で詰まることはない。出来上がったファイルはこんな感じ

{
  "type": "",
  "project_id": "",
  "private_key_id": "private_key_idの中身",
  "private_key": "-----BEGIN PRIVATE KEY-----の中身\=\n-----END PRIVATE KEY-----\n",
  "client_email": "",
  "client_id": "",
  "auth_uri": "",
  "token_uri": "",
  "auth_provider_x509_cert_url": "",
  "client_x509_cert_url": ""
}

・railsappに読み込ませる
▶︎.bash_profileに入力してパスを通す

export GOOGLE_APPLICATION_CREDENTIALS="$PATH:パスを書く/ファイル名.json"

3、実際に使ってみる。

routes.rb
get  "contents/index"  => "contents#index"
contents_controller.rb
class ContentsController < ApplicationController
  def index
  end
end
index.erb
<%=
# さっきインストールしたgemの読み込み
require "google/cloud/vision"

# インスタンス化
image_annotator = Google::Cloud::Vision.image_annotator

# 画像のパスを入力(ローカルでもネット上の画像でも問題ない)
file_name = "./resources/cat.jpg"

# 画像を認識した後に返す返り値
response = image_annotator.label_detection image: file_name
response.responses.each do |res|
  puts "Labels:"
  res.label_annotations.each do |label|
    puts label.description
  end
end
%>

4、contents/indexリダイアルすると…

スクリーンショット 2020-09-25 13.25.09.png

こんな感じの返り値が表示されてたら成功です!
(試しに猫の画像を取り込んだので「Cat」が出てますね。笑)

ここから、自分が欲しい情報を取得し整形することで、よしなに利用できます!
読んでくださりありがとうございました!!

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

Railsでアプリを作るためのチュートリアル1(Hello Railsまでの最初の手順)

新規アプリケーションの立ち上げ

rails _railsのバージョン_ new アプリ名 -d 使用するdb(mysqlなど)

データの保存形式の変更

config.database.yml
encoding: utf8mb4
encoding: utf8 #<=変更

データベースの作成

rails db:create

サーバーの起動

rails s

ローカルホストにアクセス
「Yay! You're on Rails!」

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

Railsでアプリを作るためのチュートリアル1(Yay! You're on Rails!までの最初の手順)

新規アプリケーションの立ち上げ

rails _railsのバージョン_ new アプリ名 -d 使用するdb(mysqlなど)

データの保存形式の変更

config.database.yml
encoding: utf8mb4
encoding: utf8 #<=変更

データベースの作成

rails db:create

サーバーの起動

rails s

ローカルホストにアクセス
「Yay! You're on Rails!」

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

FizzBuzz問題を解いてみよう!

【概要】

1.結論

2.FizzBuzz問題とは何か

3.どのようにプログラムするのか

4.ここから学んだこと

1.結論

eachメソッドとif..elsifを組み合わせよう!

2.FizzBuzz問題とは何か

結論としては、コードが書けないプログラマー志願者を分けるために作られたプログラムです。

具体例としては、
(i)1~100の数字を出力する際に、
(ii)3の倍数は”Fizz"
(iii)5の倍数は"Buzz"
(iv)15の倍数は"FizzBuzz"
を表示させる

というプログラムです!
基本的なことが理解できているかを試せる問題として有名です!

3.どのようにプログラムするのか

今回は使用する言語は"Ruby"になります。

def fizz_buzz
  num = 1 #---❶
   (1..100).each do |i| #---❷
    if num % 15 == 0 #---❸
      puts "FizzBuzz"
    elsif num % 3 == 0
      puts "Fizz"
    elsif num % 5 == 0
      puts "Buzz"
    else
      puts num #---❹
    end

    num = num + 1 #---❺
  end
end

fizz_buzz

❶まずここでnumに"1"を代入しないとどの変数のどの値からか、わかりません。今回は1~100なので"1"を代入しています。

❷"2.FizzBuzz問題とは何か"で説明した(i)を満たすには、繰り返し処理+条件が必要です。ここは"4.ここから学んだこと"に後述しますが、each以外のメソッドでもコーディングできます。

❸"2.FizzBuzz問題とは何か"で説明した(ii)~(iv)を満たすための条件式です。倍数の場合分けは、剰余演算子を利用して3/5/15の倍数を判別しています。注意としては3の倍数の条件式からコーディングしてしまうと、"15"の数字が出た際に"3の倍数"の条件で認識されてしまいます。プログラムは上から下に読み込まれるのが基本なので、一度条件に当てはまると以降のプログラムは無視されます。
剰余の書き方については自分の記事でも紹介しているので、探す手間が省けます!
剰余とべき乗(冪乗)の演算子

❹"2.FizzBuzz問題とは何か"で説明した(i)で"1~100"を表示させるので倍数以外の条件も出力します。なので条件以外数字はそのままの数字を出力するようにしています。

❺これがないと、”1”以降の数字を生み出せません。eachに1~100と書いてあっても"1"を100回出力する繰り返し処理になってしまいます。"num += 1" でもOKです。


4.ここから学んだこと

❷の部分は
繰り返し処理メソッドと条件の式(100以下)を組み合わせればいけるのではと思いwhileメソッド使ったところ動きました。

while num <= 100 do

いろいろ検索していたところ、
繰り返し処理は下記URLのようにたくさんの方法がありました!自分が知っていたのはtimes/each/whileメソッドでした。また条件をどう組み込むかもメソッドによって違います!

参考にしたURL:
while文
いろいろな方法で1から100までを出力する


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

scanメソッド問題

問題

任意の文字列で
"hi"がいくつあるか数えてその数を出力するメソッドを作りましょう。

出力例

count_hi('abc hi ho') → 1
count_hi('ABChi hi') → 2
count_hi('hihi') → 2

scanを使う

scanは、対象の要素から引数で指定した文字列を検索して配列として返すメソッドです。

scanの使い方

正規表現と使う場合がほとんどですが、正規表現でなくても文字列を検索して配列として返したい場合にも使うことができます。

記述方法は、オブジェクト.scan(正規表現)という書き方をします。

問題の回答

qiita.rb
def count_hi(str)
  puts str.scan("hi").length
end

引数strにscanメソッドを使用する。
今回は"hi"がいくつあるかを数えたいので、文字列の長さと配列の長さを調べるためのメソッドであるlengthメソッドを用いて出力する。

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

【Ruby on Rails】パラメーターのid取得がうまくいかなかった時

目標

id情報を取得する。
パラメーター関連でうまくいかないときの解決手順の参考になれば幸いです。

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina
google chrome

前提

※ ▶◯◯ を選択すると、説明等が出てきますので、
  よくわからない場合の参考にしていただければと思います。

今回はパラメーターのid取得で躓いてしまったため、
備忘録として記載させていただきます。

状況

  • 投稿機能は実装済み
  • 削除機能の実装を試みる
  • コントローラー、ルーティング、viewそれぞれ記述
  • destroyがnilになる。

これらを解決していきます。

controllerを疑う

app/controllers/img_searchs_controller.rb
  def destroy
    # binding.pry
    @img_searchs = current_customer.img_searchs
    @img_search = @img_searchs.find_by(id: params[:id])
    unless @img_search
      redirect_to new_img_search_path
    end
    if @img_search.destroy
      redirect_to request.referer
    else
      render :new
    end
  end

binding.pryを記述し確認するも、
@img_search = @img_searchs.find_by(id: params[:id])がnilになってしまう。
記述自体は問題なさそうだが、idが渡せていない。
paramsを確認するとid='new'になっている。
(※後述しますがここをしっかり確認できていれば解決は早かったです)
→viewの記述がおかしい可能性あり。

viewを疑う

app/views
<% @img_searchs.each do |img_search| %>

...

<%= link_to "削除", new_img_search_path(img_search), method: :delete %>
<% end %>

each文の中で変数自体も合っており、記述ミスもなさそう。
ただし、今までdestroyメソッドを使ったときには、
img_search_path(img_search), method: :delete
このようにnewがない状態であったため、少し違和感。→ルーティングを確認。

routesを疑う

config/routes.rb
resources :img_searchs, only: [:create, :destroy, :new]

合っている。。。
念の為下記も実行。

ターミナル
$ rails routes
new_img_search GET     /img_searchs/new(.:format)
                public/img_searchs#new
          DELETE  /img_searchs/:id(.:format) 
                        public/img_searchs#destroy

やはり合っている。。。
もう一度viewを確認するもやはり記述自体はあっている。
しかし、画面のカーソルを削除リンクに合わせると。。。
(google chromeでは左下にリンク先のURLが表示されます)

localhost:3000/img_searchs/new.15

/img_searchs/:idの形になっていない!
この時はなぜnewが入るのか全くわかりませんでした。

ただ先程の違和感は解消しておかなければと思い、
あまりきれいな記述ではないものの、ルーティングを書き換えることにしました。

config/routes.rb
resources :img_searchs, only: [:create, :new]
delete 'img_searchs/:id' => 'img_searchs#destroy'

そしてもう一度実行すると

ターミナル
$ rails routes
new_img_search GET     /img_searchs/new(.:format)
                public/img_searchs#new
          DELETE  /img_searchs/:id(.:format) 
                        public/img_searchs#destroy

結果は全く同じでした。
なので、asでパスを指定してみることに。

config/routes.rb
resources :img_searchs, only: [:create, :new]
delete 'img_searchs/:id' => 'img_searchs#destroy', as: 'img_search'

そしてもう一度実行すると下記のようにエラー発生。
内容としては既にそのパス名は使われてますよ。とのこと。

ターミナル
$ rails routes
rails aborted!
ArgumentError: Invalid route name, already in use: 'img_search' 
You may have defined two routes with the same name using the `:as` option, or you may be overriding a route already defined by a resource with the same naming. For the latter, you can restrict the routes created with `resources` as explained here:
...

そんな馬鹿な!と思い調べてみると、
別のページのURLでas: 'img_search'を使っていました。
これが今回の原因です。
そのため下記のようにパス名を変更。

config/routes.rb
resources :img_searchs, only: [:create, :new]
delete 'img_searchs/:id' => 'img_searchs#destroy', as: 'img_search_destroy'

その後、viewのパス名を変更。

app/views
<% @img_searchs.each do |img_search| %>

...

<%= link_to "削除", img_search_destroy_path(img_search), method: :delete %>
<% end %>

正常に動作しました。

まとめ

今回は手早く修正する必要があったため、
既にあったas: 'img_search'を変更するのではなく、
新しく追加した as: 'img_search'を as: 'img_search_destroy'に変更しました。

また今回起こってしまった要因としては、
私自身がroutes.rbの優先順を忘れてしまっていたがために、起こってしまったので、
同じエラーが起きた人の解決の一助になれたら幸いです。

ちなみにroutes.rbの優先順は上に記述したものが優先されます。

エラー解決の方法はいくつもあるため、一歩一歩焦らず蓄えていく必要がありそうです。

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

【Rails】on: :actionの条件付きで設定したコールバックはskip_callback、set_callbackを用いて回避することができない

skip_callbackとset_callbackを用いて一時的にコールバックを回避しようとしたらset_callbackがうまくいかなかった。その時の状況と回避策を記録。

やりたいこと

こちらのコールバックを一時的にスキップしたい。
ruby
before_validation :func, on: :action

最初にやったこと

skip_callbackset_callbackを用いて実装した。

skip_callback(:validation, :before, :func)
------------------------------------------
# 処理を実行
------------------------------------------
set_callback(:validation, :before, :func)

しかし、このあと他の動作に不具合が出てしまい、原因を調査した。

skip_callback、set_callbackとは

skip_callbackは設定されているコールバックをスキップさせることができる。ただし、一度実行するとずっとskipしたままになってしまうため、set_callbackを用いて戻す必要がある。

うまくいかない原因

on: :actionの再設定ができていないため。

つまり、以下のようにコールバックの設定が変わってしまっていたため、挙動が変わってしまった。

# スキップ前
before_validation :func, on: :action
# スキップ後
before_validation :func

対処

最初に行ったset_callbackは、on: :actionに関して特に指定をしていない。
ruby
set_callback(:validation, :before, :func)

set_callbackでは、on: actionの設定をしておらず、on: actionの設定が無くなってしまっていた。

on: :actionの設定をしなければいけないため、onで設定している場合はその設定も引数に入れなければならないらしい。

https://stackoverflow.com/questions/46810340/how-to-set-callback-with-the-on-create-option-in-rails-test

set_callback(:validation, :before, :func, on: :action)

しかし、こちらをやってみても改善されず。

ソースを調べてみた。
https://github.com/rails/rails/blob/070d4afacd3e9721b7e3a4634e4d026b5fa2c32c/activesupport/lib/active_support/callbacks.rb#L674

ソースコードにコメントしてある引数にはif, unless, prependしか無いためon: :actionの設定は難しそう。

最終的な対処法としては、コールバックを回避して処理を行うメソッドが存在するため、こちらを用いることにした。
https://railsguides.jp/active_record_callbacks.html#%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%82%92%E3%82%B9%E3%82%AD%E3%83%83%E3%83%97%E3%81%99%E3%82%8B

まとめ

skip_callbackset_callbackはコールバックを回避するための便利なメソッドですが、onを用いた条件付けがされている場合かつ全てのコールバックを回避しても問題ない場合はそもそもコールバックを回避できるメソッドを使った方が良いのかなと今のところ考えています。

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

【Rails】ローカル環境のデータがおかしいから全部リセットしたい!と思うその前にすべきこと

はじめに

以下のQiita記事を拝見しました。

エラーが解決しない時は、元を断つという考え方 - Qiita

詳しくは上記記事を読んでもらいたいのですが、簡単に要約すると「ローカル環境のデータがおかしくなったら、(データを消しても問題ないことを確認したうえで)データベースを再作成しよう」という内容です。

ただ、個人的には「うーん、あまりよくないな」と思ってしまったので、返信コメントを書こうと思いました。
コメントを書き始めたらかなり長くなってしまったので、独立した記事にしてしまおうと思いました。
それが本記事になります。

というわけで、ローカル環境のデータがおかしいから全部リセットしたい!と思うその前にすべきことを以下にまとめます。

ローカル環境のデータがおかしいから全部リセットしたい!と思うその前にすべきこと

(注:以下の内容はもともと冒頭に載せたQiita記事への返信コメントとして書いたものです。その前提で読み進めてください)

データベースのリセットは「データを全部消しても問題ない」というときは有効ですが、そうでないときは使えません。
個人の趣味で開発しているRailsアプリなら良いかもしれませんが、仕事で開発するRailsアプリはローカル環境といえど、「今までよく使ってきたテストデータ」がたくさん蓄積されているので、往々にして「これを全部消すわけにはいかない」ということになります。
また、開発環境でなく、本番環境で同じ問題が起きたときは「データを全部消す」という作戦は、まず使えません。

よって、「データを全部消す」以外の作戦も知っておきましょう。
というか、むしろ「データを全部消す」のは最終手段にしましょう。

僕であれば以下のような対処法をとります。

不具合の原因を調査し、データ異常かそうでないかを見極める

まず、不具合の原因を見極めます。
今回の記事であれば、post.usernilになっているのが原因のようです。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_688029_480b986b-36d4-66fd-60a6-7f1dcef94075.png

次に、その状態が妥当かどうかを判断します。
post.usernilということは、「投稿に紐付く投稿者がいない」という状態です。
これは常識的に考えると、まずありえない状況(=データの異常)だと思います。

データ異常が発生した原因を突き止め、必要に応じてコードを修正する(再発防止)

それから、なぜ「投稿者がいない投稿が生まれてしまったのか?」という原因を考えます。
その原因を突き止めないと、もしデータベースを再作成してエラーを出なくしても、しばらくするとまた「投稿者がいない投稿」がひょっこり現れるかもしれません。

ふつうに画面を操作していて投稿者無しで投稿できる仕組みになっているのであれば、それは不具合ですので、不具合が再発しないよう、必ず投稿者が設定される仕組みにコードを修正しましょう。

以下に想定できそうな原因と対処法を載せます。

optional: trueが付いていた場合

最近のRailsだとbelongs_toで定義した関連レコードはデフォルトで必須になっているので、可能性は低いと思いますが、もしoptional: trueが付いていたらこれを外します。こうすれば、usernilの状態でPostを保存しようとするとバリデーションエラーが発生します。

app/models/post.rb
 class Post < ApplicationRecord
-  belongs_to :user, optional: true
+  belongs_to :user
 end

あとからpostsテーブルにuser_id列を追加した場合

もしくは、最初にPostモデルが作成され、その後時間を空けて別のmigrationでuser_idがpostsテーブルに追加されたのかもしれません。
この場合はmigration実行後に、既存のPostレコードに対して何らかのUserレコードを紐付けて保存する対応(既存データの修正)が必要になります。
この対応を忘れていると、「投稿者がいない投稿」が生まれてしまいます。

Userレコードが削除されていた場合

もしかすると一番ありえるのは、「Postレコードを作成したあとに、投稿者のUserレコードを削除してしまったこと」が原因かもしれません。
この場合は、

  • 関連するPostレコードを持つUserレコードは削除できないようにする
  • または、Userレコードを削除したら関連するPostレコードも一緒に削除する
  • または、投稿者がいない投稿を正常なデータとして受け入れた上で、「退会済みユーザー」のような表示を画面に出す

といった対処方法が考えられます。
最初の2つについてはRailsの dependent オプションで設定可能です。
詳しくは以下の記事をご覧ください。

dependent: :restrict_with_error と :restrict_with_exception の違い - Qiita

ぱっと思いつく原因はこれぐらいですが、他にも「これが原因だった」とうい原因(=不具合)を見つけたら、今後二度と「投稿者がいない投稿」が生まれないようにプログラムを修正します。

再発防止策がとれたら、既存の異常データを修正する

おかしなデータが生まれない状況を作れたら、今度は既存の異常データを修正します。
たとえば、以下のようなスクリプトを実行すれば、「投稿者がいない投稿」に対して投稿者を設定できます。(あくまで例ですので、適宜スクリプトを修正してください)

user = User.first
Post.all.each do |post|
  if post.user.nil?
    post.user = user
    post.save!
  end
end

もしくは、「投稿者がいない投稿」を削除する、というデータ修正も選択肢のひとつになります。

Post.all.each do |post|
  if post.user.nil?
    post.destroy!
  end
end

こうすれば「すべての投稿に投稿者が紐付いている状態」になるので、post.user.image_nameを呼びだしてもエラーは出なくなるはずです。

データ異常ではなかった場合は、コードを適切に修正する

もし仮に「投稿に紐付く投稿者がいない」という状況が不具合ではない(仕様として妥当である)場合は、post.usernilかどうかでviewの処理を分ける必要があります。

<% if post.user %>
  <%# 投稿者がいる場合の処理 %>
<% else %>
  <%# 投稿者がいない場合の処理 %>
<% end %>

まとめ

このように対処すれば、「データを全部消す」という手段をとらずに済みます。
というか、「ローカル環境で個人的な趣味で作ってるだけです」という場合を除き、通常はこのような対応を取るのが適切だろうと僕は考えます。

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

【Rails】開始時刻と終了時刻を保存する

開始時刻と終了時刻を保存する

railsで、タスクの開始時刻と終了時刻を保存するプログラムを作ります。
自己流の部分が多いので、あくまでやり方の1つとして参考にしてくださると幸いです。
※今回は、開始、終了時刻を保存することをメインに扱います。削除、編集機能や見た目は勘弁してください。

画面収録 2020-09-25 2.57.23.mov.gif

考え方

開始、終了フォームを1度だけしか押せない仕組みを目指します。
具体的には、タスクの開始時刻のデータが入っている場合は開始ボタンを表示しないように、ビューの中にif文を書きます。
この考えに至った経緯は、以下の通りです。

・タスク(Task)に、開始時刻(StartTime)と終了時刻(StopTime)を紐付ける
 →問題点:何回でも開始し、終了できてしまう。
・StartTimeのデータが入っている時(開始している時)、ボタンを表示させないようにする。
・StopTimeも上と同様にする。

準備

ターミナル

rails new  _6.0.0_ アプリ名 -d mysql
cd アプリ名
bundle install

モデルの作成

rails g model task
rails g model start_time
rails g model stop_time

モデルの修正

間違いがありましたら、ご指摘お願いします。

php;task.rb
class Task < ApplicationRecord
  has_one :start_time
  has_one :stop_time
end
php;stop_time.rb
class StopTime < ApplicationRecord
  belongs_to :task
end
php;start_time.rb
class StartTime < ApplicationRecord
  belongs_to :task
end

マイグレーションファイル

class CreateTasks < ActiveRecord::Migration[6.0]
  def change
    create_table :tasks do |t|
      t.string :title
      t.timestamps
    end
  end
end
php;202..._create_start_times.rb
class CreateStartTimes < ActiveRecord::Migration[6.0]
  def change
    create_table :start_times do |t|
      t.references :task, foreign_key: true
      t.timestamps
    end
  end
end
php;202..._create_stop_times.rb
class CreateStopTimes < ActiveRecord::Migration[6.0]
  def change
    create_table :stop_times do |t|
      t.references :task, foreign_key: true
      t.timestamps
    end
  end
end

ターミナル

rails db:create
rails db:migrate

ルーティング

Rails.application.routes.draw do
  root "tasks#index"
  resources :tasks, only: [:index, :create] do
    resources :start_times, only: :create
    resources :stop_times, only: :create
  end 
end

使うものしか指定していません。
また、今回は、ネストしていますが、実はしなくても作れます。

コントローラー

ターミナル

rails g controller tasks
rails g controller start_times
rails g controller stop_times
php;start_times_controller.rb
class StartTimesController < ApplicationController
  def create
    StartTime.create(start_time_params)
    redirect_to root_path
  end

  private
  def start_time_params
    params.permit(:task_id)
  end
end
php;stop_times_controller.rb
class StopTimesController < ApplicationController
  def create
    StopTime.create(stop_time_params)
    redirect_to root_path
  end

  private
  def stop_time_params
    params.permit(:task_id)
  end
end
php;tasks_controller.rb
class TasksController < ApplicationController
  def index
    @tasks = Task.all
  end

  def create
    task = Task.create(task_params)
    redirect_to root_path
  end

  private
  def task_params
    params.permit(:title)
  end
end

ビュー

urlは、rails routesで確かめて記入してください。

php;views/tasks/index.html.haml
-# タスク保存フォーム
= form_with url: tasks_path, method: :post do |f|
  = f.text_field :title
  = f.submit "send"

-# タスク一覧表示
- @tasks.each do |task|
  %br
  タイトル
  = task.title

  -# 時間を確認したい場合は記載してください。
  - if task.start_time != nil
    %br
    開始時刻
    = task.start_time.created_at
    %br
  - if task.stop_time != nil
    終了時刻
    = task.stop_time.created_at
    %br
    %br
  -# ここまで

  - if task.start_time == nil
    = form_with url: task_start_times_path(task_id: task.id), method: :post do |f|
      = f.submit "開始"
      %br

  - if task.start_time != nil && task.stop_time == nil 
    = form_with url: task_stop_times_path(task_id: task.id), method: :post do |f|
      = f.submit "終了"
      %br

今回は、new.html.hamlを全く作っていません。
しかしform_withで、createアクションに対応するurlを指定し、method: :postをつけることで直接保存できるようになります。
うまくいかない場合は、binding.pryなどを使い、送っている値、受け取っている値を確認すると良いと思います。

まとめ

力技で作った感じがするので、もっと良い方法がある場合は、ぜひコメントしてください!

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

エラーが解決しない時は、元を断つという考え方

どうも、三町哲平です。

プログラミングをしていたら必ずエラーに出会す場面が出てきます。
簡単に解決できるエラーもあれば、何時間、下手したら数日掛かっても解決しないそんなやる気すら失ってしまうエラーも存在します。

そういうやる気を失ってしまう時に役立つエラーに使えるかも知れないテクニックを一つ紹介します。
尚、今回はRuby on Railsで発生したエラーですが、データベース絡みのエラーだとRails問わず、どのプログラミング言語での開発環境においても効果的です。

今回は、エラー発生から3日掛かって解決しましたので、短くわかりやすくまとめてみました。

まず、結論

この記事の概略を説明すると、
データベースが元の異常ぽかったら、データベースを再作成した方が早いよ。
ていう話。

では、3日間の闘いをどうぞ!

1日目. Template::Error

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

undefined method `image_name' for nil:NilClass

上記の文をピックアップしてfor nil:NilClassの部分を無視して考えた時に
image_nameというメソッドが定義されていないという意味です。

1. メソッドの定義忘れ
2. シンプルに誤字脱字

この辺りを疑い色々試してみましたが、
メソッドの定義悪でも、誤字脱字でもない...

ググってみても正解には辿り着けない...

とりあえず初日は疲れて寝てしまいました。

2日目. データベースっぽい

誤字脱字がないか再調査したが、問題なし。

ここで余り気に留めていなかった

for nil:NilClass

の部分についてググってみるようになります。

ちなみにRubyを使ったことない方は、
nilって何?となるかもしれませんが、

nilは、nullと同じ意味です。
つまり、「何もない」ということです。

何もないという内容で思い付くのは1つ...
データベース絡みっぽいなということに

そして気付けば深夜1時、またすっかり日付が変わっていました。

3日目.ググり続けた結果

Ruby on Rails 5 - undefined method `image_name' for nil:NilClass といエラーがでています|teratail

上記のサイトに辿り着きました。
ここからは、引用が続きますが、

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

このやり取りが全てです。

文字多いし小さしで読みたくない方に向けて結論だけ簡単に要約すると、
原因はおおよそ掴めたけど、もうデータベースを作り替えた方がいいよね。
という話。

postsテーブルを作成してデータを複数入力

nilの状態(何もない)のカラムを作成してはいけないルールに途中からプログラミングする

新しいカラム(user_id)を作成する

今までのuser_idカラムは全てnilの状態である

エラー発生

さいごに

原因はデータベースに入れてはいけないデータが含まれている。それならばそのデータを削除すれば良い。
もしくは開発環境のデータベースで消しても問題ないのならば今回のように再作成すればそれで復旧できます。

仮にRuby on Railsの場合ですと、

Rails
$ rails db:drop # データベースを削除する
$ rails db:create # データベースを作成する
$ rails db:migrate # データベースにマイグレートする

これと同じようにあなたの開発環境にあったソースコードを入力したら、良い訳ですね^^

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

DockerでRuby on Rails + Vue + MySQLの環境構築をする方法【2020/09最新版】

はじめに

フロントにVue、APIサーバにRailsという組み合わせは日本ではメジャーな構成の一つです。今回はこの構成をDockerを使って構築していきます。

この記事を読むことで、以下の構成をDocker作成出来るようになります!
API: Rails 6系
フロント:Vue(TypeScript) 2系
DB: MySQL 5.7

環境構築の流れ

  1. Rails、VueのDockerfile作成
  2. docker-compose.yaml作成
  3. Rails、Vueのプロジェクト作成

最終的なディレクトリ構成は以下のようになります。

[project_name]
├── api // Rails
│   ├── Dockerfile
│   ├── ...
├── docker-compose.yaml
└── front // Vue
    ├── Dockerfile
    ├── ...

では環境構築に入っていきましょう!

1. ディレクトリ作成

プロジェクトディレクトリとその配下にAPI、フロントのディレクトリを作成します

$ mkdir project_name
$ cd project_name
$ mkdir api front

以降のコマンドはカレントディレクトリがプロジェクトディレクトリ想定で記載しています。

2. Dockerfile作成

RailsとVueそれぞれのDockerfileを作成します。

2-1. Rails

指定しているバージョン
- Ruby 2.7.1
- Rails 6.0.x

api/Dockerfile
FROM ruby:2.7.1

RUN apt-get update -qq && \
    apt-get install -y build-essential \ 
                       libpq-dev \        
                       nodejs \
   && rm -rf /var/lib/apt/lists/* 

RUN mkdir /app
ENV APP_ROOT /app
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN bundle install

rm -rf /var/lib/apt/lists/*はaptのキャッシュを削除しています。これはDockerのイメージファイルサイズを軽量化するためです。

このDockerfileではRailsを含むGemfileを必要とするので、以下のファイルをapi/に作成します。

api/Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 6.0.3'

空のGemfile.lockも作成します。

$ touch api/Gemfile.lock

この時点でのディレクトリ構成

[project_name]
├── api 
│   ├── Dockerfile   <- New!
│   ├── Gemfile      <- New!
│   └── Gemfile.lock <- New!
└── front

2-2. Vue

指定しているバージョン
- node 12.18.3
- Vue 2系 <- これは後述のコンテナ内でVueプロジェクトを作成する際に指定します

front/Dockerfile
FROM node:12.18.3-alpine

ENV APP_HOME /app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME

RUN apk update && npm install -g @vue/cli

この時点でのディレクトリ構成

[project_name]
├── api 
│   ├── Dockerfile
│   ├── Gemfile
│   └── Gemfile.lock
└── front 
    └── Dockerfile <- New!

3. docker-compose.yaml作成

以下のdocker-compose.yamlをプロジェクトディレクトリに作成します

docker-compose.yaml
version: '3'

services:
  web:
    build: ./api
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    ports:
      - '3000:3000'
    depends_on:
      - db
    volumes:
      - ./api:/app
      - bundle:/usr/local/bundle
    tty: true
    stdin_open: true
  db:
    image: mysql:5.7
    volumes:
      - mysql_data:/var/lib/mysql/
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3306:3306'
  front:
    build: ./front
    volumes:
      - ./front:/app
    ports:
      - '8080:8080'
    tty: true
    stdin_open: true
    command: npm run serve

volumes:
  mysql_data:
  bundle:

MySQLとbundleのデータはボリュームにマウントし永続化しています。これによってコンテナを削除しても、データは消えません。
bundleはマウントしなくても、Gemを追加するたびにイメージのビルドすれば良いのですが時間がかかります。bundleをマウントすることでGemの追加がdocker-compose run api bundle installで済むようにです。

この時点でのディレクトリ構成

[project_name]
├── api 
│   ├── Dockerfile
│   ├── Gemfile
│   └── Gemfile.lock
├── docker-compose.yaml <- New!
└── front 
    └── Dockerfile

4. プロジェクトの作成

4-1. Rails

Railsプロジェクトの作成
rails newでRailsプロジェクトを作成します

$ docker-compose run web rails new . --force --database=mysql --api --skip-bundle

rails newの引数について
--force:Gemfileを強制的に上書き更新する
--database:使用するデータベースをMySQLにする
--api:APIモードでプロジェクトを作成。APIモードではUIに関係するファイルが省略されます。
--skip-bundlebundle installを省略します。次のdockerイメージのビルドでbundle installをするためです。

dockerイメージ更新
Gemfileが更新されたので、buildしてdocker imageを更新します。

$ docker-compose build

DBの設定ファイルを修正

RailsのDB設定ファイルapi/config/database.ymlを修正します。

api/config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
- password:
+ password: password
- host: localhost
+ host: db
  • passwordはdocker-compose.yamlの環境変数MYSQL_ROOT_PASSWORDで指定したもの
  • hostはDBのサービス名 に対応しています。

DBの作成

$ docker-compose run web rails db:create 

これでRailsの環境構築は完了です!

4-2. Vue

vue-cliでVueプロジェクトを作成
コンテナ内に入り、vue-cliを使ってVueプロジェクトの作成を対話的にします。

以下の設定項目はvue-cli v4.5.6のものです。設定内容は一例なのでお好みでどうぞ。

Vueコンテナでシェルを実行

$ docker-compose run front sh

以下フロントコンテナ内で対話的に設定していきます。

$ vue create .
# 現在のディレクトリ(/app)に作成するかの確認
? Generate project in current directory? (Y/n) Yes

# プリセットを使用するかどうか
? Please pick a preset: (Use arrow keys)
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
❯ Manually select features # TypeScriptをインストールするためこちらを選択

# プロジェクトにインストールするライブラリの選択
? Check the features needed for your project:
 ◉ Choose Vue version #
 ◉ Babel
❯◉ TypeScript # TypeScriptをインストールする場合はこれを選択
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

# Vueバージョンの選択
? Choose a version of Vue.js that you want to start the project with (Use arrow keys)
❯ 2.x
  3.x (Preview)

# Class styleを使用するかどうか。私はObject styleを使うため No
? Use class-style component syntax? (Y/n) No

# TypeScriptと一緒にbabelを使うか
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Yes

# LintとFormatterの設定に何を使うか
? Pick a linter / formatter config:
  ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
❯ ESLint + Prettier
  TSLint (deprecated)

# Lintの実行タイミング
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Lint on save # 保存時にLintを実行
 ◯ Lint and fix on commit (requires Git)

# Babel, ESLintなどの設定をどこに記述するか
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files # 各設定ファイルにする
  In package.json

# 今回設定した内容をプリセットで保存するか。基本的にはプロジェクトを以降作成することはないため No
? Save this as a preset for future projects? No

# パッケージマネージャーに何を使うか
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
  Use NPM

インストール完了後、Ctrl+Dでコンテナを停止します。

5. 動作確認

コンテナを立ち上げる

$ docker-compose up -d

-d: バックグラウンドでプロセスを実行する

5-1. Rails

localhost:3000にアクセスし以下のページが表示されることを確認
スクリーンショット 2020-09-24 22.37.32.png

5-2. Vue

localhost:8080にアクセスし以下のページが表示されることを確認
スクリーンショット 2020-09-24 23.30.07.png

おわりに

お疲れさまでした。
今回はDockerでRuby on Rails + Vue + MySQLの環境構築する方法について書きました。

VueとRailsはそこまで学習コストが高くないため、初心者にもオススメ出来る構成です。ぜひお試しください!

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

DockerでRails + Vue + MySQLの環境構築をする方法【2020/09最新版】

はじめに

フロントにVue、APIサーバにRailsという組み合わせは日本ではメジャーな構成の一つです。今回はこの構成をDockerを使って構築していきます。

この記事を読むことで、以下の構成をDocker作成出来るようになります!
API: Rails 6系
フロント:Vue(TypeScript) 2系
DB: MySQL 5.7

環境構築の流れ

  1. Rails、VueのDockerfile作成
  2. docker-compose.yaml作成
  3. Rails、Vueのプロジェクト作成

最終的なディレクトリ構成は以下のようになります。

[project_name]
├── api // Rails
│   ├── Dockerfile
│   ├── ...
├── docker-compose.yaml
└── front // Vue
    ├── Dockerfile
    ├── ...

では環境構築に入っていきましょう!

1. ディレクトリ作成

プロジェクトディレクトリとその配下にAPI、フロントのディレクトリを作成します

$ mkdir project_name
$ cd project_name
$ mkdir api front

以降のコマンドはカレントディレクトリがプロジェクトディレクトリ想定で記載しています。

2. Dockerfile作成

RailsとVueそれぞれのDockerfileを作成します。

2-1. Rails

指定しているバージョン
- Ruby 2.7.1
- Rails 6.0.x

api/Dockerfile
FROM ruby:2.7.1

RUN apt-get update -qq && \
    apt-get install -y build-essential \ 
                       libpq-dev \        
                       nodejs \
   && rm -rf /var/lib/apt/lists/* 

RUN mkdir /app
ENV APP_ROOT /app
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN bundle install

rm -rf /var/lib/apt/lists/*はaptのキャッシュを削除しています。これはDockerのイメージファイルサイズを軽量化するためです。

このDockerfileではRailsを含むGemfileを必要とするので、以下のファイルをapi/に作成します。

api/Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 6.0.3'

空のGemfile.lockも作成します。

$ touch api/Gemfile.lock

この時点でのディレクトリ構成

[project_name]
├── api 
│   ├── Dockerfile   <- New!
│   ├── Gemfile      <- New!
│   └── Gemfile.lock <- New!
└── front

2-2. Vue

指定しているバージョン
- node 12.18.3
- Vue 2系 <- これは後述のコンテナ内でVueプロジェクトを作成する際に指定します

front/Dockerfile
FROM node:12.18.3-alpine

ENV APP_HOME /app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME

RUN apk update && npm install -g @vue/cli

この時点でのディレクトリ構成

[project_name]
├── api 
│   ├── Dockerfile
│   ├── Gemfile
│   └── Gemfile.lock
└── front 
    └── Dockerfile <- New!

3. docker-compose.yaml作成

以下のdocker-compose.yamlをプロジェクトディレクトリに作成します

docker-compose.yaml
version: '3'

services:
  web:
    build: ./api
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    ports:
      - '3000:3000'
    depends_on:
      - db
    volumes:
      - ./api:/app
      - bundle:/usr/local/bundle
    tty: true
    stdin_open: true
  db:
    image: mysql:5.7
    volumes:
      - mysql_data:/var/lib/mysql/
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3306:3306'
  front:
    build: ./front
    volumes:
      - ./front:/app
    ports:
      - '8080:8080'
    tty: true
    stdin_open: true
    command: npm run serve

volumes:
  mysql_data:
  bundle:

Railsの開発サーバ起動時に-bでホストを指定します。省略するとホストからコンテナにアクセスできません。0.0.0.0を指定することで、コンテナが持つ全てのインターフェースでlistenできるようになるため、ホスト側からコンテナにアクセスできるようになります。

MySQLとbundleのデータはボリュームにマウントし永続化しています。これによってコンテナを削除しても、データは消えません。
bundleはマウントしなくても、Gemを追加するたびにイメージのビルドすれば良いのですが時間がかかります。bundleをマウントすることでGemの追加がdocker-compose run api bundle installで済むようにです。

この時点でのディレクトリ構成

[project_name]
├── api 
│   ├── Dockerfile
│   ├── Gemfile
│   └── Gemfile.lock
├── docker-compose.yaml <- New!
└── front 
    └── Dockerfile

4. プロジェクトの作成

4-1. Rails

Railsプロジェクトの作成
rails newでRailsプロジェクトを作成します

$ docker-compose run web rails new . --force --database=mysql --api --skip-bundle

rails newの引数について
--force:Gemfileを強制的に上書き更新する
--database:使用するデータベースをMySQLにする
--api:APIモードでプロジェクトを作成。APIモードではUIに関係するファイルが省略されます。
--skip-bundlebundle installを省略します。次のdockerイメージのビルドでbundle installをするためです。

dockerイメージ更新
Gemfileが更新されたので、buildしてdocker imageを更新します。

$ docker-compose build

DBの設定ファイルを修正

RailsのDB設定ファイルapi/config/database.ymlを修正します。

api/config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
- password:
+ password: password
- host: localhost
+ host: db
  • passwordはdocker-compose.yamlの環境変数MYSQL_ROOT_PASSWORDで指定したもの
  • hostはDBのサービス名 に対応しています。

DBの作成

$ docker-compose run web rails db:create 

これでRailsの環境構築は完了です!

4-2. Vue

vue-cliでVueプロジェクトを作成
コンテナ内に入り、vue-cliを使ってVueプロジェクトの作成を対話的にします。

以下の設定項目はvue-cli v4.5.6のものです。設定内容は一例なのでお好みでどうぞ。

Vueコンテナでシェルを実行

$ docker-compose run front sh

以下フロントコンテナ内で対話的に設定していきます。

$ vue create .
# 現在のディレクトリ(/app)に作成するかの確認
? Generate project in current directory? (Y/n) Yes

# プリセットを使用するかどうか
? Please pick a preset: (Use arrow keys)
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
❯ Manually select features # TypeScriptをインストールするためこちらを選択

# プロジェクトにインストールするライブラリの選択
? Check the features needed for your project:
 ◉ Choose Vue version #
 ◉ Babel
❯◉ TypeScript # TypeScriptをインストールする場合はこれを選択
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

# Vueバージョンの選択
? Choose a version of Vue.js that you want to start the project with (Use arrow keys)
❯ 2.x
  3.x (Preview)

# Class styleを使用するかどうか。私はObject styleを使うため No
? Use class-style component syntax? (Y/n) No

# TypeScriptと一緒にbabelを使うか
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Yes

# LintとFormatterの設定に何を使うか
? Pick a linter / formatter config:
  ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
❯ ESLint + Prettier
  TSLint (deprecated)

# Lintの実行タイミング
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Lint on save # 保存時にLintを実行
 ◯ Lint and fix on commit (requires Git)

# Babel, ESLintなどの設定をどこに記述するか
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files # 各設定ファイルにする
  In package.json

# 今回設定した内容をプリセットで保存するか。基本的にはプロジェクトを以降作成することはないため No
? Save this as a preset for future projects? No

# パッケージマネージャーに何を使うか
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
  Use NPM

インストール完了後、Ctrl+Dでコンテナを停止します。

5. 動作確認

コンテナを立ち上げる

$ docker-compose up -d

-d: バックグラウンドでプロセスを実行する

5-1. Rails

localhost:3000にアクセスし以下のページが表示されることを確認
スクリーンショット 2020-09-24 22.37.32.png

5-2. Vue

localhost:8080にアクセスし以下のページが表示されることを確認
スクリーンショット 2020-09-24 23.30.07.png

おわりに

お疲れさまでした。
今回はDockerでRuby on Rails + Vue + MySQLの環境構築する方法について書きました。

VueとRailsはそこまで学習コストが高くないため、初心者にもオススメ出来る構成です。ぜひお試しください!

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