title: How do you setup counter_cache on a second degree model in Ruby on Rails
body: <p><strong>Problem</strong></p>
  
  <p>Okay, so I have a weird structure issue, and the rails conventions I know are not helping.</p>
  
  <p>In my system, Users have Memberships to Servers.  This is generically a HABTM association, but I am using has_many through the Memberships model to simplify some things (according to advice from <a href="http://blog.flatironschool.com/why-you-dont-need-has-and-belongs-to-many/" rel="nofollow noreferrer">http://blog.flatironschool.com/why-you-dont-need-has-and-belongs-to-many/</a>).  Memberships have metadata about the user's relationship with the server.  This works pretty well, for what I am doing.</p>
  
  <p>Now, the trouble I run into is that each Membership can have many Profiles for the User to use on the Server.  Since Profiles are associated to one User and one Server, I just have the Profiles belong_to Membership.  What I want to do is use a counter_cache for the Profiles on the Server model since Rails seems to be running this query constantly:</p>
  
  <p><code>SELECT COUNT(*) FROM "profiles" INNER JOIN "memberships" ON "profiles"."membership_id" = "memberships"."id" WHERE "memberships"."server_id" = $1</code></p>
  
  <p><strong>My Attempts</strong></p>
  
  <p>Firstly, Rails does not have a belongs_to through, so profiles cannot "belong_to" servers through the membership model, thus I cannot provide the counter_cache there.</p>
  
  <p>My second thought was to use a delegate on the Server model to delegate :profiles_count to Membership and just do the counter_cache via memberships, but I haven't been able to get this to work.</p>
  
  <p>My third attempt was to delegate :server_id to :membership in Profile and see if that would allow me to build a belongs_to association with the Server model.  No good.  Funny thing here, there are no errors thrown for this, it just rolls back a save on a new profile without warning.</p>
  
  <p>The only other thing I can think is to add server_id and user_id columns to the Profiles model effectively duplicating the HABTM association from Memberships.  Im not sure how stable this level of interconnectedness is going to be.  This is effectively going to create has_many associations in two directions, with circular references chains: membership->server->profile->membership....</p>
  
  <p><strong>Question</strong></p>
  
  <p>Is there a specific convention I have missed that handles counter_caching in this fashion, or am I stuck with a choice between:</p>
  
  <ol>
  <li>the status quo of constantly querying the count from the database through an inner join</li>
  <li>risking an infinite loop of associations</li>
  </ol>
  
  <p><strong>Supporting Code</strong></p>
  
  <p>Here are the pertinent classes trimmed down to the relevant bits:</p>
  
  <pre><code>class User &lt; ApplicationRecord
    has_many :memberships, dependent: :destroy
    has_many :servers, through: :memberships
    has_many :profiles, through: :memberships
  end
  
  class Server &lt; ApplicationRecord
    has_many :memberships
    has_many :users, through: :memberships
    has_many :profiles, through: :memberships
  
    # This did not work with either singular or plural membership:
    # delegate :profiles_count, to: :membership
  end
  
  class Profile &lt; ApplicationRecord
    belongs_to :membership, counter_cache: true
  
    # The following also does not work:
    # belongs_to :server, counter_cache: true
    # delegate :server_id, to: :membership
  
    scope :for_user, -&gt; (id) { joins(:membership).where(memberships: { user_id: id }) }
    scope :for_server, -&gt; (id) { joins(:membership).where(memberships: { server_id: id}) }
  end
  
  class Membership &lt; ActiveRecord::Base
    belongs_to :user
    belongs_to :server, counter_cache: true
    has_many :profiles, dependent: :destroy
  end
  </code></pre>
  
  <p>Here are the create_tables from db/schema.rb truncated down to just the associated fields for brevity:</p>
  
  <pre><code>  create_table "users", force: :cascade do |t|
      # ...
    end
  
    create_table "servers", force: :cascade do |t|
      # ...
      t.integer "memberships_count", default: 0
      t.integer "profiles_count", default: 0
    end
  
    create_table "profiles", force: :cascade do |t|
      t.bigint "membership_id"
      # ...
    end
  
    create_table "memberships", force: :cascade do |t|
      t.bigint "user_id"
      t.bigint "server_id"
      # ...
      t.integer "profiles_count", default: 0
    end
  </code></pre>