You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Rails中使用抽象类实现has_many关联的方案咨询

Hey there! I totally get why STI doesn’t feel like the right fit here—when your CreditCard and BankAccount models have such distinct data structures, shoving everything into a single table gets messy fast. Let’s walk through the most mature Rails solutions for this scenario, with a focus on making that subscription.payment_source.default syntax work exactly how you want.

This is Rails’ go-to pattern for handling relationships where a single model needs to connect to multiple, structurally different models. Here’s how to set it up:

1. Database Schema Setup

First, create a payment_sources table that acts as the bridge between your Subscription and the two payment method models. It’ll hold the subscription reference, a polymorphic link to either CreditCard or BankAccount, and a flag for the default payment method.

Generate the model with:

rails generate model PaymentSource subscription:references payable:references{polymorphic} default:boolean

Then, update the migration to add a uniqueness constraint (ensuring only one default payment source per subscription):

class CreatePaymentSources < ActiveRecord::Migration[7.0]
  def change
    create_table :payment_sources do |t|
      t.references :subscription, null: false, foreign_key: true
      t.references :payable, polymorphic: true, null: false
      t.boolean :default, default: false

      t.timestamps
    end

    # Ensure one default payment source per subscription
    add_index :payment_sources, [:subscription_id], where: "default = true", unique: true
  end
end

2. Model Associations

Define the relationships in each model to tie everything together:

Subscription Model

class Subscription < ApplicationRecord
  has_many :payment_sources, dependent: :destroy

  # Add this method to mimic the `subscription.payment_source.default` syntax you want
  def payment_source
    OpenStruct.new(
      default: payment_sources.find_by(default: true)&.payable
    )
  end

  # Optional: A more explicit method for better code readability
  def default_payment_method
    payment_source.default
  end
end

PaymentSource Model

class PaymentSource < ApplicationRecord
  belongs_to :subscription
  belongs_to :payable, polymorphic: true

  # Validate that only one default exists per subscription
  validates :default, uniqueness: { scope: :subscription_id }, if: -> { default? }
end

CreditCard Model

class CreditCard < ApplicationRecord
  has_one :payment_source, as: :payable, dependent: :destroy
  has_one :subscription, through: :payment_source

  # Add your credit card-specific fields and logic here (e.g., expiration_date, cvv)
end

BankAccount Model

class BankAccount < ApplicationRecord
  has_one :payment_source, as: :payable, dependent: :destroy
  has_one :subscription, through: :payment_source

  # Add your bank account-specific fields here (e.g., routing_number, account_number)
end

3. Usage

Now you can use exactly the syntax you wanted, plus some flexible alternatives:

# Get the default payment method (returns a CreditCard/BankAccount instance or nil)
subscription.payment_source.default

# Or use the explicit method for clearer code
subscription.default_payment_method

# Set a payment source as the default
payment_source = subscription.payment_sources.find_by(payable: some_credit_card)
payment_source.update(default: true)

Why This Beats STI

STI forces all payment method fields into a single table, leading to lots of unused columns (e.g., cvv on bank account records) and tightly coupled logic. Polymorphic associations keep each payment method’s data and logic isolated in their own tables, making your codebase cleaner and easier to extend later (if you add a PayPal payment method, for example).

Honorable Mention: JSONB for Flexible Data

If your payment method structures might evolve frequently, you could store their unique data in a JSONB column on PaymentSource instead of separate tables. But this is better for scenarios where the data is unstructured or rarely needs to be queried directly. For your case—where CreditCard and BankAccount have distinct, fixed structures—polymorphic associations are the more maintainable choice.

内容的提问来源于stack exchange,提问作者mdh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:40:56