【Rails】Rails APIを用いたグループ機能実装：グループ作成とユーザーの入退会処理

Rails APIでUser周りの実装をしたので、グループの作成とユーザーの入退会処理をしていきます。
[【Rails】JWTを用いたUser周りのAPI実装](https://qiita.com/kay903347/items/3847a5cf4642e122128c)
## Groupテーブル
| カラム名 | データ型 | 制約 | 説明 |
| --- | --- | --- | --- |
| id | integer | 主キー、オートインクリメント | グループの一意な識別子 |
| name | string | null: false | グループ名 |
| created_at | datetime | null: false | レコード作成日時 |
| updated_at | datetime | null: false | レコード更新日時 |
## GroupMembershipテーブル
| カラム名 | データ型 | 制約 | 説明 |
| --- | --- | --- | --- |
| id | integer | 主キー、オートインクリメント | グループメンバーシップの一意な識別子 |
| user_id | integer | null: false, foreing ket (users.id) | ユーザーID（ユーザーへの外部キー） |
| group_id | integer | null: false, foreing ket (groups.id) | グループID（グループへの外部キー） |
| created_at | datetime | null: false | レコード作成日時 |
| updated_at | datetime | null: false | レコード更新日時 |
## ルーティング
グループの作成、更新、削除、参加、退会を実装していきます。
| HTTPメソッド | パス                     | コントローラ#アクション      | 説明                       |
|--------------|--------------------------|-----------------------------|----------------------------|
| POST         | /api/v1/groups           | groups#create               | グループを新規作成    |
| PUT/PATCH    | /api/v1/groups/:id       | groups#update               | グループ情報を更新      |
| DELETE       | /api/v1/groups/:id       | groups#destroy              | グループを削除          |
| POST         | /api/v1/groups/:id/join  | group_memberships#create    | 指定したグループに参加  |
| DELETE       | /api/v1/groups/:id/leave | group_memberships#destroy   | 指定したグループから退会 |

以下がルーティングです
```ruby:routes.rb
resources :groups do
  member do
    post 'join', to: 'group_memberships#create'
    delete 'leave', to: 'group_memberships#destroy'
  end
end
```
## 期待するレスポンス
* POST /api/v1/groups
```json:json
{
  "message": "グループが作成されました。",
  "group": {
    "id": 1,
    "name": "新しいグループ",
    "created_at": "2024-11-03T12:34:56Z",
    "updated_at": "2024-11-03T12:34:56Z"
  }
}
```
* PUT/PATCH /api/v1/groups
```json:json
{
  "message": "グループ情報が更新されました。",
  "group": {
    "id": 1,
    "name": "更新されたグループ",
    "created_at": "2024-11-03T12:34:56Z",
    "updated_at": "2024-11-03T12:45:00Z"
  }
}
```
* DELET /api/v1/groups
```json:json
{
  "message": "グループが削除されました。"
}
```
* POST /api/v1/join
```json:json
{
  "message": "グループに参加しました。",
  "group_membership": {
    "id": 1,
    "user_id": 1,
    "group_id": 1,
    "created_at": "2024-11-03T12:34:56Z"
  }
}
```
* DELETE /api/v1/leave
```json:json
{
  "message": "グループから退会しました。"
}
```
## Groupテーブルの作成
#### マイグレーションの作成
```
docker-compose run web rails generate model Group name:string
```
このコマンドにより、Groupテーブルのマイグレーションファイルが生成されます。nameカラムが追加されます。
#### マイグレーションの実行
```
docker-compose run web rails db:migrate
```
これでGroupテーブルがデータベースに作成されます。
#### モデルの編集
groupのレコードが削除された時に関連するgroup_membershipsのレコードも同時に削除するように設定。
```ruby:group_group.rb
class Group < ApplicationRecord
  has_many :group_memberships, dependent: :destroy
  has_many :users, through: :group_memberships
end
```
## GroupMembershipテーブルの作成
#### マイグレーションの作成
```
docker-compose run web rails generate model GroupMembership user:references group:references
```
このコマンドにより、GroupMembershipテーブルのマイグレーションファイルが生成されます。user_id、group_idカラムが追加されます。
user_idとgroup_idの2つの外部キーが必要です。
#### マイグレーションの実行
```
docker-compose run web rails db:migrate
```
これでGroupテーブルがデータベースに作成されます。
#### バリデーションの追加
同じユーザーが同じグループに重複して参加できないようにするためにバリデーションを追加します。
```ruby:group_membership.rb
class GroupMembership < ApplicationRecord
  belongs_to :user
  belongs_to :group

  validates :user_id, uniqueness: { scope: :group_id }
end
```
#### マイグレーションの実行
```
docker-compose run web rails db:migrate
```
これでGroupMembershipテーブルがデータベースに作成されます。
#### userモデルの編集
userのレコードが削除された時に関連するgroup_membershipsのレコードも同時に削除するように設定。
```ruby:user.rb
  has_many :group_memberships, dependent: :destroy
  has_many :groups, through: :group_memberships
```
## コントローラの作成
#### groupsコントローラ
```
docker-compose exec web rails generate controller api/v1/Groups
```
```ruby:groups_controller.rb
module Api
  module V1
    class GroupsController < ApiController
      before_action :authenticate_user
      before_action :set_group, only: [:update, :destroy]

      # グループを新規作成
      def create
        group = Group.new(group_params)
        if group.save
          GroupMembership.create(user: current_user, group: group)
          render json: { message: "グループが作成されました。", group: { id: group.id, name: group.name } }, status: :created
        else
          render json: { errors: group.errors.full_messages }, status: :unprocessable_entity
        end
      end

      # グループ情報を更新
      def update
        if @group.update(group_params)
          render json: { message: "グループ情報が更新されました。", group: {id: @group.id, name: @group.name} }, status: :ok
        else
          render json: { errors: @group.errors.full_messages }, status: :unprocessable_entity
        end
      end

      # グループを削除
      def destroy
        if @group.destroy
          render json: { message: "グループが削除されました。" }, status: :ok
        else
          render json: { error: "グループの削除に失敗しました。" }, status: :unprocessable_entity
        end
      end

      private

      # Strong Parameters
      def group_params
        params.require(:group).permit(:name)
      end

      # グループを取得
      def set_group
        @group = current_user.groups.find(params[:id])
      rescue ActiveRecord::RecordNotFound
        render json: { error: "グループが見つかりません。" }, status: :not_found
      end
    end
  end
end
```
#### group_membersipsコントローラ
```
docker-compose exec web rails generate controller api/v1/GroupMemberships
```
```ruby:group_memberships_controller/rb
module Api
  module V1
    class GroupMembershipsController < ApiController
      before_action :authenticate_user

      # グループに参加
      def create
        group = Group.find(params[:id])
        membership = current_user.group_memberships.build(group: group)

        if membership.save
          render json: { message: "グループに参加しました。" }, status: :created
        else
          render json: { errors: membership.errors.full_messages }, status: :unprocessable_entity
        end
      end

      # グループから退会
      def destroy
        membership = current_user.group_memberships.find_by(group_id: params[:id])

        if membership&.destroy
          render json: { message: "グループから退会しました。" }, status: :ok
        else
          render json: { error: "退会に失敗しました。" }, status: :unprocessable_entity
        end
      end
    end
  end
end
```
## Postmanを使ってリクエスト
Postmanを使ってそれぞれリクエストを送ってみます。
#### グループ作成
リクエストメソッドをPOSTに設定しURLを以下に設定する
```
http://localhost:3000/v1/groups
```
リクエストボディを以下のように設定して
```json:json
{
  "name": "hoge group"
}
```
リクエストを送ると以下のレスポンスが返ってきます。
```json:json
{
    "message": "グループが作成されました。",
    "group": {
        "id": 1,
        "name": "hoge group"
    }
}
```
#### グループ情報更新
リクエストメソッドをPATCHに設定しURLを以下に設定する
```
http://localhost:3000/v1/groups/1
```
リクエストボディを以下のように設定して
```json:json
{
  "name": "fuga group"
}
```
リクエストを送ると以下のレスポンスが返ってきます。
```json:json
{
    "message": "グループ情報が更新されました。",
    "group": {
        "id": 1,
        "name": "fuga group"
    }
}
```
#### グループ情報更新
リクエストメソッドをPATCHに設定しURLを以下に設定する
```
http://localhost:3000/v1/groups/1
```
リクエストボディを以下のように設定して
```json:json
{
  "name": "fuga group"
}
```
リクエストを送ると以下のレスポンスが返ってきます。
```json:json
{
    "message": "グループ情報が更新されました。",
    "group": {
        "id": 1,
        "name": "fuga group"
    }
}
```
#### グループ削除
リクエストメソッドをDELETに設定しURLを以下に設定する
```
http://localhost:3000/v1/groups/1
```
リクエストボディは設定せずにリクエストを送ると以下のレスポンスが返ってきます。
```json:json
{
    "message": "グループが削除されました。"
}
```
#### グループ退会
リクエストメソッドをDELETに設定しURLを以下に設定する
```
http://localhost:3000/v1/groups/1/leave
```
リクエストボディは設定せずにリクエストを送ると以下のレスポンスが返ってきます。
```json:json
{
    "message": "グループから退会しました。"
}
```