【Rails】グループ機能の実装手順

この記事では、Railsでグループオーナーが作成したグループに、ユーザーがグループに参加・離脱できるようなグループ作成機能を実装する方法をまとめています。

## 作製するモデルのアソシエーション
今回作成するテーブルは`GroupUser`と`Group`のふたつです。

![スクリーンショット 2024-09-14 204259.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3882438/2a5cb3bb-4522-1448-b5b0-fb7e73f0735f.png)

### Groupテーブル
グループの情報を保存します（`name`、`introduction`、`image_id`など）。
`owner_id`フィールドにより、グループの所有者（ユーザー）を表します。
### GroupUserテーブル
中間テーブルで、ユーザーがどのグループに所属しているかを管理します。
`user_id`と`group_id`で`User`と`Group`を関連付けています。

# 実装手順
## 1. GroupモデルとGroupUserモデルの作成
まず、`Group`モデルと、グループに所属するユーザーを管理する`GroupUser`モデルを作成します。
`rails generate model Group name:string introduction:text image_id:string owner_id:integer`

```ruby:group_usersテーブルのマイグレーション
class CreateGroupUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :group_users do |t|
      t.integer :user_id
      t.integer :group_id

      t.timestamps
    end
  end
end
```

`rails generate model GroupUser user:references group:references`
```ruby:groupsテーブルのマイグレーション
class CreateGroups < ActiveRecord::Migration[6.0]
  def change
    create_table :groups do |t|
      t.string :name
      t.text :introduction
      t.integer :owner_id

      t.timestamps
    end
  end
end
```

## 2. アソシエーションとバリデーションの追加
次に、`User`モデル、`Group`モデル、`GroupUser`モデルにアソシエーションとバリデーションを追加します。

```ruby:user.rb
class User < ApplicationRecord
  has_many :group_users, dependent: :destroy
  has_many :groups, through: :group_users # user.groupsが利用可能に
end
```

```ruby:group_user.rb
class GroupUser < ApplicationRecord
  belongs_to :user
  belongs_to :group

  validates_uniqueness_of :group_id, scope: :user_id # 同じユーザーが同じグループに複数回参加しないように
end
```

```ruby:group.rb
class Group < ApplicationRecord
  has_many :group_users, dependent: :destroy
  has_many :users, through: :group_users # group.usersが利用可能に

  # バリデーション
  validates :name, presence: true
  validates :introduction, length: { maximum: 50 }
  validates :owner_id, presence: true

  has_one_attached :group_image

  # 画像を取得するメソッド
  def get_group_image(weight, height)
    unless self.group_image.attached?
      file_path = Rails.root.join('app/assets/images/no_image.jpg')
      group_image.attach(io: File.open(file_path), filename: 'default-image.jpg', content_type: 'image/jpeg')
    end
    self.group_image.variant(resize_to_fill: [weight, height]).processed
  end

  # ユーザーがグループに参加しているかを判定
  def joined_by?(user)
    group_users.exists?(user_id: user.id)
  end
end
```

## 3. ルーティングの設定
次に、ルーティングにグループとそのユーザー管理用のルートを追加します。

```ruby:routes.rb
resources :groups do
  resource :group_users, only: [:create, :destroy] # グループへの参加と離脱
end
```
![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3882438/6cda382c-f85e-f798-ea19-88158f4b54fa.png)

## 4. グループ作成・一覧ページへのリンクを追加
ユーザー一覧ページに、グループ作成ページへのリンクとグループ一覧ページへのリンクを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3882438/9521d766-b619-3de8-7850-3b063ddbdecb.png)

```erb:users/index.html.erb
    :
    <div class='col-md-8 offset-md-1'>
      <h2>Users</h2>
      <%= link_to "グループを作成する", new_group_path %> | <%= link_to "グループ一覧", groups_path %>
      <%= render 'index', users: @users %>
    </div>
  </div>
</div>
```

## 5. Viewファイルの作成

・グループ作成ページ
![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3882438/44c89638-9251-a8dc-0468-0fcd623312d8.png)
```erb:groups/new.html.erb
<div class='container'>
  <div class='row'>
    <div class="col-sm-12 col-md-8 col-lg-5 px-5 px-sm-0 mx-auto">
      <h2>Group Creation</h2>
      <%= form_with model: @group, local: true do |f| %>
        <div class="form-group">
          <%= f.label :name %>
          <%= f.text_field :name, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.label :introduction %>
          <%= f.text_area :introduction, class: "form-control" %>
        </div>
        <%= f.submit "Create Group", class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
</div>
```

・グループ一覧ページ
![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3882438/830d5c8b-c3e2-66da-6748-618bf792d1f2.png)

```erb:groups/index.html.erb
<div class='container px-5 px-sm-0'>
  <div class='row'>
    <div class='col-md-8 offset-md-1'>
      <h2>Groups</h2>
      <%= render 'index', groups: @groups %>
    </div>
  </div>
</div>
```
・グループ編集ページ
![image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3882438/026be248-5450-1eb4-9ae9-83d4429edbc9.png)
```erb:groups/_index.html.erb
<table class='table'>
  <thead>
    <tr>
      <th></th>
      <th>グループ名</th>
      <th>紹介文</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <% @groups.each do |group| %>
      <tr>
        <td><%= image_tag group.get_group_image(80, 80) %></td>
        <td><%= link_to group.name, group_path(group.id) %></td>
        <td><%= group.introduction %></td>
        <% if group.owner_id == current_user.id %>
          <td><%= link_to 'Edit', edit_group_path(group.id), class: "btn btn-sm btn-success" %></td>
        <% end %>
      </tr>
    <% end %>
  </tbody>
</table>
```


## 6. Controllerの設定
最後に、グループ管理用の`GroupsController`と`GroupUsersController`を作成します。

```ruby:groups_controller.rb
class GroupsController < ApplicationController
  before_action :authenticate_user!
  before_action :ensure_correct_user, only: [:edit, :update, :destroy] #グループオーナーでないと:edit, :update, :destroyは使えない

  def new
    @group = Group.new
  end

  def create
    @group = Group.new(group_params) #グループ新規作成
    @group.owner_id = current_user.id #owner_idの代入
    if @group.save
      redirect_to groups_path, notice: "You have created group successfully."
    else
      render 'new'
    end
  end

  def index
    @groups = Group.all
  end

  def show
    @group = Group.find(params[:id])
    @users = @group.users #@groupに入っているユーザーのみ格納
    @owner_user = User.find(@group.owner_id) #グループオーナーを見つけて格納
  end

  def edit
  end

  def update
    if @group.update(group_params)
      redirect_to groups_path, notice: "You have updated group successfully."
    else
      render 'edit'
    end
  end

  private

  def group_params
    params.require(:group).permit(:name, :introduction, :group_image)
  end

  def ensure_correct_user #グループオーナーかどうかを判断する
    @group = Group.find(params[:id])
    unless @group.owner_id == current_user.id
      redirect_to groups_path
    end
  end
end
```

```ruby:group_users_controller.rb
class GroupUsersController < ApplicationController
  # グループにユーザーを参加させるアクション
  def create
    # 加入しようとしているグループを検索する
    group = Group.find(params[:group_id])
    # group_userにuser_id、group_idを格納
    group_user = current_user.group_users.new(group_id: group.id)
    # ユーザーをグループに加入
    group_user.save
    redirect_to groups_path
  end

  # グループからユーザーを脱退させるアクション
  def destroy
    # 脱退しようとしているグループを検索する
    group = Group.find(params[:group_id])
    # group_userにuser_id、group_idを格納
    group_user = current_user.group_users.find_by(group_id: group.id)
    # ユーザーをグループから脱退させる
    group_user.destroy
    redirect_to groups_path
  end
end

```