【Rails】多対多のモデル間で１つのデータに関連付けられる個数を制限する

# はじめに🧎🏻
現在個人開発しているサービスにて
多対多で関連付けたモデルに対して、紐付けられる数を制限する際に
まとまった情報がなく不便だったので備忘録も兼ねて記します。

### 環境💻
- Ruby `3.0.1`
- Ruby on Rails `6.1.4.4`
- [gem] rails-i18n `6.0.0`

### 想定読者👥
- 多対多の関連付けをしているモデルに対して、1つのデータに関連付けられる個数に対するバリデーションをかけたい人。
- そのバリデーションに引っかかった際のエラーメッセージの制御、および国際化をしたい人。


# 前提🎬

例として、映画と役者のモデルが多対多で関連付けられている場合を想定します。 
- 1つの映画`Movie`に対して役者の情報は複数持つ（出演者てきな）
- １人の役者`Actor`に対して映画の情報は複数持つ（出演作てきな）
- 上記の紐付けを中間テーブル`movie_actors`で管理する

```ruby:各モデルファイル
# movie.rb
class Movie < ApplicationRecord
  〜略〜
  has_many :movie_actors
  has_many :actors, through: :movie_actors
  〜略〜
end

# actor.rb
class Actor < ApplicationRecord
  〜略〜
  has_many :movie_actors
  has_many :movies, through: :movie_actors
  〜略〜
end

# movie_actor.rb
class MovieActor < ApplicationRecord
  belongs_to :movie
  belongs_to :actor

  # 組み合わせの一意制約
  validates :movie_id, uniqueness: { scope: :actor_id }  
end

```
# バリデーションを定義👮🏻‍♂️
ここでさらに
**「一つの映画に対して紐付けられる役者の情報を3人までに制限したい」**
という仕様があるとします。今回の肝ですね。
これを実現するには
```ruby
validates :中間テーブル名, length: { maximum: 上限数 }
```
これを制限をかける側のモデルファイルに記述するだけです。
今回の場合だと`Movie`モデルなので、

```ruby:movie.rb
class Movie < ApplicationRecord
  〜略〜
  validates :movie_actors, length: { maximum: 3 }
  〜略〜
end
```
これだけでOK。
Railsのよしな力におんぶにだっこです。~~ヒモ男化が加速しますね〜。~~

ちなみに今回は想定した仕様上、
上記のようなバリデーションをかけましたが、たとえば
- 1つの映画に対して役者の情報を2人以上、3人以下

というような制限や
- １人の役者に対して映画の情報は3つまで

のような`Actor`側の制限もできます。

# エラーメッセージの制御と国際化🇯🇵
データの制約上は上記だけでOKですが、現状だとこのバリデーションに引っかかった際は
`movie_actorsは3文字以内で入力してください`
というエラーメッセージが表示されます。(日本語化している場合)
①属性名英語だし、②(文字じゃないのに)3文字って出てるしでなんとかしたい。

#### ①属性名英語だし　→　`movie_actors`を日本語化しましょう
```ruby:model.ja.yml
ja:
  activerecord:
    attributes:
      movie:
        movie_actors: 出演者
```

#### ②3文字って出てるし　→　エラーメッセージを制御しましょう
`validates`メソッドの`length`ヘルパーでは、`message`オプションを指定することでエラーメッセージを制御することができます。
```ruby:movie.rb
validates :movie_actors, length: { maximum: 3, message: 'は3人までじゃよ〜欲張りなさんな。' }
```
↓`message`オプション以外でも色々できます
> デフォルトのエラーメッセージは、実行されるバリデーションの種類によって異なります。デフォルトのメッセージは:wrong_length、:too_long、:too_shortオプションを使ってカスタマイズすることも、%{count}を長さ制限に対応する数値のプレースホルダにも使えます。:messageオプションを使ってエラーメッセージを指定することもできます。　- [Railsガイド ActiveRecord バリデーション](https://railsguides.jp/active_record_validations.html#:~:text=%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%AE%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AF%E3%80%81%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%82%8B%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E7%A8%AE%E9%A1%9E%E3%81%AB%E3%82%88%E3%81%A3%E3%81%A6%E7%95%B0%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%AE%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AF%3Awrong_length%E3%80%81%3Atoo_long%E3%80%81%3Atoo_short%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%82%82%E3%80%81%25%7Bcount%7D%E3%82%92%E9%95%B7%E3%81%95%E5%88%B6%E9%99%90%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B%E6%95%B0%E5%80%A4%E3%81%AE%E3%83%97%E3%83%AC%E3%83%BC%E3%82%B9%E3%83%9B%E3%83%AB%E3%83%80%E3%81%AB%E3%82%82%E4%BD%BF%E3%81%88%E3%81%BE%E3%81%99%E3%80%82%3Amessage%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%82%82%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82)

これで、バリデーションに引っかかった際には
**「出演者は３人までじゃよ〜欲張りなさんな。」**
と、おそらく達観してそうなひとに優しく諭してもらえます。

#### ここからさらに`message`もi18ｎしたい人に向けて追加説明
①翻訳ファイルに定義
```ruby:model.ja.yml
ja:
  activerecord:
    errors:
      messages:
        a_movie_has_upto_three_actors: は３人までじゃよ〜欲張りなさんな。
```
②コードの書き換え
```ruby:movie.rb
validates :movie_actors, length: { maximum: 3, message:
  I18n.t('activerecord.errors.messages.a_movie_has_upto_three_actors') }
```
※ここで`I18n.t('...')`の形でレシーバを明示しないと,`Movie`をレシーバに参照してしまい`NoMethodError`が発生するため注意！
# おわりに🙇🏻‍♂️
`model.ja.yml`でactiverecord→errors→messages配下に配置しているものの、呼び出し時の省略ができてないので、 呼び出し時に長くなるのを嫌う場合は別枠で切り出してもいいかも？
ただ単純に自分がなにかミスってる可能性もある。というか多分そうなので分かる方はぜひご教授ください。泣いて喜びます。　

その他にも間違っている点や補足、タイポなどありましたらお気軽にコメント、編集リクエストいただけますと幸いです！！

# 参考記事📄
- [多対多の相手レコード数の最小を指定するバリデーション - 男女比はカレーと福神漬けと同じくらい](https://euglena1215.hatenablog.jp/entry/2016/12/16/160926)
- [Active Record バリデーション - Railsガイド](https://railsguides.jp/active_record_validations.html)