Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is my has_many through associated record (sometimes) readonly?

I have three ActiveRecord models: Partner, MembershipChannel (which is an STI model, inheriting from Channel) and ChannelMembership (I was not responsible for naming these models…)

When I load a ChannelMembership through the Partner association, I sometimes(!) end up with a readonly record. This is in Rails 3.0.9. The same code did not behave this way in 2.3.11.

> p = Partner.first
> p.channel_memberships.map(&:readonly?)
# => [false, false, false, false, false, false]
> p.reload.channel_memberships.limit(1).first.readonly?
# => false
> p.reload.channel_memberships.first.readonly?
# => true

Why is readonly? true when first is called on the association, but not on the relation from limit?

I understand that readonly is triggered if I use SQL fragments when finding a record, but this isn't the case here. It is just a plain has_many through association. The only complicating matter is that it joins on an STI model. What's more, looking at the generated SQL from the last two examples, they are identical!

I can get the behaviour I want by specifying :readonly => false on the association, but I want to understand what is going on.

There are no default scopes on Channel, MembershipChannel or ChannelMembership. Here is the association declaration on Partner:

class Partner
  has_many :membership_channels
  has_many :channel_memberships, :through => :membership_channels
end

Here is the generated SQL from my logs:

  Partner Load (0.4ms)  SELECT "partners".* FROM "partners" LIMIT 1
  ChannelMembership Load (0.7ms)  SELECT "channel_memberships".* FROM "channel_memberships" INNER JOIN "channels" ON "channel_memberships".channel_id = "channels".id WHERE (("channels".partner_id = 2) AND (("channels"."type" = 'MembershipChannel')))
  Partner Load (0.5ms)  SELECT "partners".* FROM "partners" WHERE "partners"."id" = 2 LIMIT 1
  ChannelMembership Load (1.0ms)  SELECT "channel_memberships".* FROM "channel_memberships" INNER JOIN "channels" ON "channel_memberships".channel_id = "channels".id WHERE (("channels".partner_id = 2) AND (("channels"."type" = 'MembershipChannel'))) LIMIT 1
  Partner Load (0.4ms)  SELECT "partners".* FROM "partners" WHERE "partners"."id" = 2 LIMIT 1
  ChannelMembership Load (0.6ms)  SELECT "channel_memberships".* FROM "channel_memberships" INNER JOIN "channels" ON "channel_memberships".channel_id = "channels".id WHERE (("channels".partner_id = 2) AND (("channels"."type" = 'MembershipChannel'))) LIMIT 1
like image 965
Martin Svalin Avatar asked Nov 05 '22 16:11

Martin Svalin


1 Answers

I was able to reproduce your problem through a basic has_many :through association and am also as to what's causing it.

From what I can tell, it only happens when the reload method is called on the original object. I'm not sure if this is because of anything that reload's doing specifically, or perhaps because certain attribute flags are being reset?

My second theory is that it has something to do with the fact that

p.reload.channel_memberships.limit(1)

returns an ActiveRecord::Relation through which you obtain your first ChannelMembership, and

p.reload.channel_memberships.first

loads it directly from the association. Perhaps some combination of reload resetting certain cached items (I don't know the AR source) is flagging the association as read only. When you apply the limit(1) scope on it, it may be resetting these in a new relation, and working as you'd expect.

I'd poke around ActiveRecord::Persistence / Associations a bit more for the full answer.

like image 151
Kristian PD Avatar answered Nov 10 '22 19:11

Kristian PD