rails has_many setter should set conditions if specified - ruby-on-rails

This seems like a bug in Rails to me, but there's probably not much I can do about that. So how can I accomplish my expected behavior?
Suppose we have:
class User < ActiveRecord::Base
has_many :awesome_friends, :class_name => "Friend", :conditions => {:awesome => true}
end
And execute the code:
>> my_user.awesome_friends << Friend.new(:name=>'jim')
Afterwards, when I inspect this friend object, I see that the user_id field is populated. But I would also expect to see the "awesome" column set to 'true', which it is not.
Furthermore, if I execute the following from the console:
>> my_user.awesome_friends << Friend.new(:name=>'jim')
>> my_user.awesome_friends
= [#<Friend id:1, name:"jim", awesome:nil>]
# Quit and restart the console
>> my_user.awesome_friends
= []
Any thoughts on this? I suppose the conditions hash could be arbitrarily complex, making integration into the setter impossible. But in a way it feels like by default we are passing the condition ":user_id => self.id", and that gets set, so shouldn't others?
Thanks,
Mike
EDIT:
I found that there are callbacks for has_many, so I think I might define the relationship like this:
has_many :awesome_friends,
:class_name => "Friend",
:conditions => {:awesome => true},
:before_add => Proc.new{|p,c| c.awesome = true},
:before_remove => Proc.new{|p,c| c.awesome = false}
Although, it's starting to feel like maybe I'm just implementing some other, existing design pattern. Maybe I should subclass AwesomeFriend < Friend? Ultimately I need a couple of these has_many relationships, and subclassing get's messy with all the extra files..
EDIT 2:
Okay, thanks to everyone who commented! I ultimately wrapped up the method above into a nice little ActiveRecord extension, 'has_many_of_type'. Which works like follows:
has_many_of_type :awesome_friends, :class_name => "Friend", :type=>:awesome
Which just translates to has_many with the appropriate conditions, before_add, and before_remove params (and it assumes the existence of a column named friend_type).

You need use:
my_user.awesome_friends.create(:name=>'jim') or my_user.awesome_friends.build(:name=>'jim')
In documentation:
has_many (:conditions)
Record creations from the association are scoped if a hash is used. has_many :posts, :conditions => {:published => true} will create published posts with #blog.posts.create or #blog.posts.build.

It's :class_name rather than :class, for one thing.

This isn't a bug I don't think. The :conditions hash only deterimines how you query for the objects. But I don't think it's rational to just assume that any object you stuff in the collection could be made to conform to the conditions.
In your simple example it makes sense, but you could also put more complex logic in there.
The documentation seems pretty clear on this as well:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
:conditions
Specify the conditions that the associated object must meet in order to be included as a WHERE SQL fragment, such as authorized = 1.

Related

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

Rails Thinking Sphinx:- How to select only some fields in the result and multiple tables select(association)

I am a rookie in Thinking Sphinx for Rails.
When Sphinx found a record, it will give all the fields in the table. How can i select only the needed fields?
And in my case, i also need reference to another table. how can i do that?
Thanks
This is an old thread, but as I found it whilst looking for the same information, I thought I'd share my answer.
It's not (as far as I can tell) clearly defined on the Thinking Sphinx homepage, but the search function on a model accepts the option :select - and in answer to your second question, it also accepts :joins, so if you had two related models:
class Project < ActiveRecord::Base
attr_accessible :name
has_many :tasks
end
class Task < ActiveRecord::Base
attr_accessible :name
belongs_to :project
end
You should be able to search your tasks like so:
Task.search "Fix Bug",
:select => 'tasks.id, tasks.name, projects.name as project_name',
:joins => [:project]
There's no doubt a slightly cleaner way to do this, so I'm happy to be corrected - the general idea works though!
EDIT (for thinking sphinx v3)
:select is used as a sphinx parameter in version 3, and instead you should add :select and :joins to a :sql hash. Otherwise you get some really strange errors that aren't that obvious!
The above example then becomes:
Task.search "Fix Bug",
:sql => { :select => 'tasks.id, tasks.name, projects.name as project_name',
:joins => [:project] }

Best way to override named_scope for has_many associations in Rails?

Note: I'm using Rails 2.3.8, not 3.
I have a Photo model with a default_scope:
default_scope :conditions => ["published = ?", true], :order => :position
Calling photo_album.photos returns all published photos ordered by position as it should. However, when looping through these photo albums in an admin panel to display the number of photos in each, the results are wrong:
pluralize(photo_album.photos.count, "photo")
returns 0, as none are published.
I know that similar questions have been asked, and the reply typically is along the lines of "use a with_exclusive_scope class method". As far as I can tell, this completely prevents the use of standard Rails associations - basically resulting in something like this:
pluralize(Photo.all_photos_in_album(photo_album.id).count, "photo")
and requiring a class method like:
def Photo.all_photos_in_album(album_id)
self.with_exclusive_scope { find(:all, :conditions => ["photo_album_id = ?", album_id]) }
end
just to display the total number of photos in an album. This seems insane - overriding the default should not require abandoning Rails association conventions. with_exclusive_scope can not be used in an instance method (protected method) either - this would have allowed me to create a PhotoAlbum instance method called "all_photos" to at least preserve the semblance of associations (photo_album.all_photos). But no, that is not allowed :(
Aside from removing default_scopes, which have proven very useful across the site, does anyone know of a way override default scopes and maintain Rails association syntax?
Thanks!
Edit:
I wound up adding a PhotoAlbum instance method that, while it isn't an actual default_scope override, does at make for much nicer syntax in my views:
def all_photos_count
PhotoAlbum.count_by_sql("SELECT COUNT(id) FROM photos WHERE photo_album_id = #{self.id} ORDER BY created_at")
end
pluralize(photo_album.all_photos_count, "photo")
While it's not exactly an AR has_many association and it relies on pure SQL, it's the best compromise I've found so far.
I don't understand why you don't do in Album:
has_many :photos
has_many :published_photos, :class_name => 'Photo', :conditions => ["published = ?", true], :order => :position
This way you could do:
#album.photos.count
#album.published_photos
And so on...
For me, the best solution in this case was to use neither default_scope nor has_many conditions and to just stick with named_scopes. Two named_scopes accomplished most of what I needed to keep my views lean:
named_scope :public_list, :conditions => ["published = ?", true], :order => :position
named_scope :private_list, :order => "created_at DESC"

Pass object to has_many :conditions

I need to pass self as object not class to :conditions string, is there any way to do this?
has_many :topic,
:class => 'FileTopic',
:conditions => "id in (select * from file_topics where program_id = #{self.id})"
My problem is self is always giving me the id of the class but not the instance of the class. I guess has_many is evaluated on the class level?
Thanks
It is evalued upon loading the class, yeah. But only if you use double quotes - variables in single-quoted strings are filled upon calling. More info here.
However, maybe you should look into named scopes?
Has many is a class method. So any reference to self in its arguments are references to the class.
It looks like you want to specify the foreign key on the belongs_to side of things.
Have you tried this yet:
has_many :topic, :class => 'FileTopic', :foreign_key => "program_id"
You should really have a read through the ActiveRecord::Associations documentation if you haven't yet. There are very few association problems that can't be solved using the right set of options to belongs_to/has_one/has_many

How do I pass a string to a has_many :finder_sql parameter?

In my application, a user has_many tickets. Unfortunately, the tickets table does not have a user_id: it has a user_login (it is a legacy database). I am going to change that someday, but for now this change would have too many implications.
So how can I build a "user has_many :tickets" association through the login column?
I tried the following finder_sql, but it does not work.
class User < ActiveRecord::Base
has_many :tickets,
:finder_sql => 'select t.* from tickets t where t.user_login=#{login}'
...
end
I get a weird error:
ArgumentError: /var/lib/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:402:in `to_constant_name': Anonymous modules have no name to be referenced by
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:2355:in `interpolate_sql'
from /var/lib/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:214:in `qualified_name_for'
from /var/lib/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:477:in `const_missing'
from (eval):1:in `interpolate_sql'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/association_proxy.rb:95:in `send'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/association_proxy.rb:95:in `interpolate_sql'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/has_many_association.rb:143:in `construct_sql'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/has_many_association.rb:6:in `initialize'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations.rb:1032:in `new'
from /var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations.rb:1032:in `tickets'
from (irb):1
I also tried this finder_sql (with double quotes around the login):
:finder_sql => 'select t.* from tickets t where t.user_login="#{login}"'
But it fails the same way (and anyway, if it worked it would be vulnerable to sql injection).
In a test database, I added a user_id column in the tickets table, and tried this finder_sql:
:finder_sql => 'select t.* from tickets t where t.user_login=#{id}'
Now this works fine. So apparently, my problem has to do with the fact that the users column I am trying to use is a string, not an id.
I searched the net for quite some time... but could not find a clue.
I would love to be able to pass any parameter to the finder_sql, and write things like this:
has_many :tickets_since_subscription,
:finder_sql => ['select t.* from tickets t where t.user_login=?'+
' and t.created_at>=?', '#{login}', '#{subscription_date}']
Edit: I cannot use the :foreign_key parameter of the has_many association because my users table does have an id primary key column, used elsewhere in the application.
Edit#2: apparently I did not read the documentation thoroughly enough: the has_many association can take a :primary_key parameter, to specify which column is the local primary key (default id). Thank you Daniel for opening my eyes! I guess it answers my original question:
has_many tickets, :primary_key="login", :foreign_key="user_login"
But I would still love to know how I can make the has_many :tickets_since_subscription association work.
I think you want the :primary_key option to has_many. It allows you to specify the column on the current Table who's value is stored in the :foreign_key column on the other table.
has_many :tickets, :foreign_key => "user_login", :primary_key => "login"
I found this by reading the has_many docs.
To have something like has_many :tickets_since_subscription you can use named_scopes:
In model add:
named_scope :since_subscription, lambda { |subscription_date| { :conditions => ['created_at > ?', subscription_date] }
With this, you can find what you want like this:
user.tickets.since_subscription 3.days.ago
or
user.tickets.since_subscription user.subscription_date
(of course you need subscription_date column in user model).
You can find more examples here.
If you don't want to use named_scopes you can find what you want with this:
user.tickets.all(:conditions => ['created_at > ?', subscription_date])
I think you are looking for the :foreign_key option on has_many. That should allow you to specify that the foreign key is not user_id, but user_login, without adjusting the finder logic.
See the ActiveRecord has_many documentation for more details.
Just answering to myself, in case there is no better solution. I could not find a solution with the has_many association, so I ended up creating a simple finder method. Not great at all: it does allow me to call some_user.tickets, but it does not give me all the benefits of the has_many associations (namely the clear, delete, <<,... methods on the association itself).
def tickets
return Ticket.find(:all, :conditions=>["user_login = ?", login])
end
I am still hoping that someone will come up with a better solution.

Resources