I'm building a token provisioning API that enforces quotas. When I try to create a Token, I keep getting the following error, and I cannot figure out why:
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection: Cannot modify association 'Token#limits' because the source reflection class 'Limit'is associated to 'Resource' via :has_many.
I have distilled this down to an MVP Rails 7.1.3.4 app on Ruby 3.3.3.
# db/schema.rbActiveRecord::Schema[7.1].define(version: 2024_06_28_175444) do create_table "limits", force: :cascade do |t| t.integer "plan_id", null: false t.integer "resource_id", null: false t.integer "budget", null: false t.index ["plan_id"], name: "index_limits_on_plan_id" t.index ["resource_id"], name: "index_limits_on_resource_id" end create_table "plans", force: :cascade do |t| t.string "name", null: false end create_table "resources", force: :cascade do |t| t.string "name", null: false end create_table "subscriptions", force: :cascade do |t| t.integer "plan_id", null: false t.index ["plan_id"], name: "index_subscriptions_on_plan_id" end create_table "tokens", force: :cascade do |t| t.integer "subscription_id", null: false t.integer "resource_id", null: false t.index ["resource_id"], name: "index_tokens_on_resource_id" t.index ["subscription_id"], name: "index_tokens_on_subscription_id" endend
class Plan < ApplicationRecord; endclass Resource < ApplicationRecord has_many :limitsendclass Limit < ApplicationRecord belongs_to :plan belongs_to :resourceendclass Subscription < ApplicationRecord belongs_to :planendclass Token < ApplicationRecord belongs_to :subscription belongs_to :resource has_one :plan, through: :subscription has_many :limits, ->(token) { where(plan: token.plan) }, through: :resource validate :limits_apply private def limits_apply errors.add(:limits, "no limits apply to this token") unless limits.length > 0 endend
# tests/token_test.rbrequire 'test_helper'class TokenTest < ActiveSupport::TestCase setup do @resource = Resource.create name: 'Widget' @plan = Plan.create name: 'Bronze' @subscription = Subscription.create plan: @plan @limit = Limit.create plan: @plan, resource: @resource, budget: 5 end test '.create!' do assert_nothing_raised do Token.create!(resource: @resource, subscription: @subscription) end endend
$ rails testRunning 1 tests in a single process (parallelization threshold is 50)Run options: --seed 49750# Running:EError:TokenTest#test_.create!:ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection: Cannot modify association 'Token#limits' because the source reflection class 'Limit'is associated to 'Resource' via :has_many. test/models/token_test.rb:13:in `block (2 levels) in <class:TokenTest>' test/models/token_test.rb:12:in `block in <class:TokenTest>'bin/rails test test/models/token_test.rb:11Finished in 0.043282s, 23.1043 runs/s, 0.0000 assertions/s.1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
This issue arises only when the :quotas
relation on the Token
model is loaded before save. If I change the :quotas
relation to a plain instance method, the issue disappears:
class Token < ApplicationRecord # ... # has_many :limits, ->(token) { where(plan: token.plan) }, through: :resource def limits resource.limits.where(plan: plan) end # ...end
$ rails testRunning 1 tests in a single process (parallelization threshold is 50)Run options: --seed 56588# Running:.Finished in 0.049800s, 20.0803 runs/s, 20.0803 assertions/s.1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
What am I missing in regards to my association definitions to make this work?