[Rails6]グループ機能を実装する

### 前提
記事を書いている私は現役のエンジニアではなくエンジニアを目指して学習中の者です。
よって、気をつけてはいますが記述内容には誤った内容が含まれている危険性があります。
その為この記事は参考程度に読んで頂き詳しい内容は公式の記述を参考にすることをおすすめします。

### この記事で実装すること
Rails6系のPFでグループ機能を実装したのでその時の手順と詰まった箇所についてまとめました。
PFは卒業アルバムページを作成できるようになっており、そのアルバムを共有するメンバーをグループで設定しています。
よって以下のような機能を持ちます。
- アルバムに招待するメンバーを選択できる
- アルバムメンバーにだけ該当するアルバムを表示する

### 使用技術
Ruby 3.1.2
Rails 6.1.6

### 実装手順
既に以下のモデルを持つアプリを作成した前提で進めていきます。
**Userモデル**
ユーザーは複数のアルバムを持つことができます。
```ruby:user.rb
has_many :albums, dependent: :destroy
```
**Albumモデル**
このアルバムをグループで共有できるように実装していく。
```ruby:album.rb
belongs_to :user
```

##### 1, 中間テーブルを作成する
アルバムをグループで閲覧できるようにする場合
Albumは複数のユーザーが持つことができ、ユーザーも複数のアルバムを持つという多対多の関係が成立します。
よって、誰がどのアルバムに属しているかを記録するために中間テーブル(albumusers)を作成してあげます。
```
#referencesを指定すると自動で外部キーの設定などを行なってくれます
$ rails g model AlbumUser album:references user:references
```

##### 2, それぞれのモデルにアソシエーションを設定する

```ruby:album_user.rb
belongs_to :album
belongs_to :user
#アルバムに同じユーザーが1人以上登録されないようにする
validates :user_id, uniqueness: { scope: :album_id }
```

```ruby:user.rb
has_many :albums, dependent: :destroy
#userとalbum_userは1対多の関係になる
has_many :album_users, dependent: :destroy
#user.belong_albumsでユーザーが所属しているアルバムを取得できる
has_many :belong_albums, through: :album_users, source: :album
```

```ruby:album.rb
belongs_to :user
#albumとalbum_userは1対多の関係になる
has_many :album_users, dependent: :destroy
#album.usersでアルバムに所属するユーザーを取得できる
has_many :users, through: :album_users, source: :user
```

##### 3, viewからグループメンバーのidを受け取る
コントローラーで@albumをviewに渡す。
```ruby:albums_controller.rb
def new
  @album = Album.new
end
```

招待したいユーザーのidを受け取るには**collection_check_boxes**を使用します。
詳しい説明は[collection_check_boxesについて](https://railsdoc.com/page/collection_check_boxes)にありますがこれを使うと画像のようにユーザー全員の名前をチェックボックス形式で表示できます。
アルバムを作成しているユーザーが招待したいユーザーを選んで作成を押すと選択されたユーザーのidを配列でparamsに含めることができます。
[![Image from Gyazo](https://i.gyazo.com/f0462569defae689cccc726fb98209d9.png)](https://gyazo.com/f0462569defae689cccc726fb98209d9)
```ruby:views/albums/new.html.erb
<%= form_with model: @album, local: true do |f| %>
  <%= render "shared/error_messages", object: f.object %>
  <%= f.label :album_name %>
  <%= f.text_field :album_name %>
      
  <%= f.label :user_ids, 'メンバー', class: "text-sm block"%>
  <%= f.collection_check_boxes :user_ids, User.all, :id, :name %>

  <%= f.submit '作成する', class: 'btn btn-primary' %>
<% end %>
```

##### 4, createアクションでidを受け取りアルバムメンバーを保存します。
作成ボタンを押すと以下のparamsが送られてきます。
```
> params
=> #<ActionController::Parameters {"album"=>{"album_name"=>"testアルバム", "user_ids"=>["", "3", "6"]}, "commit"=>"作成する", "controller"=>"albums", "action"=>"create"} permitted: false>
```
user_idsのidとalbumのidをAlbumUserに保存すればアルバムにメンバーを追加することができます。
詰まった箇所で詳しく説明しますがalbum_paramsにuser_idsを追加する必要はないと考えています。
```ruby:albums_controller.rb
class AlbumsController < ApplicationController

  def create
    @album = current_user.albums.build(album_params)
    #menber_idsがnilでもエラーが発生しないようにnilガードをしている
    menber_ids ||= []
    #paramsからidの入った配列を受け取り、アルバム作成中のユーザーのidも追加してあげます
    #rejectメソッドで空の要素を削除してmapメソッドでidをinteger型に変更しています
    (params[:album][:user_ids] << current_user.id).reject(&:blank?).map(&:to_i).each do |id|
      #モデルで定義したusersを使用して@albumに所属するユーザーを保存しています
      @album.users << User.find(id)
    end
    if @album.save
      redirect_to albums_path, notice: '作成に成功しました'
    else
      flash.now['alert'] = '作成に失敗しました'
      render :new
    end
  end

  private

  def album_params
    params.require(:album).permit(:album_name)
  end
end
```
<<の意味については[こちら](https://railsguides.jp/association_basics.html#has-many%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89-collection-object)を参照して下さい

このままでも実装はできているのですがコントローラーのコード量が増えてしまうのでモデルにロジックを切り出します。
Albumモデルにメソッドを定義してコントローラーを編集します。

```ruby:album.rb
def set_album_menbers(menber_ids, current_user)
  menber_ids ||= []
  (menber_ids << current_user).reject(&:blank?).map(&:to_i).each do |id|
    self.users << User.find(id)
  end
end
```

```ruby:albums_controller.rb
def create
  @album = current_user.albums.new(album_params)
  @album.set_album_menbers(params[:album][:user_ids], current_user.id)
  if @album.save
    redirect_to album_path(@album), notice: '作成に成功しました'
  else
    flash.now['alert'] = '作成に失敗しました'
    render :new
  end
end
```

##### 5, 表示するアルバムをユーザーが所属するものだけに限定する
最後にアルバム一覧画面に表示されるアルバムをユーザーが所属しているものに限定します。
```ruby:albums_controller.rb
def index
  #Userモデルで定義したbelong_albumsを使うと所属するアルバムだけを取得できる
  @albums = current_user.belong_albums.order(created_at: :desc)
end
```

### 詰まった箇所
最初はnew.html.erbで送信したuser_idsをalbum_paramsで受け取る方法で実装を行なっていました。
その場合だとコードは以下のようになります。こっちの方がコード自体はシンプルになります。
```ruby:albums_controller.rb
class AlbumsController < ApplicationController
  def create
    @album = current_user.albums.build(album_params)
    @album.users << current_user
    if @album.save
      redirect_to albums_path, notice: '作成に成功しました'
    else
      flash.now['alert'] = '作成に失敗しました'
      render :new
    end
  end

  private

  def album_params
    params.require(:album).permit(:album_name, {user_ids: []})
  end
end
```
album_paramsにuser_idsを含めた場合は
``current_user.albums.build(album_params)``の時点でAlbumUserのインスタンスが自動的に作成されます。そして、@album.saveでAlbumUserのインスタンスも同時に保存されます。
この方法で問題ないと思ったのですがアルバムの作成に失敗した場合に以下のように「Usersは不正な値です」というエラーメッセージが出てきてしまいます。
[![Image from Gyazo](https://i.gyazo.com/06e5a41b59f9f47c8f1994b8dcdc09f7.png)](https://gyazo.com/06e5a41b59f9f47c8f1994b8dcdc09f7)
これはAlbumモデルにuser_idsというカラムがないために表示されてしまいます。
ユーザーからしたら「Usersは不正な値です」というメッセージは意味のわからないものになってしまうのでこのメッセージが表示されないように実装手順の変更を行いました。

以上でグループ機能と詰まった箇所についての説明を終わります。
初学者なので何か間違った箇所があった場合は指摘頂けると助かります！