Railsで中間テーブルダブルポリモーフィックやってみた。

[Ruby on Rails Advent Calendar 2019](https://qiita.com/advent-calendar/2019/rails)の18日目が未投稿だったので代わりにでも。

## はじめに
Railsを用いたサービス開発中に、中間テーブルとポリモーフィック関連を組み合わせる機会があり、ググってもあまり情報がないのでここに書く。

## 前提
- とあるプラットフォームが複数のサブスクリプションサービスを持っている（サービスA, サービスB, etc.）
    - ユーザーは一つのアカウントで複数のサービスを契約できる。また同一サービスに対して複数契約もできる。
- それぞれのサービスに対して、`申し込み情報`と`契約情報`を別テーブルで保持する。これらは`ユーザー`テーブルと関連づけられる。
- ビジネスフロー例
    1. サービスAへの申し込み発生 → 申し込みレコードを発行
    2. 申し込みの妥当性確認などビジネスサイドの手続き。
    3. 2の処理完了 → 申し込み情報レコードを元に契約情報レコードを発行
- 各申し込み/契約に対して、`支払い情報`と`請求先情報`が別テーブルで関連づいている。
    - これらの情報はサービスをまたいで共有することができる。例えば、とあるユーザーがサービスAへの申し込みで登録した支払い/請求先情報を、サービスBへの申し込みにも利用することができる（→中間テーブルを採用）。


## ER図を眺めてみる
サービスAのことだけ考えた場合、ER図は下記のようになる。
（各テーブルのフィールド、また本題と関係ないリレーションは省略）

<img width="657" alt="スクリーンショット 2019-12-21 11.04.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168594/9b6eab78-8962-826e-7fd2-b6feaba53469.png">


サービスA向けの申し込み情報テーブルを`ServiceAOrder`, 契約情報を`ServiceAContract`として、中間テーブルを介してそれぞれが支払い情報`Payment`、請求先情報`BillRecipient`を持つ。

この時点で中間テーブルが4つ作られているが、サービスが増えるごとに4つずつ新しく中間テーブルを増やすのは非常にだるい、、

ここで気がつく。
中間テーブルにポリモーフィック関連って使えるのかな？
もしかして両サイドポリモーフィックもいける、、？

**もしかして中間テーブルダブルポリモーフィックできちゃう、、？**

## 実装

というわけでやってみた。ソースコードは[こちら](https://github.com/kumewata/rails_has_many_through_polymorphic)

検証環境はRuby 2.6.3, Rails 6.0.2

方針としては`Subscription`モデルを中間テーブルとして、`holder`と`target`という2つのポリモーフィックなbelongs_toを作る。やろうとすれば割り当てるモデルに制限はないが、意味合い的に次のような割り当てをする。

- holder: SeviceAOrder, ServiceAContract, 他のサービスを追加していく
- target: Payment, BillRecipient

またシンプルに中間テーブルを利用する場合だと次のような指定をすれば良いが、ポリモーフィック関連を利用する場合はhas_manyにオプションで`source`, `source_type`をつけてテーブルを明示的に指定する必要がある（Railsガイドの[このあたり](https://railsguides.jp/association_basics.html#has-many%E3%81%AE%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-source)）。

```ruby
has_many :subscriptions
has_many :payments, through: :subscriptions
```

モデルのリレーション一覧↓

```ruby
# app/models/subscription.rb
class Subscription < ApplicationRecord
  belongs_to :holder, polymorphic: true
  belongs_to :target, polymorphic: true
end

# app/models/service_a_order.rb
class ServeceAOrder < ApplicationRecord
  has_many :subscriptions, as: :holder
  has_many :payments, through: :subscriptions, source: :target, source_type: 'Payment'
  has_many :bill_recipients, through: :subscriptions, source: :target, source_type: 'BillRecipient'
end

# app/models/service_a_contract.rb
class ServeceAContract < ApplicationRecord
  has_many :subscriptions, as: :holder
  has_many :payments, through: :subscriptions, source: :target, source_type: 'Payment'
  has_many :bill_recipients, through: :subscriptions, source: :target, source_type: 'BillRecipient'
end

# app/models/bill_recipient.rb
class BillRecipient < ApplicationRecord
  has_many :subscriptions, as: :target
  has_many :service_a_orders, through: :subscriptions, source: :holder, source_type: 'ServiceAOrder'
  has_many :service_a_contracts, through: :payment_bill_recipient_holders, source: :holder, source_type: 'ServiceAContract'
end

# app/models/payment.rb
class Payment < ApplicationRecord
  has_many :subscriptions, as: :target
  has_many :service_a_orders, through: :subscriptions, source: :holder, source_type: 'ServiceAOrder'
  has_many :service_a_contracts, through: :subscriptions, source: :holder, source_type: 'ServiceAContract'
end
```


簡単なテスト↓

```ruby
# spec/models/service_a_order_spec.rb
require 'rails_helper'

RSpec.describe ServiceAOrder, type: :model do
  describe '中間テーブル機能チェック' do
    it 'service_a_orderレコードに対して、ポリモーフィックなpaymentとbill_paymentレコードがきちんと紐づく' do
      user = User.new(name: 'Dan', email: 'test@example.com')
      user.save!
      service_a_order = ServiceAOrder.new(plan_type: 1, user: user)
      service_a_order.save！

      payment = Payment.new(payment_type: 1)    
      service_a_order.subscriptions.create(target: payment)
      # service_a_order.payments << payment # こっちの書き方でもOK
      bill_recipient = BillRecipient.new(address: 'Chiyoda')
      service_a_order.subscriptions.create(target: bill_recipient)
      # service_a_order.bill_recipients << bill_recipient # こっちの書き方でもOK
      
      expect(service_a_order.payments.count).to eq 1
      expect(service_a_order.payments.first).to eq(payment)
      expect(service_a_order.bill_recipients.count).to eq 1
      expect(service_a_order.bill_recipients.first).to eq(bill_recipient)
    end
  end
end
```

ちなみに[ソースコード](https://github.com/kumewata/rails_has_many_through_polymorphic/tree/master/app/models)の方では`ServiceBOrder`と`ServiceBContract`を追加しているが、いけてそう。


## 参考
[Railsのポリモーフィック関連とはなんなのか](https://qiita.com/itkrt2y/items/32ad1512fce1bf90c20b)
[Combining has_many :through with polymorphic associations in ActiveRecord](https://www.sihui.io/has_many-through-and-polymorphic/)

## まとめ

そのうち地雷を踏みそうだけど後悔はしていない。