Rails is doing what I want - but I don't understand how

Hi guys, I have the strangest thing happening. The funny part is its
doing exactly what I want to do, I just don’t understand how.

Basically here is my model.

class Role < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :rights

def self.names
names = Array.new()
for role in Role.find :all
names << role.name
end
return names
end
end

migration file for reference:

class CreateRoles < ActiveRecord::Migration

def self.up
create_table :roles_users, :id => false do |t|
t.column “role_id”, :integer
t.column “user_id”, :integer
end

create_table :roles do |t|
t.column "name", :string
end

end

def self.down
drop_table :roles_users
drop_table :roles
end
end

As you can see I wrote a self.names methods so that I can easily call
all the names out of the roles table like so:

role_names = Role.names
=> [“trainers”,“admins”]

So that makes perfect sense.

So the weird but cool part, is that when I go to grab all the names of
the roles that belong to a user like so:

user = User.find :first
=> #<User:0x25cd680 @attributes={“id”=>“1”,
“login”=>“trainer”,“email”=>“[email protected]”}>

user.roles
=> [#<Role:0x25c6998 @attributes={“name”=>“trainer”, “role_id”=>“1”,
“id”=>“1”, “user_id”=>“3”}>]

user.roles.names
=> [“trainer”]

How does that work?
I aspected it to error out, or even require some sort of special
instance ‘names’ definition at the very least.

So, I’ve been digging and have released that ‘roles’ just calls a
special has_and_belongs_to_many find definition, but I’m still floored
that this works the why I want it to - but just can’t grasp what is
going on. I’m guessing that the Role.find methods I’m calling from
within the self.names method is using a cached copy of all the roles
found earlier by the user.roles methods???

Any suggestions or riddle solving would be greatly appreciated.
Thanks,
Jim

you’re mostly right. here’s an example to see why it works

try doing

user.roles.ids

you should get a NoMethodError: undefined method `ids’ for Role:Class

Note the Role:Class…this is the key…see you DO have a names class
method for Role

so yes, it is a bit of rails ‘magic’ going on. i’m not sure exactly
how it works though as the query that is generated is NOT the find
:all query

ans a matter of fact, both the below calls generates the same query.

user.roles
user.roles.names

my guess is that there is some sort of with_scope or association
extension going on…as if you were to remove the names method from
your Role class and add this association extension

has_and_belongs_to_many :roles, :join_table => “roles_users” do
def names
find(:all).collect { |p| p.name }
end
end

to your user class, you would see the same results only you don’t have
the benefit of having a class method any more.

that is quite strange though. perhaps someone else can provide more
details on this behavior.

Chris

On Aug 18, 2006, at 10:35 AM, Jim F. wrote:

names = Array.new()
for role in Role.find :all
  names << role.name
end
return names

end
end

I’m not sure about the rest of the email, but your names method could
be simpler:

def self.names
find(:all).collect { |r| r.name }
end

:slight_smile:


– Tom M.

Chris H. wrote:

so yes, it is a bit of rails ‘magic’ going on. i’m not sure exactly
how it works though as the query that is generated is NOT the find
:all query

ans a matter of fact, both the below calls generates the same query.

user.roles
user.roles.names

Given that you can do user.roles.find(…), and this adds the condition
that the role found belongs to the user (and the find appears as a class
method on Role), it looks as if the same magic is being applied to the
database call in the names class method.

You can check the query by looking in the development.log.

regards

Justin

On 8/18/06, Jim F. [email protected] wrote:

=> [“trainer”]
found earlier by the user.roles methods???

Any suggestions or riddle solving would be greatly appreciated.
Thanks,
Jim

Hi Jim

It’s not that complicated: first, there is a proxy class that is being
used here called HasAndBelongsToManyAssociation. It captures the
basic method calls/messages sent to the associated collection, so
user.roles for example. In HasAndBelongsToManyAssociation (which is
by the way defined in
active_record/associations/has_and_belongs_to_many_association.rb)
there is a method_missing. Take a look at its definition to
understand.

Cheers,

Not sure I’m seeing straight or completely understanding the question
but it seems to me that that will only work when the given user only
has a single role, otherwise you’ll have to iterate over each roll to
get the names? Am I missing the point here? I don’t see the confusion.

Tim

Bosko M. wrote:

On 8/18/06, Jim F. [email protected] wrote:

=> [“trainer”]
found earlier by the user.roles methods???

Any suggestions or riddle solving would be greatly appreciated.
Thanks,
Jim

Hi Jim

It’s not that complicated: first, there is a proxy class that is being
used here called HasAndBelongsToManyAssociation. It captures the
basic method calls/messages sent to the associated collection, so
user.roles for example. In HasAndBelongsToManyAssociation (which is
by the way defined in
active_record/associations/has_and_belongs_to_many_association.rb)
there is a method_missing. Take a look at its definition to
understand.

Cheers,

def method_missing(method, *args, &block)
if @target.respond_to?(method) ||
(!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
super
else
@reflection.klass.with_scope(:find => { :conditions =>
@finder_sql, :joins => @join_sql, :readonly => false }) do
@reflection.klass.send(method, *args, &block)
end
end
end

I’m guessing it’s the:
@reflection.klass.with_scope(:find => {:conditions => @find_sql,…})

portion that is narrowing the scope of the find method in
user.roles.find(:all)??

Also, I was aware of the proxy class prior to posting, I’m just not far
enough along in my railing to dig through all that code to find where
the magic was happening, and for that I appologize.

Now with that said, did I just discover a safe way to implement future
collect type definitions? My primary concern was that the “magic” was
unintentional. As long as I know that this way is the best way to get
what I want then I’ll be happy.

Thanks guys,
Jim

Tim McIntyre wrote:

Not sure I’m seeing straight or completely understanding the question
but it seems to me that that will only work when the given user only has
a single role, otherwise you’ll have to iterate over each roll to get
the names?

No, this is about how the names method that the OP defined on Role is
getting magically called via the roles collection on User,
simultaneously passing in an extra condition. See Bosko’s contributions
to this thread.

regards

Justin

On 8/18/06, Jim F. [email protected] wrote:

Hi Jim
Cheers,
end
end
end

I’m guessing it’s the:
@reflection.klass.with_scope(:find => {:conditions => @find_sql,…})

portion that is narrowing the scope of the find method in
user.roles.find(:all)??

Not in that call, no. The above gets caught by the proxy class’ own
definition of find. It doesn’t hit the method_missing code.

Also, I was aware of the proxy class prior to posting, I’m just not far
enough along in my railing to dig through all that code to find where
the magic was happening, and for that I appologize.

Absolutely no need to appologize! Particularly not to me. :slight_smile:

Now with that said, did I just discover a safe way to implement future
collect type definitions? My primary concern was that the “magic” was
unintentional. As long as I know that this way is the best way to get
what I want then I’ll be happy.

Thanks guys,
Jim

You can in fact implement things that way, but I don’t recommend it.
I am not convinced that it was intentionally designed to be used for
YOUR class methods, or if you should rely on this behavior for a long
time (you probably could, but I don’t). For the sake of clarity, I
tend to prefer just calling ClassName.my_class_method(my_parameters)
from my own code, because then the scope of all the finds inside the
class method is obvious just by looking at the class method call. But
certainly, in the example you cited, the find(:all) inside your
“names” class method gets a narrower scope tossed to it by the
method_missing/proxy class code above.

Cheers,

Bosko M. wrote:

On 8/18/06, Jim F. [email protected] wrote:

Hi Jim
Cheers,
end
end
end

I’m guessing it’s the:
@reflection.klass.with_scope(:find => {:conditions => @find_sql,…})

portion that is narrowing the scope of the find method in
user.roles.find(:all)??

Not in that call, no. The above gets caught by the proxy class’ own
definition of find. It doesn’t hit the method_missing code.

Also, I was aware of the proxy class prior to posting, I’m just not far
enough along in my railing to dig through all that code to find where
the magic was happening, and for that I appologize.

Absolutely no need to appologize! Particularly not to me. :slight_smile:

Now with that said, did I just discover a safe way to implement future
collect type definitions? My primary concern was that the “magic” was
unintentional. As long as I know that this way is the best way to get
what I want then I’ll be happy.

Thanks guys,
Jim

You can in fact implement things that way, but I don’t recommend it.
I am not convinced that it was intentionally designed to be used for
YOUR class methods, or if you should rely on this behavior for a long
time (you probably could, but I don’t). For the sake of clarity, I
tend to prefer just calling ClassName.my_class_method(my_parameters)
from my own code, because then the scope of all the finds inside the
class method is obvious just by looking at the class method call. But
certainly, in the example you cited, the find(:all) inside your
“names” class method gets a narrower scope tossed to it by the
method_missing/proxy class code above.

Cheers,

Thank,

I’m going to put this down as “undiscovered/unintended feature” and
steer clear until someone at the top confirms that it will stick around
permanently.

Thanks everyone,
Jim