Pass object to has_many :conditions - ruby-on-rails

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

Related

Static scope inside has_many in Rails 4

I'm having the following issue. I want to set a certain amount of has_many relations in a model, with names taken from a passed array (or in this case, the keys of a hash). Like this:
object_class_names = {:foo => FooClass, :bar => BarClass}
for key_name in object_class_names.keys
has_many "#{key_name}_objects".to_sym,
-> {where(var: key_name)},
:class_name => object_class_names[key_name]
end
This results in two has_many relations: some_object.foo_objects & some_object.bar_objects. Both have a specific class_name and a specific scope, set in the where clause in the lambda. However, because the scope is a lambda it gets the key_name in the where clause dynamically. This is the last known key_name variable, which is the last one in the loop, in this case 'bar'. So both foo_objects and bar_objects return a set of objects scoped with where(var: "bar").
Usually the lambda makes a great way to pass in dynamic scopes in has_many relations, but in this case I don't really need it. Is it possible to set a static scope inside a has_many relation?
You might use Hash#each_pair here:
object_class_names.each_pair do |key_name, klass|
has_many :"#{key_name}_objects", -> { where(var: key_name) }, class_name: klass.to_s
end
Does this work? I haven't tested it, but the theory is that you're accessing the specific key name you want, rather than the last-known key name.
object_class_names.keys.each_with_index do |key_name, index|
has_many "#{key_name}_objects",
-> { where(:var => object_class_names.keys[index]) },
:class_name => object_class_names[key_name]
end

rails has_many setter should set conditions if specified

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.

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] }

has_many and a legacy scheme

I'm looking for advice here:
I have two tables in legacy scheme, A and B, joined on A.somename = B.othername. Both of those columns are strings. So how do I set up relations between them in rails (v2.1.0)? Given that A has many B's, what would be the best practice:
use :finder_sql and just write a SQL select,
configure the relation through other parameters (how? I know I can set :foreign_key = 'othername', but that will just try to set up a A.id = B.othername relation - what can I do to set up the correct one?),
something else that has not crossed my mind.
So, what would you suggest?
If you're stuck with Rails 2.1 for some reason, the best option seems to be using set_primary_key, like this:
class A
set_primary_key 'somename'
has_many :bs, :foreign_key => 'othername'
end
There is also an alias that lets you use attribution-like syntax for that (self.primary_key = 'somename').
By the way, if you're able to upgrade to 2.3, you can use the primary_key option directly with has_many, like this:
has_many :debitos, :primary_key => 'somename', :foreign_key => 'othername'
If you choose to use this, you won't need to declare the primary key for the class using set_primary_key.

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