在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.
Top Recommended Approach: Polymorphic Associations
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

