Open30

Ruby, Rails関連

FukuiFukui

form_with ..., scope: :hoge

  • 生成されるinputタグのname属性にキーを追加できる
  • modelを渡しているとキーを上書きする
  • form_with url: root_url => name="name"params[:name]
  • form_with url: root_url, scope: :hoge => name="hoge[name]"params[:hoge][:name]
  • form_with model: space=> name="space[name]"params[:space][:name]
  • form_with model: space, scope: :hoge => name="hoge[name]"params[:hoge][:name]
FukuiFukui

ぼっち演算子

  • [オブジェクト]&.[メソッド]
  • オブジェクトがnilでも例外が発生せず処理が継続する
  • オブジェクトがnilの場合にnoMethodエラーになりがちだが、それを回避できる
FukuiFukui

!を付けるときとは

  • よく使うところとして、以下のメソッドは失敗してもfalseやオブジェクトを返しちゃう
    • create => 作成失敗したオブジェクト
    • find_by => nil
    • save => false
    • destroy => false
    • update
      • ActiveRecord::Relationオブジェクト(usersとか) => 作成失敗したオブジェクト
      • ActiveRecord::Persistenceオブジェクト(userとか)=> false
  • !を付けると失敗した時にすべからく例外を生じさせる

例外が発生すると何が嬉しい?

  • DBのトランザクションが走るので安全
    • エラーを例外ページ(404や500系)に集約させられるため、セキュリティ上の穴に気づかれにくい
    • /users/100/editとかにアクセスした時に「許可されていません」とかを出すように実装すると「あ、このユーザーは存在自体はしてるんだな」と悪い人に思われるかも。それなら例外に飛ばしたほうが安全である

使わないどころ

  • 逆に条件分岐で処理を分けたいときは使えない
  • 加えて、createはオブジェクトを返すので、if @user.createが失敗しても真偽値はtrueになっちゃう。saveを使いましょう。
FukuiFukui

controllerで定義したメソッドをviewで使う

  • helper_methodメソッドにメソッド名をシンボルで渡すとviewで利用可能になる
helper_method :current_user

private
def current_user
    hogehoge
end
FukuiFukui

enum

enumとは

  • int型カラムなどに値に応じてラベルを付けておけるようなイメージの代物
    • 0なら一般ユーザー、1なら管理者、2なら…とか
  • DB層の話ではないので、モデルファイルに定義しておく
  • enumを定義しておくと便利なインスタンスメソッドも利用できる

書き方

  • モデルファイルに以下を追加(int型のroleカラムがあるとして)
  • enum role: { general: 0, superior: 1, admin: 2 }

使えるメソッド

  • 属性が文字列で返るようになる
    • user.role => adminとかになる。2は返らない
  • 数値を返してほしい場合はカラム名_before_type_castメソッド
    • user.role_before_type_cast => 2
  • 文字列でインスタンスが作成できる
    • User.create(role: "admin")でもいける
  • admin?が使える
    • user.admin?で真偽値を取得できる

i18n対応のgem

  • gem 'enum_help'を追加
  • config/application.rbにデフォルトlocaleを設定
    • これはgem 'rails-i18n'でも必要な設定ですね。
    • config.i18n.default_locale = :jaを追加
  • config/locales/ja.ymlを作成し以下の雛形で編集
ja:
  enums:
    user:
      role:
        general: 一般
        admin: 管理者
  • カラム名_i18nメソッドで翻訳されたenumを取得できる
    • user.role_i18n => "管理者"
    • User.roles_i18n[:admin] => "管理者"rolesと複数形になっていることに注意
FukuiFukui

dependent: :destroy

どういうことか

  • Userモデルとそれに紐づくPostモデルがあるとする
    • Userモデルにはhas_many :postsが定義されている
    • Postにはbelongs_to :userが定義されている
  • Postのuser_idカラムは外部キー制約が付いている
  • User(id: 1)がPost(id: 3)を持っている場合に退会等でUserを削除しようとすると、Postの外部キー制約に引っかかってUserを削除できなくなる

dependent: :destroyを加えると子レコードも一緒に削除される

  • これを回避するため、Userモデルにhas_many :posts, dependent: :destroyと加える
  • こうすると親レコーどが削除されると子レコードも一緒に削除される

子レコードは削除したくない場合

  • dependent: :nullify
    • 子レコードのuser_idカラムにnullが入るようになる
    • 親レコードだけ削除されて子レコードは削除されない
  • dependent: :restrict_with_error
    • 親レコードにエラーメッセージが追加される
    • parent.errors.full_messagesで取り出せる
  • dependent: :restrict_with_exception
    • 親レコードを削除しようとすると例外を生じさせる
FukuiFukui

ransack(工事中)

基本的な流れ

  • controller(検索フォーム表示)
    • search_formにはransackオブジェクトが渡されないとエラーになる
    • @q = Post.ransack(params[:q])とか@q = current_user.posts.ransack(params[:q])とかをviewに渡してあげる
    • もしくはsearch_form_for q || Post.ransack do |f|としてもエラーにならない
  • view
    • search_form_tagを用意
    • フォームヘルパーの第一引数に検索条件を指定
      • <%= f.search_field :body_cont %>とか
  • controller(検索結果表示)
    • @q = Post.ransack(params[:q])
    • @posts = @q.result(distinct: true)
      • params[:q]にクエリが入っている
      • resultメソッドでレコードを取得
      • distinct: trueで重複したレコードを除く

view

  • search_form_tagform_withと同じような感じでフォームヘルパーを利用できる
  • ただし第一引数にカラム:nameや属性取得のメソッド:hogehoge_idsとかではなく検索条件が入る
    • :title_or_body_or_answers_body_contとか
  • 検索条件はアソシエーションも考慮できる
  • リクエスト先のurlを指定もできる
    • <%= search_form_for @q, url: root_url do |f| %>とか
FukuiFukui

N+1対策(工事中)

どういうことか

工事中

どうすればいいのか

  • controllerでレコードを取得する際に工夫する
    • Before
      • controller:@users = User.all
      • view:eachメソッドにてuser.postsとかuser.imageを回す
    • After
      • contllerで以下メソッドをチェーンさせる
        • includesメソッドで関連するテーブルをまとめて取得する
          • User.includes(:posts, :comments)
        • (ActiveStorage)with_attached_imagesメソッドを利用する

具体例

@posts = @q.result(distinct: true).with_attached_images
                    .includes(user: { profile_image_attachment: { blob: :variant_records } })

@comments = @post.comments.includes(user: { profile_image_attachment: { blob: :variant_records } })

@spaces = @q.result.with_attached_images.includes(:features).order(created_at: :desc)

https://qiita.com/moroball14/items/5d4228cd3523f7a1ad04

FukuiFukui

デフォルトのlayoutファイルを変える

controller全体

  • controllerにてlayout "admin/layouts/application"を追加
  • そのcontrollerでは任意のpathのapplication.html.erbをlayoutとして使用してくれるようになる

actionごと

  • render layout: "layouts/application"

layoutを使用しない

  • controller:layout false
  • action:render layout: false
FukuiFukui

Basic認証

def basic_auth
    authenticate_or_request_with_http_basic do |username, password|
      username == 'admin' && password == 'password'
    end
end

これをbefore_actionだかなんだかで仕込んであげる。

FukuiFukui

ランダムな数字を出す

SecureRandomモジュール

  • SecureRandom.base64(10)
  • SecureRandom.hex(16)
    • ランダムなhex文字列(16進数文字列)を生成する
  • SecureRandom.uuid

Base64モジュール

  • Base64.strict_encode64(@user.uuid)
    • 改行コードをエンコードしない
FukuiFukui

Credentials

登録

  • EDITOR="vi" bin/rails credentials:editでエディタを開く

呼び出し

  • Rails.application.credentials.hogehoge.fugafugaで呼び出せる

注意点

  • ローカルのmaster.keyを使用して暗号化と復号を行う
  • そしてmaster.keyはgitignoreされている
  • つまりgit cloneしてきてもmaster.keyが無いため復号できない
  • sshで接続してローカルのmaster.keyをコピーするか、新しく作成しなおすかする必要がある
FukuiFukui

環境変数の取り出し

  • ENV['REDIS_URL']
FukuiFukui

キューイング

Sidekiqのインストール

  • gemをinstall
  • config/application.rbにキューイングアダプターの設定を加える
    • config.active_job.queue_adapter = :sidekiq
    • ※CIのテスト環境でredisが無いなどでsidekiqが利用できない場合は:inline:testを指定
  • config/initializers/sidekiq.rbにredisの設定を加える
Sidekiq.configure_server do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end
  • routes.rbに管理画面のルーティングをマウントする
if Rails.env.development?
    require 'sidekiq/web'
    mount LetterOpenerWeb::Engine, at: "/letter_opener" =>これは関係ない。けどよく使う
    mount Sidekiq::Web, at: '/sidekiq'
end
  • config/sidekiq.ymlを作成。ここにキューイングの設定を加えていく。定期実行についてもcronと同じ方式でここに定義していける(いま調査中)
:concurrency: 5
:queues:
  - default
  - active_storage_analysis
  - active_storage_purge

定義ファイル

:concurrency: 5
:queues:
  - default
  - active_storage_analysis
  - active_storage_purge
:scheduler:
  :schedule:
    line_notifications:
      cron: '0 8 * * *'
      class: LineNotificationsJob
  • concurrency:1つのSidekiqプロセスで使用するスレッド数
  • queues:ジョブ実行の優先順位を付ける
  • scheduler:scheduler gemが必要。cron形式でジョブを定期実行できる

sidekiq起動

  • sidekiq -C config/sidekiq.ymlで起動
  • /sidekiqにアクセスしてダッシュボードを閲覧できることを確認

アダプターごとの違い

  • config.active_job.queue_adapter = :inline
    • エンキューと実行までを行う
    • ジョブが即座に実行される。ジョブの実行結果をすぐにテストしたい場合に適しています
  • config.active_job.queue_adapter = :test
    • エンキューまでを行う
    • ジョブがキューに入るが、手動で実行する必要がある。ジョブがエンキューされることをテストする場合に適しています。
  • つまり、メール送信などで送信結果までを含めてテストしたい場合はinlineじゃないとだめ
FukuiFukui

ActiveJob

ActiveJobの作成

  • (バックグラウンドでのLINE通知機能を想定して開発する)
  • rails g job line_notifications
      invoke  test_unit
      create    test/jobs/line_notifications_job_test.rb
      create  app/jobs/line_notifications_job.rb

Jobの内容を定義

  • Jobファイルは以下のようになっている。perform以下に処理を書いていく
class LineNotificationsJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # Do something later
  end
end
  • messageAPIのSDKを利用して定義
require 'line/bot'

class LineNotificationsJob < ApplicationJob
  queue_as :default

  def perform(*args)
    users = User.all
    users.each do |user|
      message_text = "【今日のクイズ🌝】\n\nフランス語で「稲妻」...\n回答はコチラ #{Rails.application.routes.url_helpers.users_path}"
      message = { type: 'text', text: message_text }
      uid = user.authentications.where(provider: 'line').take.uid
      response = client.push_message(uid, message)
      logger.info "PushLineSuccess"
    end
  end

  private
  def client
    Line::Bot::Client.new { |config|
      config.channel_id = Rails.application.credentials.line_message.channel_id
      config.channel_secret = Rails.application.credentials.line_message.secret_key
      config.channel_token = Rails.application.credentials.line_message.access_token
    }
  end
end

テスト送信

  • rails consoleにて以下を実行
    • HelloWorldJob.perform_now

失敗する時

  • sidekiqは一定間隔でjobを再実行するので、再実行回数を定義しておくといい
    • sidekiq_options retry: 3をジョブのClassに追加する

複雑な場合分けでジョブを行うテクニック

  • sidekiqでジョブスケジューリングするが、そこにいくつもジョブを定義するのはイケてない
  • なので「ジョブ実行するかを確認するジョブ」「実際に実行されるジョブ」を切り分けて定義し、前者だけをスケジューリングしておく
  • 例えばユーザーが設定した時間を参照して通知を送りたい場合…

CheckNotificationsJobでユーザーの設定した時間を参照

class CheckNotificationsJob < ApplicationJob
  queue_as :default

  def perform
    current_hour = Time.zone.now.hour
    User.where(notification_time: current_hour).find_each do |user|
      LineNotificationJob.perform_later(user.id)
    end
  end
end

該当するユーザーにだけ送信ジョブを呼び出す

  • LineNotificationJob.perform_later(user.id)

送信ジョブが実行される

class LineNotificationJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    …以下送信コード
  end
end
FukuiFukui

url系のhelperをmodelとconsoleでも使う

rails console

  • app.order_unit_path(order_unit.code)とかでconsoleでも出力できる

model内で使用する場合

  • Rails.application.routes.url_helpers.order_unit_pathなど
FukuiFukui

リンクを発行する時のあれこれ

クエリパラメーター付きリンク

  • edit_user_path(@user, group_id: 1, team_id: 2)
    • =>http://localhost:3000/users/1/edit?group_id=1?team_id=2

id以外でリンクを発行したい

  • edit_user_path(@user.code, group_id: 1, team_id: 2)
    • =>http://localhost:3000/users/137923hjdhd123/edit?group_id=1?team_id=2※例
FukuiFukui

DockerでRails7, PostgreSQL, Tailwind, DaisyUIの環境構築

  • 気をつけること
    • DaisyUIがImportmapに対応していないためesbuildを用いてyarn addすること
    • rails new時点でtailwindesbuildは導入しておくこと
    • bin/devでサーバー起動すること
    • じゃないと後ほどごちゃる
  • ファイル準備
    • Dockerfile
      • yarn関連の記述を一旦コメントアウト
    • compose.yml
      • bundleとnode_modulesはボリュームマウントにしておくこと
      • bundle installyarn installしてもローカルのフォルダをマウントしていることによって上書きされてしまうため
    • entrypoint.sh
    • Gemfile(gem 'rails'のみ)
    • Gemfile.lock(空)
  • rails関連ファイルを作成
    • docker compose run web rails new . -j esbuild --css tailwind -d postgresql
  • Dockerfileにyarn関連の記述を戻す
    • COPY package.json ${ROOT}
    • COPY yarn.lock ${ROOT}
    • RUN yarn install
  • Procfile.devに以下を追加
    • bin/rails server -p 3000 -b '0.0.0.0'
  • Package.jsonに以下を追加 ※cssの箇所は適宜変更する
    • "scripts": {
    • "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
    • "build:css": "sass ./app/assets/stylesheets/application.tailwind.css ./app/assets/builds/application.css --no-source-map --load-path=node_modules"
    • }
    • これやらないとup時に以下のエラーが発生
      • web-1 | 15:36:44 js.1 | error Command "build" not found.
      • web-1 | 15:36:44 css.1 | error Command "build:css" not found.
      • 原因
        • Procfile.devに定義されているjs: yarn build --watch css: yarn build:css --watchに対して、Package.jsonに何も定義されていないのでnot foundになっている
  • sassコマンドを使用するためdocker-compose run web yarn add sassを実行
    • これをやらないとweb-1 | 16:18:07 css.1 | /bin/sh: 1: sass: not foundエラーが出る
  • docker compose run web yarn add daisyui
  • docker compose build --no-cache

完成形

Dockerfile
FROM ruby:3.3.3
ARG ROOT="/beta-master"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR ${ROOT}

RUN apt-get update; \
  apt-get install -y --no-install-recommends \
	postgresql-client tzdata 

RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update && apt-get install -y yarn

COPY Gemfile ${ROOT}
COPY Gemfile.lock ${ROOT}
RUN gem install bundler
RUN bundle install --jobs 4

COPY package.json ${ROOT}
COPY yarn.lock ${ROOT}
RUN yarn install

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["bin/dev"]
compose.yml
services:
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bin/dev"
    volumes:
      - .:/beta-master:cached
      - bundle:/usr/local/bundle
      - node_modules:/beta-master/node_modules
    depends_on:
      - db
      - redis
      - chrome
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      REDIS_URL: redis://redis:6379
      SELENIUM_DRIVER_URL: http://chrome:4444/wd/hub
    ports:
      - 3000:3000
    stdin_open: true
    tty: true
  db:
    image: postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
  redis:
    image: redis:latest
    ports:
      - 6379:6379
    volumes:
      - redis:/data
  chrome:
    image: selenium/standalone-chrome-debug:latest
    ports:
      - 4444:4444
      - 5901:5900

volumes:
  bundle:
  postgres_data:
  node_modules:
  redis:

FukuiFukui

GitHub ActionsでCIを実装する

コンテナ準備

  • postgresのコンテナを利用
    • portをpostgresのデフォルトである5432に設定
  • ruby3.3.3のコンテナを利用
    • docker-composeを使用しないため、postgres用の環境変数を別途渡してやる(envキー内にて定義しておく)

nodeとyarnをインストール

  • nodeのインストールとyarn installを実行
      - name: Install Node.js
        run: |
          curl -sL https://deb.nodesource.com/setup_20.x | bash -
          apt-get install -y nodejs
      - name: Install Yarn
        run: |
          curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
          echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
          apt-get update && apt-get install -y yarn

bundle installのエラーを解消する

  • エラー発生…
Downloading net-pop-0.1.2 revealed dependencies not in the API or the lockfile
(net-protocol (>= 0)).
Running bundle update net-pop should fix the problem.
Error: Process completed with exit code 34.

DBのエラーを解消する

  • まだモデル等を作っていないためschema.rbが存在せず、db:createがエラーになる
    • 空のschema.rbを作成

アセットのコンパイルを行う

  • テスト環境はbin/devで起動していないため、アセットのコンパイルを行う
- name: Precompile assets
        run: |
          RAILS_ENV=test bundle exec rails assets:precompile
  • config/environments/test.rbにも定義
config.assets.debug = true
config.assets.compile = true

以上!

完成形

ci.yml
name: Test
on: [push]
jobs:
  rspec:
    runs-on: ubuntu-latest
    services:
      db:
        image: postgres:latest
        ports:
          - 5432:5432
        env:
          TZ: "Asia/Tokyo"
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: password
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    container:
      image: ruby:3.3.3
    env:
      RAILS_ENV: test
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    steps:
      - uses: actions/checkout@v2
      - name: Install chrome and yarn
        run: |
          wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
          echo 'deb http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list
          apt update -y
          apt install -y google-chrome-stable yarn
      - name: Install Node.js
        run: |
          curl -sL https://deb.nodesource.com/setup_20.x | bash -
          apt-get install -y nodejs
      - name: Install Yarn
        run: |
          curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
          echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
          apt-get update && apt-get install -y yarn
      - name: bundler config
        run: bundle config set path 'vendor/bundle'
      - name: cache gems
        id: cache-gems
        uses: actions/cache@v2
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-gems-
      - name: setup bundle
        if: steps.cache-gems.outputs.cache-hit != 'true'
        run: |
          bundle install --jobs 4 --retry 3
      - name: install node.js dependencies
        run: yarn install
      - name: Wait for DB
        run: sleep 10
      - name: setup db schema
        run: |
          bundle exec rails db:create db:schema:load --trace
      - name: Precompile assets
        run: |
          RAILS_ENV=test bundle exec rails assets:precompile
      - name: run spec
        run: bundle exec rspec
      - name: Archive rspec result screenshots
        if: failure() 
        uses: actions/upload-artifact@v3
        with:
          name: rspec result screenshots
          path: |
            tmp/screenshots/
            tmp/capybara/
  rubocop:
    runs-on: ubuntu-latest
    container:
      image: ruby:3.3.3
    steps:
      - uses: actions/checkout@v2
      - name: bundler config
        run: bundle config set path 'vendor/bundle'
      - name: Cache gems
        id: cache-gems
        uses: actions/cache@v2
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-gems-
      - name: setup bundle
        if: steps.cache-gems.outputs.cache-hit != 'true'
        run: bundle install --jobs 4 --retry 3
      - name: run rubocop
        run: bundle exec rubocop
  erb-linter:
    runs-on: ubuntu-latest
    container:
      image: ruby:3.3.3
    steps:
      - uses: actions/checkout@v2
      - name: bundler config
        run: bundle config set path 'vendor/bundle'
      - name: Cache gems
        id: cache-gems
        uses: actions/cache@v2
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-gems-
      - name: setup bundle
        if: steps.cache-gems.outputs.cache-hit != 'true'
        run: bundle install --jobs 4 --retry 3
      - name: run erblint
        run: bundle exec erblint .
FukuiFukui

CIコンテナ起動時にcredentialsが読み込まれずエラーになる問題

GitHub ActionsでCredentialsを利用する

ci.yml
env:
      RAILS_ENV: test
      RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
FukuiFukui

ミニアプリでよく使うgem導入

rubocop

  • gemfileのdevelopment groupに以下追加
    • rubocop
    • rubocop-rails
  • rubocop --auto-gen-configを実行し.rubocop_todo.yml.rubocop.ymlを作成
    • todoは規約の回避を定義しておける(後で直すよ、を定義しておける的な)
    • auto-genで作成されるtodoは最初からGemfileなどがexcludeされている。空にしてやると全ての規約が適用されるので空にしてあげる。
  • .rubocop.ymlに規約や除外ファイルを書いていく
  • rubocopで検知できるか確認!

rspec

  • gemfileのdevelopment, test groupに追加
    • gem 'rspec-rails'
  • rspec --initを実行し.rspecspec/spec_helper.rbを作成
  • rails generate rspec:installspec/rails_helper.rbを作成 ※spec_helperも再作成される

factory bot

  • gemfileにdevelopment, test groupに追加
    • gem 'factory_bot_rails'
  • spec/support/factory_bot.rbを作成し以下を記載
  • spec/rails_helper.rbのsupport配下の設定ファイルの読み込みのコメントアウトを外す
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

capybara

  • gemfileのtest groupに追加(railsのバージョンによってはrails newで最初から入ってるかも)
    • gem 'capybara'
    • gem 'selenium-webdriver'
  • spec/support/capybara.rbを作成しドライバーの設定を追加(ここらへんはコピペする)
  • dockerの場合chromeコンテナを別途用意し環境変数など設定しておく

一度Rspecとcapybaraをテスト実行する

  • rails g controller posts indexを実行しcontrollerとviewを作成
  • routes.rbにルーティング定義
  • rails g rspec:system posts_indexを実行しシステムスペック作成
  • rspecコマンドでテストが通るか確認

erb-lint

  • rubocopと同じgroupにgemを追加
    • gem 'erb_lint', require: false
  • .erb-lint.ymlを作成(これも一旦設定はコピペする)

annotate

  • development, test groupに追加
    • gem 'annotate'
  • rails g annotate:installで設定ファイル追加
  • .rubocop.ymlのexcludeに以下を追加
    • - 'lib/tasks/auto_annotate_models.rake'

i18n

  • gem 'rails-i18n'を追加
  • config/application.rbconfig.i18n.default_locale = :jaを追加
  • ついでにタイムゾーンの設定もやっちゃう
    • config.time_zone = 'Tokyo'
    • config.active_record.default_timezone = :local
  • config/locales/ja.ymlを作成しロケールを定義

config

  • gem 'config'を追加
  • rails g config:installで設定ファイルを作成
  • config/settings/*に環境毎の設定ファイルが作成される(yml形式)
default_url_options:
  host: 'localhost:3000'
  • 各ファイルにてSettings.key.key...で定数を呼び出すことができる
    • 上記の例だとSettings.default_url_options.host

draper

  • bundle install
  • rails generate draper:install
  • rails generate decorator User
  • @user.decorate.メソッド名でviewでメソッドを呼び出し
FukuiFukui

turbo frame tag内からDELETEリクエストを飛ばし且つリダイレクトさせる

  • turbo_method: :deleteを加えるとturboリクエストになる(当たり前だが)
  • したがって仮にトップにリダイレクトしてもturboリクエストのままになり、「Content Missing」になる
  • じゃあturbo無効にしてmethod: :delete, data: {turbo: false}を加えると、GETリクエストになっちゃう
  • turbo frame tag外に配置すると無事動作した

button_to

  • method: :delete, data: {turbo: false}を加えても問題なく動作した

同じような人発見

https://abillyz.com/moco/studies/612

FukuiFukui

リクエストスペックで変数を参照する場合

これ👉controller.instance_variable_get("@hoge")

FukuiFukui

なぜ 別途定義したhas_one にも dependent: :destroy が必要なのか

アソシエーションの挙動

  • has_many :authentications, dependent: :destroy: この設定では、User が削除されると、その User に関連付けられたすべての Authentication オブジェクトが削除されます。
  • has_one :line_authentication, dependent: :destroy: ここでは、User が削除されると line_authentication が削除されることを保証します。

理由

  1. 個別の管理が可能: has_many は複数の関連を持つ場合に一括して扱いますが、特定の関連(この場合は line_authentication)を個別に管理したい場合があります。line_authentication は特定の provider: 'line' に限定された関連であり、特別なロジックや処理が必要な場合には個別に dependent オプションを指定することで、明示的に削除の制御が可能になります。

  2. 意図の明示: コードの可読性や保守性の観点から、特定の目的を持った関連を明示的に定義することで、後から見た開発者がその意図を理解しやすくなります。

  3. 別々の削除ポリシー: 例えば、他の関連に対して異なる削除ポリシーを設定したい場合には、has_onedependent オプションが必要です。has_many が全体に対して適用されるのに対し、has_one では特定の関連に対して独自のポリシーを設定できます。

なぜ has_many :authentications には inverse_of が不要なのか

inverse_of の役割

  • inverse_of オプションは、双方向の関連付けを持つモデル間でキャッシュを使用してパフォーマンスを向上させるためのオプションです。

必要性

  1. デフォルトの挙動: Rails は、has_manybelongs_to のような基本的な関連付けでは、デフォルトで双方向の関連付けを解決します。inverse_of を明示的に指定しなくても、User オブジェクトを通じて Authentication オブジェクトにアクセスし、その逆も可能です。

  2. パフォーマンスの観点: 大きなデータセットを扱う場合に、inverse_of を設定することで、データベースへの不必要な問い合わせを削減し、パフォーマンスを向上させることができます。しかし、基本的な has_many の関連付けではこの恩恵が少なく、明示的な設定が必要でない場合もあります。

  3. 要件の違い: inverse_of は主に複雑な関連付けや条件付き関連付けに対して使用されることが多く、通常の has_many の関連付けには必ずしも必要ではありません。

特定の関連で inverse_of が必要なケース

  • 条件付き関連付け: 例えば、has_one :line_authentication, -> { where(provider: 'line') } のような条件付き関連付けの場合、inverse_of が指定されていないと、Rails が関連付けを正確に理解できないことがあります。

  • 複数の関連付けがある場合: 同じモデルを指す複数の関連付けがある場合、Rails が逆方向の関連付けを誤解する可能性があるため、inverse_of を指定することがあります。

FukuiFukui

1. カスタムエラーページの作成

まず、カスタムエラーページのビューを作成します。public/ディレクトリに静的なHTMLファイルを作成する方法と、Railsのビューでダイナミックなページを作成する方法があります。ここではRailsのビューを使った方法を説明します。

1.1 ビューの作成

app/views/errors/ディレクトリを作成し、そこに以下のようにエラーページのテンプレートファイルを作成します。

mkdir app/views/errors
  • 404エラーページ: app/views/errors/not_found.html.erb
  • 500エラーページ: app/views/errors/internal_server_error.html.erb

それぞれのファイルにカスタムエラーページのHTMLを記述します。

例: app/views/errors/not_found.html.erb

<h1>ページが見つかりません</h1>
<p>お探しのページは存在しないか、削除された可能性があります。</p>

例: app/views/errors/internal_server_error.html.erb

<h1>内部サーバーエラー</h1>
<p>サーバー内部でエラーが発生しました。しばらくしてから再度お試しください。</p>

2. エラーハンドラーの設定

次に、Railsにエラーハンドリングを行わせるように設定します。application_controller.rbに以下のコードを追加します。

class ApplicationController < ActionController::Base
  # 404エラー
  rescue_from ActiveRecord::RecordNotFound, with: :render_404
  rescue_from ActionController::RoutingError, with: :render_404

  # 500エラー
  rescue_from StandardError, with: :render_500

  def render_404
    render template: 'errors/not_found', status: 404
  end

  def render_500(exception)
    logger.error exception.message
    logger.error exception.backtrace.join("\n")
    render template: 'errors/internal_server_error', status: 500
  end
end

3. ルーティングの設定

Railsに対して、ルーティングエラーをハンドリングする設定を行います。config/routes.rbに以下のコードを追加します。

Rails.application.routes.draw do
  # 他のルーティングがある場所

  # ルーティングエラーをキャッチ
  match '*path', to: 'application#render_404', via: :all
end

4. エラーを発生させる設定

デフォルトでは、Railsは開発モードでエラーのスタックトレースを表示します。カスタムエラーページを確認するには、config/environments/production.rbに以下の設定を追加するか、config/environments/development.rbでも一時的に有効にすることができます。

config.consider_all_requests_local = false

5. public/フォルダの静的ページを利用する場合

本番環境でpublic/404.htmlpublic/500.htmlを利用する場合、これらのファイルを上書きすることで、Railsが自動的にエラーページを表示します。これらのファイルは静的なHTMLページです。

まとめ

  • カスタムビューを作成: app/views/errors/にエラーページを作成。
  • エラーハンドリングを設定: application_controller.rbで404と500のエラーを処理。
  • ルーティング設定: config/routes.rbでルーティングエラーをキャッチ。

これで、404や500系のエラーが発生した際に、カスタムしたエラーページが表示されるようになります。

FukuiFukui

テストで機能をスタブする

  • スタブの目的:

    • スタブ(stub)とは、テスト中に特定のメソッドの呼び出しを置き換えることで、外部の依存関係を排除し、テストを簡素化するために使用されます。
    • 外部API、データベースアクセス、時間のかかる処理など、テストで直接実行したくない部分をスタブすることが多いです。
  • スタブの定義方法:

    • allow(テスト対象のオブジェクト).to receive(:スタブするメソッド).and_return(スタブの戻り値)
    • 上記の構文で、特定のオブジェクトのメソッド呼び出しをスタブして、テスト中に固定の結果を返すように設定します。
  • スタブの種類と方法:

    • スタブにはいくつかの方法があり、状況に応じて選択します。
    1. 基本的なスタブ:

      allow(object).to receive(:method_name).and_return(value)
      
      • これは特定のobjectに対してmethod_nameをスタブし、valueを返すように設定します。
    2. インスタンススタブ:

      stub = instance_double(ClassName)
      allow(ClassName).to receive(:new).and_return(stub)
      
      • instance_doubleを使用してクラスのインスタンスをスタブし、クラスのnewメソッドが呼び出されたときにスタブを返すように設定します。
    3. クラススタブ:

      • クラスそのものに対してスタブを設定することも可能です。
    4. allow_any_instance_ofの使用:

      allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
      
      • これは特定のクラスの任意のインスタンスでメソッドをスタブします。
      • 注意点: allow_any_instance_ofは強力ですが、過度に使用するとテストの意図が不明瞭になる可能性があるため、適切な場面でのみ使用することをお勧めします。
FukuiFukui

turbo frame tag内に配置したリンクでDELETEリクエストを飛ばし、destroyアクションを経由してトップにリダイレクトする

  • link_to
    • turbo_method: :deleteを加えるとturboリクエストになる(当たり前だが)
    • トップにリダイレクトしてもturboリクエストのままになり、「Content Missing」になる
    • method: :delete, data: {turbo: false}を加えるとGETリクエストになっちゃう
    • turbo frame tag外に配置すると無事動作した
  • button_to
    • method: :delete, data: {turbo: false}を加えても問題なく動作した
  • 同じような人発見

結果、data: { turbo_frame: "_top" }をつける

https://zenn.dev/shita1112/books/cat-hotwire-turbo/viewer/turbo-frames#_topをターゲットにする

FukuiFukui

リクエストスペックでログインユーザーをスタブする

結論、allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)current_userを返すスタブを作成する。

ChatGPTのわかりやすい解説

以下に、allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)の意味と、スタブについて分かりやすく説明します。

スタブとは?

スタブとは、テスト中に特定のメソッドが呼ばれたときに、通常の処理ではなく、決まった結果を返すように設定するものです。スタブを使うことで、実際のメソッドの処理を行わずに、テストしたい機能に必要な状態を簡単に作り出すことができます。

例えば、データベースからユーザー情報を取得するメソッドがあるとします。このメソッドをテストする際に、データベースにアクセスせずにテストを実行したい場合、スタブを使ってそのメソッドが常に決まったユーザー情報を返すように設定できます。

allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)の意味

このコードは、以下のような動作を設定します:

  1. allow_any_instance_of(ApplicationController):

    • これは、ApplicationControllerクラスの「どんなインスタンス(実際のオブジェクト)でも」という意味です。
    • つまり、ApplicationControllerから作られたどのオブジェクトでも対象になります。
  2. .to receive(:current_user):

    • これは、current_userというメソッドが呼ばれるときにどうするかを指定します。
    • current_userは、通常はログインしているユーザーを返すメソッドです。
  3. .and_return(user):

    • これは、current_userメソッドが呼ばれたときに、実際のユーザーオブジェクトを返すように設定します。
    • userは、テスト用に準備されたユーザーオブジェクトです。

例を使った説明

想像してみてください。テストしているアプリケーションには、ユーザーがログインしているかどうかを確認するためにcurrent_userというメソッドがあります。このメソッドは、通常はデータベースからユーザー情報を取り出して返しますが、テスト中にデータベースにアクセスするのは面倒ですし、時間がかかることもあります。

そこで、allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)という設定を使うと、テスト中はcurrent_userメソッドが実際のデータベースアクセスを行わず、代わりにテスト用に設定したuserオブジェクトを返すようになります。これにより、テストが簡単になり、より迅速に実行できるようになります。

まとめ

  • スタブ: メソッドが呼ばれたときに、特定の決まった結果を返すようにすること。
  • allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user): ApplicationControllerクラスのどのインスタンスでも、current_userメソッドを呼ぶと、テスト用に準備されたuserオブジェクトを返すようにする設定。

この方法で、テスト中にユーザー情報を簡単に設定し、テストを効率的に実行することができます。