開発日記~日々のアウトプット~

学習したことを忘れないために記録して行きます。

axiosでヘッダーにトークンを設定してリクエストを送る方法

RailsApiとReactでWebアプリを作成しており、
認証機能はdevise_token_authを使って実装していました。

devise_token_authで認証するには、access-tokenclientuidという
3つの要素をリクエストヘッダを使ってhttpリクエストを送らないといけないのですが、
axiosではどうやるのか分からなかったので、調べてみました。

axios.createを使う

トークンをヘッダを使ってリクエストを送る処理は何回も利用すると思うので、
ローカルストレージ からトークンを取得してヘッダに設定するという前処理を
通化しようと試みました。

import axios from "axios";

export const secureHTTP = axios.create({
  baseURL: process.env.REACT_APP_API_HOST,
  headers: {
    "access-token": localStorage.getItem("access-token"),
     "client": localStorage.getItem("client"),
     "uid": localStorage.getItem("uid")
  }
});

こんな感じで新しくインスタンスを作成して、これをimportしてリクエストを送信すると、
ちゃんとAPIからデータを取得することが出来ました。

しかし、このコードでどハマりしました。。。

ハマった所

問題が発生したのは、ログイン直後、この secureHTTP を使って getリクエストを送った時です。なぜかヘッダにトークンが設定されず 401エラーが返ってきてしまいます。 ちなみにページを更新すると、普通にリクエストは成功します。

interceptersを使う

axios.intercepters を利用することで、この問題は解消されました。

intercepters はリクエストの前処理やレスポンス後の処理を共通化する際に
使うとのことだったので早速試してみることに。

最終的なコード↓

const getToken = () => {
  const accessToken = localStorage.getItem("access-token");
  const client = localStorage.getItem("client");
  const uid = localStorage.getItem("uid");

  if (accessToken && client && uid) {
    return {
      "access-token": accessToken,
      client: client,
      uid: uid
    };
  }
  return null;
};

secureHTTP.intercepters.request.use(config => {
  const authToken = getToken();

  if (authToken) {
    config.headers = authToken;
  }
  return config;
});

こちらのコードではログイン直後でもページを更新せずとも リクエストは成功しました。

ちゃんと動いてくれたのは良いんだけど、 どうして前のコードじゃダメなのかモヤモヤ。。。

Reactを使った開発は初めてなのですが、 エラーを調査して解消できても
なぜ上手くいったのか分からないところがあったりするので 少しずつ疑問点を解消したい。

参考にした記事

github.com

RailsアプリをCirclCIでビルドする

はじめに

現在個人開発しているアプリでCircleCIを活用して CI/CD環境を構築しようと挑戦中なのですが、とりあえず ビルド・テストまで出来たのでまとめてみます。

ホントはデプロイまで自動化したいのですが、まだそこまで行っていないので この記事ではビルド・テストを実行できることをひとまず目標にします。

CircleCIの良い所

個人的に使ってみて良かったところは ローカル実行環境があるところです。

一番最初に使ったときはそんなこと知らずプッシュ → エラーが出たら それを修正してまたプッシュという不毛なことをしていたのですが、 実は circleci コマンドというものがあってこれで動作確認できます。

これで動作確認していないコードをコミットせずに済みます。

環境構築

次の手順で環境を構築して動作確認までします。

  1. circleci cliをインストール
  2. 設定ファイルを作成
  3. ローカルで動作確認してみる

circleci cliをインストール

brewを使ってインストールします。

brew update
brew install circleci

これでローカルで動作確認できるようになりました。 次は設定ファイルを作成します。

設定ファイルを作成

まず、次のコマンドで設定ファイルを作成します。

mkdir .circleci
touch config.yml

この config.yml に設定を書いて行きます。 全部書くと結構な量になるので区切って解説して行きます。

version: 2
jobs:
  build:
    docker: 
      - image: circleci/ruby:2.6.5
        environment: 
          RAILS_ENV: test
         DB_HOST: 127.0.0.1
      - image: circleci/mysql:5.7
        environment:
          MYSQL_USER: root
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
    working_directory: ~/repo

CircleCIはDocker環境でCIを実行するので、Railsアプリをビルドするために 必要なDockerイメージを用意します。イメージはCircleCIが予め用意してくれているので 上記のようにバージョンを指定するだけでおkです。

またdocker-composeのように複数のイメージを指定することもできます。 僕はDBにMySQLを採用したのでMySQLのイメージも準備しました。

次にenvironment キーで環境変数を定義します。 ポイントはhostに 127.0.0.1 を指定すること、RSpecでテストを 実行するので、RAILS_ENV にtestを指定することです。

次に作業ディレクトリを working_directory で指定します。 デフォルトでは ~/project になってます。 Githubで取得したソースコードはこの中に入ります。

次は、Railsアプリのビルド設定を書いて行きます。 working_directoryキーの下に steps キーを書きます。

(省略)
    working_directory: ~/repo
    steps:
      - chekout

      - restore_cache:
          key: v1-dependecies-{{ checksum "Gemfile.lock" }}
      - run:
          name: bundle install
          command: bundle install --jobs=4 --retry=3 --path vendor/bundle
      - save_cache:
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
          paths:
            - ./vendor/bundle
      - run:
          name: db setup
          command: |
            bundle exec rake db:create
            bundle exec rake db:schema:load
      - run:
          name: run test
          command: bundle exec rspec

上から順に解説して行きます。

checkout

このステップでリポジトリソースコードを取得します。 working_directoryで指定したディレクトリ、ここでは~/repoに あなたのリポジトリ のファイルが展開されます。

save_cacheとrestore_cache

まず、railsアプリをビルドするには当然gemをbundle install する必要あるわけですが、 毎回CIでビルドする度、実行するのは時間がかかり過ぎてだるくないですか?

そこで一番最初にビルドするときは、普通に bundle install して その結果を save_cache でキャッシュします。そして次回ビルドするときは キャッシュを利用することで一々 bundle install せずに済むようになります。

save_cache ステップには paths キーと key キーを指定します。 paths キーにはキャッシュに追加するファイルを、 key キーにはキャッシュの識別名を指定します。

この識別名には利用できるテンプレートがあり、ここでは {{ checksum "ファイル名" }} というテンプレートを使います。 これはファイルの内容をbase64エンコードしたSHA256ハッシュ です。

キャッシュを復元するには restore_cache を利用します。 restore_cache ステップには key または keys キーを指定します。

run

run ステップではシェルを通じて実行するコマンドを指定します。 name キーには実行するコマンドのタイトルを指定できますが、 これはなくても構いません。 command キーに実行するコマンドを指定します。 ここでは依存関係をインストールしたり、データベースを準備したり テストを実行したりしています。

これで設定ファイルが完成しました。

ローカルで実行

次のコマンドで実行できます。

circleci local execute

このコマンドを実行すると、例えばymlファイルで文法の間違いがあったりすると それを教えてくれるので、必ずプッシュする前に実行しましょう。

最後に

次はCapistranoを使ってデプロイの自動化にも挑戦したいです。

devise_token_authでAPIの認証機能を高速で作る part2

devise_token_authでAPIの認証機能を高速で作る part1

前回ではユーザーの登録機能を作成しました。 今回はemailによるログイン機能を作成して行きます。

手順

  1. RSpecでrequestスペックを作成する
  2. controllerを作成してレンダリングをカスタマイズ
  3. ログアウト機能も一緒に作成

RSpecでテストを書く

ここでは次の方針でテストを書いて行きます。

# frozen_string_literal: true

require "rails_helper"

RSpec.describe "Sessions", type: :request do
  let(:user) { create :user }

  describe "POST /api/auth/sign_in" do
    context "when invalid params" do

      subject(:login_path) { post api_user_session_path, params: invalid_params }

      let(:invalid_params) { { email: "", password: "" } }

      it "returns 401 status code" do
        login_path

        expect(response).to have_http_status :unauthorized
      end

      it "returns errors json" do
        login_path

        expect(json).to include(
          "status" => 401,
          "message" => "メールアドレスかパスワードが間違っています。"
        )
      end
    end

    context "when valid params" do
      subject(:login_path) { post api_user_session_path, params: valid_params }

      let(:valid_params) { { email: user.email, password: user.password } }

      it "returns 200 status code" do
        login_path

        expect(response).to have_http_status :ok
      end

      it "gives you to access_token if login success" do
        login_path

        expect(response).to have_header("access-token")
      end
    end
  end
end

テストはこんな感じ。

sessions_controllerを作成

次はコントローラを作成します。

rails g controller api/auth/sessions

ルーティングを変更します。

# frozen_string_literal: true

Rails.application.routes.draw do
  namespace :api, format: "json" do
    mount_devise_token_auth_for "User", at: "auth", controllers: {
      registrations: "api/auth/registrations",
      sessions: "api/auth/sessions"
    }
  end
end

次はレンダリングをカスタマイズして行きます。

# frozen_string_literal: true

class Api::Auth::SessionsController < DeviseTokenAuth::SessionsController
  protected

  def render_create_success
    render json: @resource, status: :ok
  end

  def render_create_error_bad_credentials
    render json: { status: 401, message: "メールアドレスかパスワードが間違っています。" }, status: :unauthorized
  end
end

jsonの形式についてご自身のサービスに合わせて変更してください。 僕はベタに active_model_serializer というgemを使っています。

ログアウト出来るようにする

ログイン機能も作ったので、ログアウト機能も作ります。

こちらもテストから書いて行きます。

#  省略

describe "#DELETE /api/auth/sign_out" do
    subject(:login_path) { post api_user_session_path, params: valid_params }

    let(:valid_params) { { email: user.email, password: user.password } }

    context "when token is valid" do
      it "returns 204 status code" do
        auth_headers = user.create_new_auth_token
        delete destroy_api_user_session_path, headers: auth_headers

        expect(response).to have_http_status :no_content
      end
    end

    context "when token is invalid" do
      it "sould return 404 status code" do
        delete destroy_api_user_session_path

        expect(response).to have_http_status :not_found
     end
   end
 end

ここでのポイントはユーザーを特定するために アクセストークン をヘッダーに含めなければいけない という事です。

# トークンを生成
auth_headers = user.create_new_auth_token

# ヘッダーに入れる
delete destroy_api_user_session_path, headers: auth_headers

deviseが予め create_new_auth_token という便利メソッドを 提供してくれているので、めっちゃスマートに書けます。

テストコードはこれでおkです。 次はコントローラでレンダリングを変更します。

# 省略

def render_destroy_success
  head :no_content
end

これでテストはパスします。

以上でログイン・ログアウト機能の実装は完了です。

最後に

スタンダードな認証機能は実装できましたが、 今はSNSの認証がないとユーザーライクとは言えないので、 明日はgoogletwittergithubを用いた認証機能を 追加して行きます。

devise_token_authでAPIの認証機能を高速で作る part1

はじめに

現在個人で開発しているサービスの認証機能を作る必要が あったのですが、一から作るのは大変だし早くサービスの核の 部分を作り込みたかったので、devise_token_auth というgemを 採用することに。

開発の方針

  1. 認証に成功したらアクセストークンを返す
  2. クライアントはトークンをlocalStorageに保存
  3. クライアントは、Authorization Bearerヘッダでトークンを送信
  4. サーバはトークンを受け取り、ユーザーを認可する

最終的にはこんな感じの機能を実装して行きますが、 今日は devise のインストールとユーザーの登録機能 まで作ることにします。

1. devise_token_authの準備

gem "devise_token_auth

とGemfileに記載してインストール。

rails g devise_token_auth:install User auth

次にこちらのコマンドを実行。

生成されたmigrationファイルを編集して行きます。

class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[6.0]
  def change
    
    create_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

      ## 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 :avatar_url
      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

ここら辺は、最終的に自分が実装したい物に合わせて 修正してください。僕はシンプルな認証機能で良かったので こんな感じになりました。

rails db:migrate

ここで僕はなぜかエラーが出てしまったので修正して行きます。

# frozen_string_literal: true

class User < ActiveRecord::Base
  extend Devise::Models
  devise :database_authenticatable, :registerable,
         :rememberable, :validatable, :omniauthable
  include DeviseTokenAuth::Concerns::User

end

エラーの箇所は自動で生成されたUserモデルです。 確か deviseメソッド が定義されてませんよー的な 感じでした。

これは次の1行を追加することで解決できます。

extend Devise::Models

今度こそmigrate。

rails db:migrate

2. RSpecでrequestスペックを書く

先にテストを書いてから、それがパスするように アプリケーションを実装して行きます。

# frozen_string_literal: true

require "rails_helper"

RSpec.describe "Users", type: :request do
  describe "POST /users" do
    context "when params invalid" do
      subject(:post_users_path) { post api_user_registration_path, params: invalid_params }

      let(:invalid_params) do
        {
          name: "",
          email: "",
          password: ""
        }
      end

      it "returns 422 status code" do
        post_users_path
        expect(response).to have_http_status :unprocessable_entity
      end

      it "returns errors json" do
        post_users_path

        expect(json).to eq({
          "errors" => {
            "password" => [
              "can't be blank",
              "is too short (minimum is 8 characters)"
            ],
            "name" => [
              "can't be blank"
            ],
            "email" => [
              "can't be blank"
            ]
          }
        })
      end
    end

    context "when valid params" do
      subject(:post_users_path) { post api_user_registration_path, params: valid_params }

      let(:user) { build :user }
      let(:valid_params) do
        {
          name: user.name,
          email: user.email,
          password: user.password
        }
      end

      it "returns 201 status code" do
        post_users_path
        expect(response).to have_http_status :created
      end

      it "returns json body" do
        post_users_path
        expect(json).to include(
          "name" => user.name,
          "email" => user.email
        )
      end

      it "create a new user" do
        expect { post_users_path }.to change(User, :count).by(1)
      end
    end
  end
end

テストするのはレスポンスのステータスコードと ボディの内容です。

3. コントローラを作成

今の状態だとdeviseのデフォルトのコントローラが 使われてしまうので、rails g コマンドで作成します。

rails g controller api/auth/registrations

そしてこのコントローラへのルーティングを定義します。

# frozen_string_literal: true

Rails.application.routes.draw do
  namespace :api, format: "json" do
    mount_devise_token_auth_for "User", at: "auth", controllers: {
      registrations: "api/auth/registrations"
    }
  end
end

ここからレンダリングをカスタマイズしたり strong parameter を修正してUserモデルに追加した 属性を受け取れるようにして行きます。

# frozen_string_literal: true

class Api::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController
  def sign_up_params
    params.permit(:name, :email, :password)
  end

  protected

  def render_create_success
    render json: @resource, status: :created
  end

  def render_create_error
    errors = resource_errors.reject { |key| key == :full_messages }
    render json: { errors: errors }, status: :unprocessable_entity
  end
end

sign_up_params

このメソッドをオーバーライドして自分で追加した 属性を受け取れるようにします。

render_create_success(error)

このメソッドでレンダリングをカスタマイズ出来ます。

Postmanで動作確認

APIの開発に役立つPostmanというソフトウェアがあるので こちらを使って期待したレスポンスが返ってくるか確認したいと思います。

postman
postman

ちゃんと欲しいレスポンスが返ってきてますね。 ヘッダーにアクセストークン が入っているかも確認します。 むしろこっちが重要。

access-token

赤線が引いてある所が重要で、 この access-tokenclientuid を headerに入れてリクエストを送信することで 作成したユーザーのアカウントを編集したり リソースにアクセス出来ます。

最後に

以上でユーザーの登録機能の実装は完了になります。 ほとんどコードを書く事も無く作れてしまうので deviseさんはすごいですね。

次はログイン・ログアウト機能を作って行きます!!

devise_token_authでAPIの認証機能を高速で作る part2

[忘備録]DockerコンテナからローカルのMySQLに接続する(Rails)

やりたいこと

Dockerコンテナ上のRailsアプリをローカルの MySQLに接続する必要があったのですが、 詰まった所があったので、どうやってやったのか まとめて行きたいと思います。

手順全体

流れは以下のようになります。

  1. database.ymlを記述
  2. 環境変数をコンテナに挿し込む
  3. ローカルのMySQLの設定を変更

手順詳細

1. database.ymlを記述

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root

development:
  <<: *default
  database: server_development
  host: db

test:
  <<: *default
  database: server_test
  host: <%= ENV["DB_HOST"] %>
  socket: /tmp/mysql.sock

production:
  <<: *default
  database: server_production
  username: server
  password: <%= ENV["SERVER_DATABASE_PASSWORD"] %>

ポイントはテスト環境におけるhostの部分です。

環境変数を使うのはCircleCiでテストを実行するには、 hostは127.0.0.1 、socketは /tmp/mysql.sock と 書かなくていけないのに対し、ローカルでテストを 実行するには hostは localhost としないと行けないからです。

hostの違いを環境変数で解決する訳です。

2. 環境変数を用意する。

ルートディレクトリに .env のような 環境変数を管理するファイルを作成し、 次の変数を定義します。

DB_HOST=docker.for.mac.localhost

これをdocker-compose.ymlで読み込みます。

3. MySQLで認証エラーが発生

これで行ける!と思ったのですが、 Authentication plugin 'caching_sha2_password' cannot be loaded というエラーが発生。 (控え忘れたので正確にはもっと長かった)

どうもMySQL8.0になってから認証方式が変わったみたいで、 mysql_native_password から caching_sha2_password に変更されたみたいです。

4. MySQLの認証方式を変更

まず、各ユーザーの認証方式を確認します。

mysql> SELECT user,host,plugin FROM mysql.user;
user host plugin
root localhost caching_sha2_password

と表示されると思います。

次に認証方式を変更します。

mysql> ALTER USER ユーザー名@ホスト IDENTIFIED WITH mysql_native_password BY 'password';

5. MySQLのパスワードのポリシーを変更する

認証方式を変更するコマンドを入力すると もしかしたらパスワードのポリシーがうんたらかんたら と怒られるかもしれません。

ERROR 1819 (HY000): Your password does not satisfy the current policy requirements

僕は怒られました。。。

なのでこのポリシーをゆるくして行きます。 こちらのコマンドでポリシーを確認。

SHOW VARIABLES LIKE 'validate_password%';
Variable_name Value
validate_password.policy MEDIUM

と表示されました。

これを変更します。

set global validate_password_policy=LOW;

そしてもう一度、認証方式を変更するコマンドを入力します。 これで完了です。

最後に

以上が僕がDockerコンテナからローカルホストのMySQLに 接続するためにやったことになります。

これで快適なテスト環境が手に入りました〜

RailsAPI Reactアプリケーションの開発環境をDockerで構築する part-1

はじめに

RailsAPIのチュートリアルとReactのチュートリアルを完了したので とにかく何か作りたいという想いに駆られWebアプリを開発して行くことに。

ポートフォリオとしてこちらのサービスを開発したのですが その時何もアウトプットしなかったことをめっちゃ後悔しているので、開発の様子を 日記形式でアウトプットして行きたいと思います。

作りたいもの

僕は普段、開発に役立ちそうな記事やエラーの解決に役立った記事を Qiita, Evernote,はてブにストックしているのですが、ちょっとその量が 半端ないことになってまして、それらを一元管理出来るものを作って行きます!

今日の進行具合

今日の進展はこんな感じです。

  • React/Rails開発環境をDockerで構築
  • ユーザーの登録機能

開発環境の構築

開発環境はやっぱり構築したいものですよね。 自分のためにも作った開発環境をまとめてみます!

ちなみにディレクトリはこんな感じになります。

.
├── README.md
├── docker-compose.yml
├── .env
├── .gitignore
├── client
│   ├── Dockerfile
│   ├── README.md
│   ├── node_modules
│   ├── package.json
│   ├── public
│   ├── src
│   └── yarn.lock
└── server
    ├── Dockerfile
    ├── Gemfile
    ├── Gemfile.lock
    ├── README.md
    ├── Rakefile
    ├── app
    ├── bin
    ├── bundle
    ├── config
    ├── config.ru
    ├── db
    ├── lib
    ├── log
    ├── mysql-data
    ├── public
    ├── spec
    ├── storage
    ├── tmp
    └── vendor

まずはReactの開発環境から

Reactの開発環境は create-react-app でパパッと作ります。 (ちゃんとここら辺も自分で出来ないとな。。。)

npx create-react-app client

次にこのReactアプリをDockerコンテナで動かして行きます。

touch docker-compose.yml
cd client
touch Dockerfile

docker-compose.ymlの内容はこちら。

version "3"
services:
  client:
    build ./client
    command yarn start
    volumes:
      - ./client:/app/client
    ports:
      - "3000:3000"

Dockerfileはこちら。

FROM node:12.1.0

RUN mkdir -p /app/client

WORKDIR /app/client

・docker-compose.ymlの解説

buildキー は、このコンテナを./clientの中にある Dockerfileから構築することを意味します。

commandキー は、コンテナ起動後に実行するコマンドを指定します。 ここではyarn start と指定してReactアプリを起動させます。

volumesキー は、ホスト側の./clientをコンテナ側の/app/clientに マウントしてホスト側からコードを変更出来るようにします。

これがないと、ホスト側でコードを変更しても 一々イメージをbuildしてコンテナを起動しないと いけなくなります。

portsキー はコンテナの3000番ポートをホストの3000番ポートに マッピングします。

次はDockerfileの解説です。

Reactは動かすにはNode.jsが必要なので、 コンテナのベースとなるイメージにはnode を指定します。

次に作業用のディレクト/app/client を作成し、 WORKDIR でコンテナ内の作業ディレクトリを指定します。

次にコンテナを起動して行きます。

docker-compose build
docker-compose up

これで、localhost:3000 にアクセスし おなじみの画面が表示されれば完了となります。

これでReactの開発環境は整いました!

次はサーバーサイドであるRailsの開発環境を構築して行きます! が、ちょっと長くなるので分割します。

トランザクションコールバックについてまとめてみた

はじめに

Railsアプリケーションを作成している時に トランザクションコールバックを使う機会があったのですが、 いまいち理解出来ていなかったのでまとめてみました。

コールバックの概要

まず始めに、コールバックについて改めてRailsガイドを読み、 学習し直しました。

コールバックの解説に進む前にオブジェクトのライフサイクルに ついて知っておく必要があるので、そちらから説明します。

Railsアプリケーションを作成している時、Active Recordオブジェクトを createしたりdestroyしたりしますよね?

このcreate/update/destroyという一連の流れを オブジェクトのライフサイクルと呼びます。

そして、Active Recordはオブジェクトのライフサイクル期間における 特定の期間に呼び出されるメソッドを提供しており、これをコールバックと言います。

つまり、Active Recordオブジェクトが作成/更新/削除されるなどの イベントが発生した時に、常に実行されるメソッドを記述することが 出来るということです。

コールバックの解説はこれで終わりです。

コールバックにも様々なメソッドがあり、 次のようなものが提供されています。

before_validation
before_save
after_create

他にもたくさんありますがこの辺で。。。

さて、ここからトランザクションコールバックの 解説に移ります。

トランザクションコールバックとは

データベースのトランザクションが完了した時に トリガされるコールバックのことで、

これには2種類あります。

after_commitafter_rollbackです。

このメソッドにはエイリアスも用意されています。

after_create_commit
after_update_commit
after_destroy_commit

こっちの方が分かりやすくて個人的には好きです。

after_createとは何が違うのか

トランザクションコールバックであるafter_create_commit を 使った時にそもそも after_create と何が違うの? 何で after_create じゃダメなの? という疑問が沸いてきたのでこちらも解説します。

メッセージを送信した時、受信したユーザーに 通知が届くというケースを考えてみます。 モデルはこんな感じです。(適当)

class User << ApplicationRecord
  has_many :notifications
  has_many :messages
end
class Notification << ApplicationRecord
  belongs_to :user
end
class Message << ApplicationRecord
  belongs_to :user
  
  after_create_commit :create_notification
  
  private
  
    def create_notification
      Notification.create(content: "#{sender}からメッセージが届きました。", user_id: recipient_id)
    end
end

さて、ここではafter_create_commitというトランザクションコールバックが 使われていますが、何故after_createでは駄目なのでしょうか。

例えば、after_create コールバック直後に 何らかの例外が発生したとしてロールバックが発生したとします。

するとMessageの作成には失敗しているにも関わらず、 Notificationは作成されるという問題が発生してしまいます。 しかし、after_create_commit コールバックを使えば このような事態にも対処できます。

最後に

コールバックはとても便利な機能でうまく使えれば、Fat Controllerも避けられそうです。

ただし、コールバックを使うときはモデルの一貫性が 損なわれないか考えないとダメみたいですね。