Proper way to prevent ActiveRecord::ReadOnlyRecord? - ruby-on-rails

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!

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.

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!

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

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

Related

rails: has_many association: best practices to validate access to array

I'm pretty new to ruby and ruby-on-rails, so I need to learn best practices.
I have Tag model, each tag can have many sub-tags and many super-tags:
has_many :super_tags, :through => :tag_hier_rels, :source => :super_tag
has_many :sub_tags, :through => :reverse_tag_hier_rels, :source => :sub_tag
has_many :tag_hier_rels, :foreign_key => "sub_tag_id"
has_many :reverse_tag_hier_rels, :foreign_key => "super_tag_id", :class_name => "TagHierRel"
I need to prevent user to create circular references. But with auto-generated methods :super_tags and :sub_tags I can't do this: everyone can do something like:
tag.super_tags.push another_tag, and I have no control on this.
Rails validation mechanism isn't useful here: this mechanism prevents user to save invalid objects to database, but I need to prevent him even to modify object incorrectly: if I have circular reference, and I need to get all the sub-tags or super-tags recursively, I will run into stack overflow.
So I have done the following:
Declared these associations as private ones:
private :sub_tags=, :sub_tags
private :super_tags=, :super_tags
Added methods with _copy postfix:
def sub_tags_copy
return sub_tags.clone
end
def super_tags_copy
return super_tags.clone
end
And added methods that actualy modify arrays:
def sub_tags_push(tag)
sub_tags.push tag if !self.all_sub_tags.include? tag and !self.all_super_tags.include? tag
end
def super_tags_push(tag)
super_tags.push tag if !self.all_sub_tags.include? tag and !self.all_super_tags.include? tag
end
# TODO: more methods (at least we need to remove tags)
(methods all_sub_tags and all_super_tags generate arrays recursively)
It works, but I don't really like this solution: at least, it's not obvious for user that he should use ..._copy methods.
Probably I am doing this wrong?
UPD:
Or, is it bad practice in general to disallow user to change an object in wrong way? Probably I should allow user to change an object in wrong way, but only validate it before saving?
At least, I already figured out that it's hard to supply error messages if user does something wrong: currently, model just silently does not modify an object, and no error message is generated. I have to implement my own error messages engine, and this fact is an evidence that approach is really bad... I seem to struggle against the framework instead of using it.
You are always referring to user where you mean programmers?
If that is the case, you should try to assert this kind of stuff through
Education (developer to developer)
Validation (via tests or rails validations)
Constraints (on the database level through db constraints or triggers)
I would try to do it top down.

Rails' includes() doesn't work with dynamic association conditions

Working on a multi-tenant app where most of my models will have a tenant_id field so I can ensure read permissions by finding through the association (current_tenant.applications.find(params[:id])):
class Application < ActiveRecord::Base
belongs_to :tenant
has_many :app_questions, :conditions => proc {{:tenant_id => tenant_id}}, :dependent => :destroy
end
I like how this allows me to elegantly create a new AppQuestion with the tenant_id set automatically:
#application = current_tenant.applications.find(params[:app_question][:application_id])
#question = #application.app_questions.build(params[:app_question])
#...
Problem is, when I try to use includes() to eager-load the association it throws an error:
current_tenant.applications.where(:id => params[:id]).includes(:app_questions => :app_choices).first
NoMethodError (undefined method `tenant_id' for #<Class:0x007fbffd4a9420>):
app/models/application.rb:7:in `block in <class:Application>'
I could refactor so that I don't have to have the proc in the association conditions, but am wondering if anyone has a better solution.
The ref does say: "If you need to evaluate conditions dynamically at runtime, use a proc"
I've replied to the other question with more details trying to explain why this cannot work.
When they say dynamic is because the proc can be executed at runtime, but not in the context of an existing instance of the Application class because it doesn't exist when you invoke this relation
Application.where(:id => params[:id]).includes(:app_questions => :app_choices)
The ability for :conditions to accept a proc isn't documented in the ref. I suspect it doesn't work the way you guessed it might.
:conditions accepts either an SQL WHERE clause, or a hash that can be turned into on. It's inserted into the SQL that gets the :app_questions records, and if it's a proc it's only called once to get the snippet for the SQL statement to be constructed.
It might help to have a look at your database relationships. Should app_questions link to tenants or applications?
Assuming the relation itself works you could try preload instead of includes

Default conditions for Rails models

I have a model which has a field called deleted, which is used to mark those deleted items.
So normally I would just want to query those having deleted = false items, and in some special cases to list those deleted items for restoring.
Is it possible to do that? What I could do now is just using a named scope having :conditions => {:deleted => false}
Is there a better way to do it so that When I do Item.other_named_scope, I could find all those not-deleted items?
You can use default_scope for this.
class Post
default_scope :conditions => {:deleted => false}
end
Now all queries to the Post model will be on ACTIVE posts. When you want to override this behavior use with_exclusive_scope:
Post.with_exclusive_scope{ find_all_by_deleted(true) } #returns deleted records
Reference:
Link 1
Caveat
The default_scope affects every finder call. It should be used with care and with full awareness of the unwanted side-effects.

How do I make named_scope work properly with a joined table?

Here's my situation. I have two tables: pledges and pledge_transactions. When a user makes a pledge, he has only a row in the pledges table.
Later when it comes time to fulfill the pledge, each payment is logged in my pledge_transactions table.
I need to be able to query all open pledges which means that the sum of the amounts in the transactions table is less than the pledged amount.
Here's what I have so far:
named_scope :open,
:group => 'pledges.id',
:include => :transactions,
:select => 'pledge_transactions.*',
:conditions => 'pledge_transactions.id is not null or pledge_transactions.id is null',
:having => 'sum(pledge_transactions.amount) < pledges.amount or sum(pledge_transactions.amount) is null'
You might be asking yourself why I have that superfluous and ridiculous conditions option specified. The answer is that when I don't force ActiveRecord to acknowledge the pledge_transactions table in the conditions, it omits it completely, which means my having clause becomes meaningless.
My belief is that I have run into a shortcoming of ActiveRecord.
Ultimately I need to be able to do the following:
Pledge.open
Pledge.open.count
Pledge.open.find(:all, ...)
etc.
Anybody have a more elegant answer to this problem? Please no suggestions of incrementing a pledges amount_given field each time a transaction occurs. That feels like a band-aid approach and I'm much more of a fan of keeping the pledge static after it is created and computing the difference.
If I weren't using Rails here, I'd just create a view and be done with it.
Thanks!
How is the :transactions association defined? Does it stipulate :class_name = 'PledgeTransaction' (or whatever the class is, if it uses set_table_name)?
Have you looked at the :joins parameter? I think it might be what you were looking for. Certainly that :conditions thing doesn't look right.
If I weren't using Rails here, I'd just create a view and be done with it
Just because it's Rails doesn't mean you can't use a view. OK, depending on the way it's constructed you may not be able to update it, but otherwise go for it. You can create and drop views in migrations, too:
class CreateReallyUsefulView < ActiveRecord::Migration
def self.up
# this is Oracle, I don't know if CREATE OR REPLACE is widely-supported
sql = %{
CREATE OR REPLACE VIEW really_usefuls AS
SELECT
... blah blah SQL blah
}
execute sql
end
def self.down
execute 'drop view really_usefuls'
end
end
class ReallyUseful < ActiveRecord::Base
# all the usual stuff here, considering overriding the C, U and D parts
# of CRUD if it's supposed to be read-only and you're paranoid
end
I think the books/docs don't go into this much because implementation of, and support for views varies significantly across platforms.
I think using NOT EXISTS in your conditions will get you what you want. I'm assuming the association is on the pledge_transaction as pledge_id. Here's how I would implement #open
named_scope :open,
:conditions =>
"
NOT EXISTS (
select 1
from pledge_transactions
where
pledge.id = pledge_transactions.pledge_id AND
pledge_transactions.amount < pledge.amount
)
"
}
}
This will allow you to do Pledge.open, Pledge.open.count and Pledge.open.find_by_{what ever}.

Overwriting/Adding an ActiveRecord association dynamically using a singleton class

The business logic is this: Users are in a Boat through a join table, I guess let's call that model a Ticket. But when a User instance wants to check who else is on the boat, there's a condition that asks if that user has permission see everyone on the Boat, or just certain people on the Boat. If a User can see everyone, the normal deal is fine: some_user.boats.first.users returns all users with a ticket for that boat. But for some users, the only people that are on the boat (as far as they're concerned) are people in, let's say the dining room. So if User's ticket is "tagged" (using an acts_as_taggable style system) with "Dining Room", the only Users returned from some_user.boats.first.users should be Users with tickets tagged "Dining Room".
Just for the record, I'm not trying to design something to be insane from the getgo - I'm trying to wedge this arbitrary grouping into a (mostly) existent system.
So we've got:
class User
has_many :tickets
has_many :boats, :through => :tickets
end
class Ticket
belongs_to :user
belongs_to :boat
end
class Boat
has_many :tickets
has_many :users, :through => :tickets
end
Initially, I thought that I could conditionally modify the virtual class like:
singleton = class << a_user_instance ; self ; end
singleton.class_eval(<<-code
has_many :tickets, :include => :tags, :conditions => ['tags.id in (?)', [#{tag_ids.to_s(:db)}]]
code
)
That gets all the way down to generating the SQL, but when generated, it generates SQL ending in:
LEFT OUTER JOIN "tags" ON ("tags"."id" = "taggings"."tag_id") WHERE ("tickets"._id = 1069416589 AND (tags.id in (5001,4502)))
I've tried digging around the ActiveRecord code, but I can't find anywhere that would prefix that 'id' in the SQL above with an underscore. I know that associations are loaded when an ActiveRecord class is loaded, and I'd assume the same with a singleton class. shrug.
I also used an alias_method_chain like:
singleton = class << a_user_instance ; self ; end
singleton.class_eval(<<-code
def tickets_with_tag_filtering
tags = Tag.find(etc, etc)
tickets_without_tag_filtering.scoped(:include => :tags, :conditions => {:'tags.id' => tags})
end
alias_method_chain :tickets, :tag_filtering
code
)
But while that approach produces the desired Tickets, any joins on those tickets use the conditions in the class, not the virtual class. some_user.boats.first.users returns all users.
Any type of comment will be appreciated, especially if I'm barking up the wrong tree with this approach. Thanks!
So a wild guess about your underscore issue is that Rails is generating the assocation code based on the context at the time of evaluation. Being in a singleton class could mess this up, like so:
"#{owner.table_name}.#{association.class.name}_id = #{association.id}"
You could get in there and define a class name property on your singleton class and see if that fixes the issue.
On the whole I don't recommend this. It creates behavior that is agonizing to track down and impossible to extend effectively. It creates a landmine in the codebase that will wound you or someone you love at a later time.
Instead, consider using a named_scope declaration:
class User
has_many :taggings, :through => :tickets
named_scope :visible_to, lambda { |looking_user|
{ :include => [ :tickets, :taggings ],
:conditions => [ "tickets.boat_id in (?) and taggings.ticket_id = tickets.id and taggings.tag_id in (?)", looking_user.boat_ids, looking_user.tag_ids ]
}
}
end
While you may have to go back and edit some code, this is much more flexible in the ways it can be used:
Boat.last.users.visible_to( current_user )
It's clear that a restriction is being placed on the find, and what the purpose of that restriction is. Because the conditions are dynamically calculated at runtime, you can deal with the next weird modification your client hits you with. Say some of their users have xray vision and clairvoyance:
class User
named_scope :visible_to, lambda { |looking_user|
if looking_user.superhuman?
{}
else
{ :include => [ :tickets, :taggings ],
:conditions => [ "tickets.boat_id in (?) and taggings.ticket_id = tickets.id and taggings.tag_id in (?)", looking_user.boat_ids, looking_user.tag_ids ]
}
end
}
end
By returning an empty hash, you can effectively nullify the effect of the scope.
Why not just grab all users on the boat and include their tags.
Then run a quick filter to include & return only the users with the same tag as the inquiring user.
What version of Rails are you using? Have you tried upgrading to see if the underscore issue is fixed? It's like it can't find the foreign key to put in as "tag_id" or somethin'.
My ruby-fu is limited, so I'm not sure how to dynamically include the correct method options at run-time.
Just to help you clarify, you have to worry about this two places. You want to filter a user's viewable users so they only see users with the same tags. Your structure is:
user <--> tickets <--> boats <--> tickets <--> users
... right?
So, you need to filter both sets of tickets down to the ones with the current_user's tags.
Maybe you just need a current_user.viewable_users() method and then filter everything through that? I'm not sure what existing functionality you've got to preserve.
Blech, I don't feel like I'm helping you at all. Sorry.
Your approach is the problem. I know it seems expedient at the moment to hack something in where you don't have to refactor the existing call sites, but I believe given time this will come back to haunt you as the source of bugs and complexity.
Sleeping dogs that lie come back to bite you hard, in my experience. Typically in the form of a future developer who doesn't know your association is "magic" and uses it assuming it's just pail ole rails. He/she likely won't even have a reason to write a test case that would expose the behavior either, which raises the odds you'll only find out about the bug when it's live in production and the client is unhappy. Is it really worth the time you're saving now?
Austinfrombostin is pointing the way. Different semantics? Different names. Rule number one is always to write code that says what it does as clearly as possible. Anything else is the path of madness.

Resources