20200925のRailsに関する記事は21件です。

【rails】他テーブルのDBの情報をビューで表示させる

はじめに

 この記事は、学んだことを復習するために投稿者が理解できる言葉まで落とし込んで書いております。まだまだ初心者ですが、この記事が読んでくださった方の悩みが解決すれば嬉しいです。

1.結論

  1. アソシエーションが定義されていること
  2. ルーティングが入れ子構造(ネスト)になっていること
  3. 表示させたい側のコントローラーで値を取得していること
  4. ビューで表示させる記述がなされていること

2.コード

2-1.テーブル

  • itemsテーブル
class CreateItems < ActiveRecord::Migration[6.0]
  def change
    create_table :items do |t|
      t.string     :name               , null: false # 商品名
      t.text       :explanation        , null: false # 商品の説明
      t.integer    :category_id        , null: false # カテゴリー
      t.integer    :status_id          , null: false # 商品の状態
      t.integer    :delivery_fee_id    , null: false # 配送料の負担
      t.integer    :shipping_region_id , null: false # 発送地域
      t.integer    :shipping_day_id    , null: false # 発送までの日数
      t.integer    :selling_price      , null: false # 販売価格
      t.references :user               , null: false, foreign_key: true  # 外部キー(user情報)
      t.timestamps
    end
  end
end
  • ordersテーブル
class CreateOrders < ActiveRecord::Migration[6.0]
  def change
    create_table :orders do |t|
      t.references :user, null: false, foreign_key: true # 外部キー(ユーザー情報)
      t.references :item, null: false, foreign_key: true# 外部キー(商品情報)
      t.timestamps
    end
  end
end

2-2.モデル

  • itemモデル
class Item < ApplicationRecord
  belongs_to :user
  has_one :order # ポイント①
  has_one_attached :image

  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to_active_hash :category
  belongs_to_active_hash :status
  belongs_to_active_hash :delivery_fee
  belongs_to_active_hash :shipping_region
  belongs_to_active_hash :shipping_day

  # 共通で、空の投稿を保存できないようにする
  with_options presence: true do
    validates :image
    validates :name
    validates :explanation
    validates :selling_price
  end

  # 共通で、選択が「---」の時は保存できないようにする
  with_options numericality: { other_than: 1 } do
    validates :category_id
    validates :status_id
    validates :delivery_fee_id
    validates :shipping_region_id
    validates :shipping_day_id
  end

  # 販売価格の範囲が、¥300以上~¥9,999,999未満であること
  validates :selling_price, numericality: { greater_than_or_equal_to: 300, less_than_or_equal_to: 9_999_999 }

  # 販売価格は半角数字のみ入力可能
  VALID_DELIVERY_FEE_REGEX = /[0-9\d]/.freeze
  validates :selling_price, format: { with: VALID_DELIVERY_FEE_REGEX }
end

  • orderモデル
class Order < ApplicationRecord
  belongs_to :user
  belongs_to :item # ポイント①
  has_one :address
end

2-3.ルーティング

Rails.application.routes.draw do
  devise_for :users
  root 'items#index'
  resources :items do
    resources :orders, only:[:index, :create]
  end
end

ターミナルで確認します。
image.png

アソシエーション先のレコードのidをparamsに追加してコントローラーに送るためにルーティングのネストを使用します。

2-4.コントトーラー

class OrdersController < ApplicationController
  def index
    @item = Item.find(params[:item_id])
  end
end

コントローラーですでに保存してある商品情報を、Item.find(params[:item_id])で取得し、@itemに代入します。

2-5.ビュー

@item.表示したいカラム名

# 上記のように記述することで、他テーブル(items)の情報をビューで表示させることが可能です。

app/views/orders/index.html.erb/

    <div class='buy-item-info'>
      <%= image_tag @item.image, class: 'buy-item-img' %>
      <div class='buy-item-right-content'>
        <h2 class='buy-item-text'>
          <%= @item.explanation %>
        </h2>
        <div class='buy-item-price'>
          <p class='item-price-text'>¥<%= @item.selling_price %></p>
          <p class='item-price-sub-text'>(税込)<%= @item.delivery_fee.name %></p>
        </div>
      </div>
    </div>

以上です。

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

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

モデルとは

  • 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で続きを読む

【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で続きを読む

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で続きを読む

[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で続きを読む

seed-fuを使ってseedデータを作成する方法

はじめに

railsにはデフォルトでseedが使えますが、初期データを入れる際にはseed-fuを使う方が便利なようです。
sedd-fuでは一部のレコードを更新したり、指定のモデルだけのデータを作成したりできます。
今回は、seed-fuの導入方法と基本的な使い方をご紹介します。

seed fuをインストール

Gemfileに記載して、bundle installします。

Gemfile
gem 'seed-fu'
ターミナル
$ bundle install

ディレクトリ構成

db/fixturesは最低限必要なディレクトリなのでこちらは作成してください。
db/fixtures に作成した seedファイルを置くこともできますが、db/fixtures/developmentdb/fixtures/productionのようにseedファイルを作成すると環境ごとに異なるデータを作成することもできます。

また、db/fixtures以下のseedファイルはアルファベット順に読み込まれます。
以下のようにファイル名に数字を入れると読み込む順番を指定することが可能になります。

db
├ fixtures
    ├ 01_group.rb
    ├ 02_user.rb
    ├ 03_tweet.rb

例として02_user.rbを記載します。

02_user.rb
10.times do |n|
  User.seed do |s|
    s.name = "name-#{n}"
    s.email = "user-#{n}@test.com"
  end
end

rails db:seedでseed fuが使えるようになる設定

rails db:seed_fuで全てのデータを順に作成することができますが、
下記のようにseeds.rbに一文書くことで、通常のseedのようにデータを作成することもできます。
seed-fuを知らない人でもデフォルトのseedと同じようにデータを作成できるので設定しておくと良いと思います。

seeds.rb
# rails db:seed で Seed Fu 呼び出せるように設定
SeedFu.seed

ファイルを指定してデータを作成する場合のコマンド

下記のコマンドでモデルを指定してデータを作成可能です。

ターミナル
rake db:seed_fu FILTER=01_group, 02_user  # 複数指定する場合はカンマ区切り

終わりに

seed-fuではいろいろと便利な機能があるので、詳細は下記を参考にしてください。
railsで初期データを入れる(seed-fuの使い方)
seedデータってどうやって入れる?seed-fuを使った便利なSeedデータ挿入法

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Railsプロジェクトを作成したあとにActiveRecordを使わないようにする

rails newするときに--skip-active-recordオプションを付けなかった場合から付けた場合への差分です。

Railsプロジェクトを作成したあとに、DBを使わなくなった場合などの参考に。

Railsバージョン6.0.0.3

.gitignore
- # Ignore the default SQLite database. 
- /db/*.sqlite3 
- /db/*.sqlite3-journal 
- /db/*.sqlite3-*


- # Ignore uploaded files in development.   
- /storage/*    
- !/storage/.keep
Gemfile
- # Use sqlite3 as the database for Active Record   
- gem 'sqlite3', '~> 1.4'


- # Use Active Storage variant  
- # gem 'image_processing', '~> 1.2'    
Gemfile.lock
省略
app/javascript/packs/application.js
- require("@rails/activestorage").start()
app/models/application_record.rb
削除
bin/setup
- # puts "\n== Copying sample files ==" 
- # unless File.exist?('config/database.yml')   
- #   FileUtils.cp 'config/database.yml.sample', 'config/database.yml'  
- # end 


- puts "\n== Preparing database ==" 
- system! 'bin/rails db:prepare'    
config/application.rb
- require 'rails/all'
+ require "rails"
+ # Pick the frameworks you want:
+ require "active_model/railtie"
+ require "active_job/railtie"
+ # require "active_record/railtie"
+ # require "active_storage/engine"
+ require "action_controller/railtie"
+ require "action_mailer/railtie"
+ # require "action_mailbox/engine"
+ # require "action_text/engine"
+ require "action_view/railtie"
+ require "action_cable/engine"
+ require "sprockets/railtie"
+ require "rails/test_unit/railtie"
config/database.yml
削除
config/environments/development.rb
- # Store uploaded files on the local file system (see config/storage.yml for options). 
- config.active_storage.service = :local


- # Raise an error on page load if there are pending migrations.    
- config.active_record.migration_error = :page_load 


- # Highlight code that triggered database queries in logs. 
- config.active_record.verbose_query_logs = true    
config/environments/production.rb
- # Store uploaded files on the local file system (see config/storage.yml for options). 
- config.active_storage.service = :local    


- # Do not dump schema after migrations.    
- config.active_record.dump_schema_after_migration = false  
config/environments/test.rb
- # Store uploaded files on the local file system in a temporary directory. 
- config.active_storage.service = :test 
config/initializers/wrap_parameters.rb
- # To enable root element in JSON for ActiveRecord objects.    
- # ActiveSupport.on_load(:active_record) do    
- #   self.include_root_in_json = true  
- # end
config/storage.yml
削除
db/seeds.rb
削除
package.json
-  "@rails/activestorage": "^6.0.0",
test/test_helper.rb
-  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 
- fixtures :all 

実際にrails newしたものの差分です。
https://github.com/hid3h/diff-rails-active-record/pull/1/commits/725b4147da5b94db4d5f5ded0c7bf5c27c3d4489

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

【rails】enumのレコード登録数をバリデーションで制限する方法

はじめに

開発を行っているとenumを使って、データを分ける場合があると思います。
enumで分けたデータに対してそれぞれレコードの登録数を制限する実装をしたのでメモとして残しておきます。

開発環境

Rails 6.0.3
Ruby 2.7.1

テーブル

今回は下記のようなUsersテーブルを想定しています。
性別(sex)はenumでman/femaleのどちらかしか選択できないようにしています。

name sex(enum)
ユーザー1 man
ユーザー2 female

レコード数を制限する方法

models/user.rbにメソッドを作成し、man/femaleそれぞれのレコード数を確認して、もし制限数(10件)を超えていればエラーを追加します。
on: :createを記載しないと毎回バリデーションが適用され、編集できなくなってしまうので忘れないよう!
(私はここに気づかず編集の際にエラーが発生してしまいました。)

user.rb
class User < ApplicationRecord
  REGISTER_LIMIT_COUNT = 10 # 定数として登録数を管理

  enum sex: { man: 0, female: 1 }

  validate :limit_user_register_count, on: :create  # 作成したメソッドでバリデーション

  def limit_user_register_count
    if self.sex == "man" && man.count >= REGISTER_LIMIT_COUNT # enumがman かつ manのレコード数が制限を超えている
        errors.add(:user, "man count is over") # エラーを追加
    elsif self.sex == "female" && female.count >= REGISTER_LIMIT_COUNT
        errors.add(:user, "female count is over")
    end
  end
end

Rspecでバリデーションを確認する方法

user_spec.rb
require 'rails_helper'

RSpec.describe Guide, type: :model do
  context 'データを11個以上登録する場合' do
    before { create_list(:user, 10, sex: 'man') } # manのデータをを10人分作成

    it 'エラーメッセージを返すこと' do
      man = build(:user, sex: 'man') # 11人目をbuildする(バリデーションがかかるのでcreateは不可)
      man.valid?                     # validすることでcreateしようとしてvalidationが走り、エラーが追加される
      expect(man.errors[:user]).to eq('man count is over')
    end
  end
end
factories/users.rb
FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    sex { 0 }
  end
end
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

【Rails】ActionMailerを使用してSendGridのAPI経由でメール送信する場合の差出人名

概要

メールを送信するときに差出人名を表示させたいことがあると思います。検索すると、Action Mailerでfromフィールドに差出人名を表示したいに書いている通り、fromに差出人名を文字列で入れれば反映されるのですが、SendGridのAPI経由で送る場合はこの方法では反映されないです。というわけでどうすれば良いのかというのをメモ。

対応

SendGridでは専用のメールアドレス設定用クラス(Email)が用意されていて、Send sender name with email SendGrid - Railsにある通り、アドレスと差出人名を設定する仕様になっています。

実装サンプル

ActionMailerでのSendGrid・API使用について、Rails: SendGrid(Web API)とAction Mailerでメールを送信するの記事がわかりやすくまとまっているのでこちらを元にします。
設定箇所はlib/mail/send_grid.rbのfromの部分になります。下記のような形で2つ目の引数に、差出人名を設定します。(差出人名は固定の前提)

send_grid.rb
 from = SendGrid::Email.new(email: mail.from.first, name: '山田太郎')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む