Ruby, Rails関連
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]
ぼっち演算子
[オブジェクト]&.[メソッド]
- オブジェクトがnilでも例外が発生せず処理が継続する
- オブジェクトがnilの場合に
noMethod
エラーになりがちだが、それを回避できる
!を付けるときとは
- よく使うところとして、以下のメソッドは失敗しても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を使いましょう。
controllerで定義したメソッドをviewで使う
-
helper_method
メソッドにメソッド名をシンボルで渡すとviewで利用可能になる
helper_method :current_user
private
def current_user
hogehoge
end
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
と複数形になっていることに注意
-
dependent: :destroy
どういうことか
- Userモデルとそれに紐づくPostモデルがあるとする
- Userモデルには
has_many :posts
が定義されている - Postには
belongs_to :user
が定義されている
- 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
- 親レコードを削除しようとすると例外を生じさせる
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_tag
はform_with
と同じような感じでフォームヘルパーを利用できる - ただし第一引数にカラム
:name
や属性取得のメソッド:hogehoge_ids
とかではなく検索条件が入る-
:title_or_body_or_answers_body_cont
とか
-
- 検索条件はアソシエーションも考慮できる
- Postレコードに対して、それに紐づくCommentsレコードのカラムに対して検索する場合
<%= f.search_field :comments_body_cont %>
- https://activerecord-hackery.github.io/ransack/going-further/associations/
- リクエスト先のurlを指定もできる
-
<%= search_form_for @q, url: root_url do |f| %>
とか
-
N+1対策(工事中)
どういうことか
工事中
どうすればいいのか
- controllerでレコードを取得する際に工夫する
- Before
- controller:
@users = User.all
- view:eachメソッドにて
user.posts
とかuser.image
を回す
- controller:
- After
- contllerで以下メソッドをチェーンさせる
- includesメソッドで関連するテーブルをまとめて取得する
User.includes(:posts, :comments)
- (ActiveStorage)with_attached_imagesメソッドを利用する
- includesメソッドで関連するテーブルをまとめて取得する
- contllerで以下メソッドをチェーンさせる
- Before
具体例
@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)
よく使うルーティング(工事中)
member
collection
module
namespace
resource/resources
param: :column
デフォルトの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
Mailer
Basic認証
def basic_auth
authenticate_or_request_with_http_basic do |username, password|
username == 'admin' && password == 'password'
end
end
これをbefore_actionだかなんだかで仕込んであげる。
ランダムな数字を出す
SecureRandomモジュール
SecureRandom.base64(10)
-
SecureRandom.hex(16)
- ランダムなhex文字列(16進数文字列)を生成する
SecureRandom.uuid
Base64モジュール
-
Base64.strict_encode64(@user.uuid)
- 改行コードをエンコードしない
Credentials
登録
-
EDITOR="vi" bin/rails credentials:edit
でエディタを開く
呼び出し
-
Rails.application.credentials.hogehoge.fugafuga
で呼び出せる
注意点
- ローカルの
master.key
を使用して暗号化と復号を行う - そして
master.key
はgitignoreされている - つまり
git clone
してきてもmaster.key
が無いため復号できない - sshで接続してローカルの
master.key
をコピーするか、新しく作成しなおすかする必要がある
環境変数の取り出し
ENV['REDIS_URL']
キューイング
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じゃないとだめ
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
url系のhelperをmodelとconsoleでも使う
rails console
-
app.order_unit_path(order_unit.code)
とかでconsoleでも出力できる
model内で使用する場合
-
Rails.application.routes.url_helpers.order_unit_path
など
リンクを発行する時のあれこれ
クエリパラメーター付きリンク
-
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
※例
- =>
DockerでRails7, PostgreSQL, Tailwind, DaisyUIの環境構築
- 気をつけること
- DaisyUIがImportmapに対応していないためesbuildを用いて
yarn add
すること -
rails new
時点でtailwind
とesbuild
は導入しておくこと -
bin/dev
でサーバー起動すること - じゃないと後ほどごちゃる
- DaisyUIがImportmapに対応していないためesbuildを用いて
- ファイル準備
- Dockerfile
- yarn関連の記述を一旦コメントアウト
- compose.yml
- bundleとnode_modulesはボリュームマウントにしておくこと
-
bundle install
やyarn install
してもローカルのフォルダをマウントしていることによって上書きされてしまうため
- entrypoint.sh
- Gemfile(gem 'rails'のみ)
- Gemfile.lock(空)
- Dockerfile
- 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になっている
- Procfile.devに定義されている
- 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
完成形
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"]
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:
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.
- 上記はruby3.3.3のバグっぽい
- https://techracho.bpsinc.jp/hachi8833/2024_06_17/142559
- URLに従ってGemfile.lockにnet-protocolを依存関係に追加
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
以上!
完成形
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 .
CIコンテナ起動時にcredentialsが読み込まれずエラーになる問題
GitHub ActionsでCredentialsを利用する
- 以下に従い、リポジトリにscretsを登録する。
- master.keyを登録。
- ワークフローに環境変数を渡す。
secrets.RAILS_MASTER_KEY
でsecretsを呼び出せる
env:
RAILS_ENV: test
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
ミニアプリでよく使う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
を実行し.rspec
とspec/spec_helper.rb
を作成 -
rails generate rspec:install
でspec/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.rb
にconfig.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でメソッドを呼び出し
turbo frame tag内からDELETEリクエストを飛ばし且つリダイレクトさせる
link_to
-
turbo_method: :delete
を加えるとturboリクエストになる(当たり前だが) - したがって仮にトップにリダイレクトしてもturboリクエストのままになり、「Content Missing」になる
- じゃあturbo無効にして
method: :delete, data: {turbo: false}
を加えると、GETリクエストになっちゃう - turbo frame tag外に配置すると無事動作した
button_to
-
method: :delete, data: {turbo: false}
を加えても問題なく動作した
同じような人発見
リクエストスペックで変数を参照する場合
これ👉controller.instance_variable_get("@hoge")
has_one
にも dependent: :destroy
が必要なのか
なぜ 別途定義したアソシエーションの挙動
-
has_many :authentications, dependent: :destroy
: この設定では、User
が削除されると、そのUser
に関連付けられたすべてのAuthentication
オブジェクトが削除されます。 -
has_one :line_authentication, dependent: :destroy
: ここでは、User
が削除されるとline_authentication
が削除されることを保証します。
理由
-
個別の管理が可能:
has_many
は複数の関連を持つ場合に一括して扱いますが、特定の関連(この場合はline_authentication
)を個別に管理したい場合があります。line_authentication
は特定のprovider: 'line'
に限定された関連であり、特別なロジックや処理が必要な場合には個別にdependent
オプションを指定することで、明示的に削除の制御が可能になります。 -
意図の明示: コードの可読性や保守性の観点から、特定の目的を持った関連を明示的に定義することで、後から見た開発者がその意図を理解しやすくなります。
-
別々の削除ポリシー: 例えば、他の関連に対して異なる削除ポリシーを設定したい場合には、
has_one
のdependent
オプションが必要です。has_many
が全体に対して適用されるのに対し、has_one
では特定の関連に対して独自のポリシーを設定できます。
has_many :authentications
には inverse_of
が不要なのか
なぜ
inverse_of
の役割
-
inverse_of
オプションは、双方向の関連付けを持つモデル間でキャッシュを使用してパフォーマンスを向上させるためのオプションです。
必要性
-
デフォルトの挙動: Rails は、
has_many
やbelongs_to
のような基本的な関連付けでは、デフォルトで双方向の関連付けを解決します。inverse_of
を明示的に指定しなくても、User
オブジェクトを通じてAuthentication
オブジェクトにアクセスし、その逆も可能です。 -
パフォーマンスの観点: 大きなデータセットを扱う場合に、
inverse_of
を設定することで、データベースへの不必要な問い合わせを削減し、パフォーマンスを向上させることができます。しかし、基本的なhas_many
の関連付けではこの恩恵が少なく、明示的な設定が必要でない場合もあります。 -
要件の違い:
inverse_of
は主に複雑な関連付けや条件付き関連付けに対して使用されることが多く、通常のhas_many
の関連付けには必ずしも必要ではありません。
inverse_of
が必要なケース
特定の関連で -
条件付き関連付け: 例えば、
has_one :line_authentication, -> { where(provider: 'line') }
のような条件付き関連付けの場合、inverse_of
が指定されていないと、Rails が関連付けを正確に理解できないことがあります。 -
複数の関連付けがある場合: 同じモデルを指す複数の関連付けがある場合、Rails が逆方向の関連付けを誤解する可能性があるため、
inverse_of
を指定することがあります。
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.html
やpublic/500.html
を利用する場合、これらのファイルを上書きすることで、Railsが自動的にエラーページを表示します。これらのファイルは静的なHTMLページです。
まとめ
-
カスタムビューを作成:
app/views/errors/
にエラーページを作成。 -
エラーハンドリングを設定:
application_controller.rb
で404と500のエラーを処理。 -
ルーティング設定:
config/routes.rb
でルーティングエラーをキャッチ。
これで、404や500系のエラーが発生した際に、カスタムしたエラーページが表示されるようになります。
テストで機能をスタブする
-
スタブの目的:
- スタブ(stub)とは、テスト中に特定のメソッドの呼び出しを置き換えることで、外部の依存関係を排除し、テストを簡素化するために使用されます。
- 外部API、データベースアクセス、時間のかかる処理など、テストで直接実行したくない部分をスタブすることが多いです。
-
スタブの定義方法:
allow(テスト対象のオブジェクト).to receive(:スタブするメソッド).and_return(スタブの戻り値)
- 上記の構文で、特定のオブジェクトのメソッド呼び出しをスタブして、テスト中に固定の結果を返すように設定します。
-
スタブの種類と方法:
- スタブにはいくつかの方法があり、状況に応じて選択します。
-
基本的なスタブ:
allow(object).to receive(:method_name).and_return(value)
- これは特定の
object
に対してmethod_name
をスタブし、value
を返すように設定します。
- これは特定の
-
インスタンススタブ:
stub = instance_double(ClassName) allow(ClassName).to receive(:new).and_return(stub)
-
instance_double
を使用してクラスのインスタンスをスタブし、クラスのnew
メソッドが呼び出されたときにスタブを返すように設定します。
-
-
クラススタブ:
- クラスそのものに対してスタブを設定することも可能です。
-
allow_any_instance_of
の使用:allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
- これは特定のクラスの任意のインスタンスでメソッドをスタブします。
-
注意点:
allow_any_instance_of
は強力ですが、過度に使用するとテストの意図が不明瞭になる可能性があるため、適切な場面でのみ使用することをお勧めします。
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" }をつける
リクエストスペックでログインユーザーをスタブする
結論、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)
の意味
このコードは、以下のような動作を設定します:
-
allow_any_instance_of(ApplicationController)
:- これは、
ApplicationController
クラスの「どんなインスタンス(実際のオブジェクト)でも」という意味です。 - つまり、
ApplicationController
から作られたどのオブジェクトでも対象になります。
- これは、
-
.to receive(:current_user)
:- これは、
current_user
というメソッドが呼ばれるときにどうするかを指定します。 -
current_user
は、通常はログインしているユーザーを返すメソッドです。
- これは、
-
.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
オブジェクトを返すようにする設定。
この方法で、テスト中にユーザー情報を簡単に設定し、テストを効率的に実行することができます。