30

I'm currently using Rails 2.3.9. I understand that specifying the :joins option in a query without an explicit :select automatically makes any records that are returned read-only. I have a situation where I would like to update the records and while I've read about different ways to approach it, I was wondering which way is the preferred or "proper" way.

Specifically, my situation is that I have the following User model with an active named scope that performs a JOIN with the subscriptions table:

class User < ActiveRecord::Base
  has_one :subscription

  named_scope :active, :conditions => { :subscriptions => { :status => 'active' } }, :joins => :subscription
end

When I call User.active.all, the user records that are returned are all read-only, so if, for instance, I call update_attributes! on a user, ActiveRecord::ReadOnlyRecord will be raised.

Through reading various sources, it seems a popular way to get around this is by adding :readonly => false to the query. However, I was wondering the following:

  • Is this safe? I understand the reason why Rails sets it to read-only in the first place is because, according to the Rails documentation, "they will have attributes that do not correspond to the table’s columns." However, the SQL query that is generated from this call uses SELECT `users`.* anyway, which appears to be safe, so what is Rails trying to guard against in the first place? It would appear that Rails should be guarding against the case when :select is actually explicitly specified, which is the reverse of the actual behavior, so am I not properly understanding the purpose of automatically setting the read-only flag on :joins?
  • Does this seem like a hack? It doesn't seem proper that the definition of a named scope should care about explicitly setting :readonly => false. I'm also afraid of side effects if the named scoped is chained with other named scopes. If I try to specify it outside of the scope (e.g., by doing User.active.scoped(:readonly => false) or User.scoped(:readonly => false).active), it doesn't appear to work.

One other way I've read to get around this is to change the :joins to an :include. I understand the behavior of this better, but are there any disadvantages to this (other than the unnecessary reading of all the columns in the subscriptions table)?

Lastly, I could also retrieve the query again using the record IDs by calling User.find_all_by_id(User.active.map(&:id)), but I find this to be more of a workaround rather than a possible solution since it generates an extra SQL query.

Are there any other possible solutions? What would be the preferred solution in this situation? I've read the answer given in the previous StackOverflow question about this, but it doesn't seem to give specific guidance of what would be considered correct.

Thanks in advance!

Community
  • 1
  • 1
Claw
  • 767
  • 8
  • 22
  • Could you narrow down your questions to 1 or 2 ? – charlysisto Oct 12 '11 at 11:05
  • @charlysisto: I apologize for all the individual sub-questions, but the main thing I wanted to know is whether there is specific guidance on the correct way to work around the `ActiveRecord::ReadOnlyRecord` issue. The other questions were posed mainly so that I could understand why a particular solution is better than another. – Claw Oct 12 '11 at 19:59

4 Answers4

14

I believe that it would be customary and acceptable in this case to use :include instead of :join. I think that :join is only used in rare specialized circumstances, whereas :include is pretty common.

If you're not going to be updating all of the active users, then it's probably wise to add an additional named scope or find condition to further narrow down which users you're loading so that you're not loading extra users & subscriptions unnecessarily. For instance...

User.active.some_further_limiting_scope(:with_an_argument)
  #or
User.active.find(:all, :conditions => {:etc => 'etc'})

If you decide that you still want to use the :join, and are only going to update a small percentage of the loaded users, then it's probably best to reload just the user you want to update right before doing so. Such as...

readonly_users = User.active
# insert some other code that picks out a particular user to update
User.find(readonly_users[@index].id).update_attributes(:etc => 'etc')

If you really do need to load all active users, and you want to stick with the :join, and you will likely be updating most or all of the users, then your idea to reload them with an array of IDs is probably your best choice.

#no need to do find_all_by_id in this case. A simple find() is sufficient.
writable_users_without_subscriptions = User.find(Users.active.map(&:id))

I hope that helps. I'm curious which option you go with, or if you found another solution more appropriate for your scenario.

Jon Garvin
  • 1,178
  • 9
  • 26
  • Thanks for the response! I gave you an upvote (and I see you already got the bounty that someone else set on this question) but I still haven't really seen an answer to my liking since posting this question. I feel I'm still lacking enough understanding of the reasons for each approach. – Claw Oct 22 '11 at 10:09
  • 2
    For what I'm trying to do, I do need to load every active user and haven't really heard a good argument so far against using `:join`, especially since my use-case more typically matches when one would use a `:join` (as explained in [this Railscast](http://railscasts.com/episodes/181-include-vs-joins)). For that reason, I'm currently going with the option to refetch the query again using the record IDs, but I still regard it as a workaround for the time being. – Claw Oct 22 '11 at 10:10
3

I think the best solution is to use .join as you have already and do a separate find()

One crucial difference of using :include is that it uses outer join while :join uses an inner join! So using :include may solve the read-only problem, but the result might be wrong!

Zack Xu
  • 11,505
  • 9
  • 70
  • 78
2

I ran across this same issue and was not comfortable using :readonly => false

As a result I did an explicit select namely :select => 'users.*' and felt that it seemed like less of a hack.

You could consider doing the following:

class User < ActiveRecord::Base
  has_one :subscription

  named_scope :active, :select => 'users.*', :conditions => { :subscriptions => { :status => 'active' } }, :joins => :subscription
end
saneshark
  • 1,243
  • 13
  • 25
0

Regarding your sub-question: so am I not properly understanding the purpose of automatically setting the read-only flag on :joins?

I believe the answer is: With a joins query, you're getting back a single record with the User + Subscription table attributes. If you tried to update one of the attributes (say "subscription_num") in the Subscription table instead of the User table, the update statement to the User table wouldn't be able to find subscription_num and would crash. So the join-scopes are read-only by default to prevent that from happening.

Reference: 1) http://blog.ethanvizitei.com/2009/05/joins-and-namedscopes-in-activerecord.html

  • 1
    The SQL query that is generated only selects the User attributes (``SELECT `users`.*``, which I already mentioned) and the Subscription table attributes are not accessible from the record that is returned. This does appear to contradict what is stated in your source; has the behavior perhaps changed since it was written? – Claw Jul 25 '12 at 01:03