Problem
Okay, so I have a weird structure issue, and the rails conventions I know are not helping.
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 http://blog.flatironschool.com/why-you-dont-need-has-and-belongs-to-many/). Memberships have metadata about the user's relationship with the server. This works pretty well, for what I am doing.
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:
SELECT COUNT(*) FROM "profiles" INNER JOIN "memberships" ON "profiles"."membership_id" = "memberships"."id" WHERE "memberships"."server_id" = $1
My Attempts
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.
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.
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.
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....
Question
Is there a specific convention I have missed that handles counter_caching in this fashion, or am I stuck with a choice between:
- the status quo of constantly querying the count from the database through an inner join
- risking an infinite loop of associations
Supporting Code
Here are the pertinent classes trimmed down to the relevant bits:
class User < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :servers, through: :memberships
has_many :profiles, through: :memberships
end
class Server < 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 < 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, -> (id) { joins(:membership).where(memberships: { user_id: id }) }
scope :for_server, -> (id) { joins(:membership).where(memberships: { server_id: id}) }
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :server, counter_cache: true
has_many :profiles, dependent: :destroy
end
Here are the create_tables from db/schema.rb truncated down to just the associated fields for brevity:
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