My Tech Blog

個人的な備忘録が中心になると思います

Ruby on Railsを少し触ってみた

Ruby on Railsをあまり触ったことがないので、少しだけ触ってみたのでまとめておく

メモ

使用したRailsのバージョン

rails: 8.0.1
database: postgresql

テスト等で使えそうなライブラリ

  • rpec-rails
  • factory_bot
    • テスト時にモデルファクトリを簡単に定義できる
  • faker
    • 名前などの様々な架空のデータを生成できる
  • gimei
    • 日本の人名や住所などを生成でき、カナ名も取得できる(Fakerだとカナ名は無理)

プロジェクトの作成

# APIモードで新規プロジェクトを作成する
rails new app --API --database=postgresql

データベースの接続設定

app/config/database.ymlの接続情報を修正する

default:  # 共通パラメータ
# ...
     

development: # 開発用DB
    username: postgres
    password: postgres
    host: localhost
    port: 5432

test: # テスト用DB
# ...

production: # 本番用DB
# ...

DB操作コマンド

# データベースの作成
rails db:create

# データベースの削除
rails db:drop

# migrateで作成したスキーマをデータベースに適用する
rails db:schema:load

# 最新のマイグレーションの実行(マイグレーションがすでにある場合)
rails db:migrate

# シードを挿入
rails db:seed

# データベースのリセット(drop -> create -> seed)
rails db:reset

マイグレーション

スキーマを定義する場合は次のコマンドを生成する

# モデルの作成
## gはgenerateの省略(generateでも可)
rails g model User username:string hashed_password:string

app/db/migrationsにタイムスタンプ付きのマイグレーションファイルが作成されるので、コマンドで生成できない部分を追記する。 ユーザーと投稿モデルをそれぞれ新規マイグレーションした場合のサンプルコードを下記に示す。

# ユーザーモデルの作成
class CreateUser < ActiveRecord::Migration[8.0]
    def change
        create_table :users do |t|
            t.string email, null: false
            t.string hashed_password, null: false
            t.string nickname, null: false

            t.timestamps
        end
        
        add_index :users, :email, unique: true
    end
end

# 投稿モデルの作成
class CreatePost < ActiveRecord::Migration[8.0]
    def change
        create_table :posts do |t|
            t.references :users, null: false, foreign_key: true
            t.string :title, null: false
            t.string :content, null: false

            t.timestamps
        end
    end
end

参照する側ではt.refernces <参照リソース名の複数形>のように宣言し、引数にはnull制約や外部キー制約を設定できる。

モデルの関連付け

アプリケーション側のロジックを管理するクラスはApplicationRecordを継承したモデルクラスapp/models/xxx.rbを修正する。 アプリケーション上で属性として関連モデルを取得する場合にも、マイグレーションで参照を関連付けたうえで、別途このクラス内で関連付けを定義する必要がある。 ユーザーと投稿が1:N(has many)の関係にある場合には、次のような定義を追加する。

class User < ApplicationRecord
    has_many :posts
end

class Post < ApplicationRecord
    belongs_to :user
end

こうすることで、Userからは複数のPosts、Postからは一つのUserが対応付けされる。

シードデータの用意

db/seeds.rbを編集してシードデータを用意する。

# hashmapのリストを渡すと一括登録できる
User.create!(email: "admin@example.com", nickname: "admin", hashed_password: BCrypt::Password.create("password"))

コントローラの作成

定義したモデルに対して、APIのアクセスポイントを定義するにはコントローラを生成する。

# usersはURLのパス/usersに対応する
rails g controller users

# api/v1のようなプレフィックスが欲しい場合は、次のようにする
rails g controller api/v1/users

次のメソッドを実装することで、それぞれ対応するエンドポイントを作成することができる。 (名前はその慣例に従う必要があるが、カスタムすることもたぶんできる)

class UsersController < ApplicationController
    before_action :set_user, only: [ :show, :update, :destroy ]
    
    # GET /users
    def index
        users = User.all
        render json: users.as_json, status: :ok
    end
    
    # GET /users/{id}
    def show
        render json: @user.as_json, status: :ok
    end

    # POST /users
    def create
        user = User.new(user_params)
        if user.save
            render json: user.as_json, status: :created
        else
            render json: user.errors.full_messages, :unprocessable_entity
        end
    end
    
    # PATCH/PUT /users
    def update
        if @user.update(user_params)
            render json: user.as_json, status: :ok
        else
            render json: user.errors.full_messages, status: :unprocessable_entity
        end
    end

    # DELETE
    def destroy
        @user.destory
        head :no_content
    end

    private
        # パラメータの必須フィールドを指定する
        def user_params
            params.require(:user).permit(:email, :password, nickname)
        end
        
        # before_actionで紐づけることで任意のメソッドの呼び出し時に変数をセットできる
        def set_user
            @user = User.find(params[:id])
        end

end

プライベートメソッド

  • before_actionとプライベートメソッドを組み合わせて、必要なパラメータのバリデーションを行うことができる(railsではよく用いられている様子)

レスポンス形式をカスタマイズする

レスポンスフィールドをカスタマイズしたい場合には、いくつか方法があるようだがApplicationRecordのas_jsonメソッドの引数onlyに返したいフィールドを指定すれば良い。 共通化したい場合などは、as_jsonメソッドをオーバーライドする。

class User < ApplicationRecord
    # ...
    def as_json
        super(only: [:id, :email, :nickname, :created_at, :updated_at, :deleted_at])
    end

end

テスト

Rspecを用いたAPIのテストを導入してみた。

RSpecの用意

次のコマンドを打つとspecというディレクトリと設定ファイルが生成される。

rails g rspec:install

FactoryBotでテストデータを生成する

spec/factoriesにUserモデルのファクトリを定義してみる。

FactoryBot.define do
    factory :users do
        transient do
            password { "password" }
        end
        email {  Faker::Internet.email }
        nickname {  "nickname" }
        
        after(:build)  do | user, evaluator |
            user.hashed_password =  BCrypt::Password.create(evaluator.password)
        end
    end
end

テストコード内で次のように呼び出す

# 単体を作成する
# フィールドは引数に渡して設定できる
user = FactoryBot.create(:user, password: "12345")


# 複数作成する
users = FactoryBot.create_list(:user, 10, password: "password")

APIのテストを書く

次のコマンドを打って、Usersコントローラのテストファイルを生成する。 spec/requests/users.specが作成される。

rails g rspec:controller users

一覧取得と作成のAPIテストを書いてみる。

RSpec.describe "Users", type: :request do
    describe "GET /index" do
        it "ユーザー一覧の取得に成功する" do
           FactoryBot.create_list(:user, 10)
           
           get "/api/v1/users"

           expect(response).to have_http_stastus(:ok)

           json = JSON.parse(response.body)

           expect(json.length).to 10
        end
    end

    describe "POST /" do
        valid_params = {
            nickname: "nickname",
            email: "test@example.com",
            password: "password"
        }
        
       expect { post "/api/v1/users", params: {  user: valid_params } }.to change(User, :count).by(1)
       
       expect(response).to have_http_status(:created)
   
       json = JSON.parse(response.body)
       
       expect(json[:nicname]).to eq(valid_params[:nickname])
       expect(json[:nicname]).to eq(valid_params[:email])
    end

end