Help us understand the problem. What is going on with this article?

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

Ruby on Rails Advent Calendar 2019の18日目が未投稿だったので代わりにでも。

はじめに

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

前提

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

ER図を眺めてみる

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

スクリーンショット 2019-12-21 11.04.06.png

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

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

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

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

実装

というわけでやってみた。ソースコードはこちら

検証環境はRuby 2.6.3, Rails 6.0.2

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

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

またシンプルに中間テーブルを利用する場合だと次のような指定をすれば良いが、ポリモーフィック関連を利用する場合はhas_manyにオプションでsource, source_typeをつけてテーブルを明示的に指定する必要がある(Railsガイドのこのあたり)。

has_many :subscriptions
has_many :payments, through: :subscriptions

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

# 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

簡単なテスト↓

# 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

ちなみにソースコードの方ではServiceBOrderServiceBContractを追加しているが、いけてそう。

参考

Railsのポリモーフィック関連とはなんなのか
Combining has_many :through with polymorphic associations in ActiveRecord

まとめ

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

kumewata
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした