konyak
konyak

Reputation: 11706

Rails ability to call my class method on an Array

Considering I have the following classes and some records in the DB for each one:

class User < ActiveRecord::Base
  has_many :user_roles
  has_many :roles, :through => :user_roles
end

class Role < ActiveRecord::Base
  has_many :user_roles
  has_many :users, :through => :user_roles

  def self.method1
    "boo!"
  end
end

Why does User.first.roles.method1 work successfully? I thought User.first.roles returns an Array, whose class doesn't have method1 defined.

> User.first.roles.method1
=> "boo!"

Of course, Role.all.method1 throws an undefined method error as I expected.

My stack: Ruby 1.9.3p484, Rails 3.2.13

Upvotes: 1

Views: 1574

Answers (4)

Max Williams
Max Williams

Reputation: 32933

Ok - i understand what you're asking now - i think a lot of people including me have been confused by the question, sorry. I think the confusion came about because you reworded the question, and because "How am i able to do this" is a very vague and broad reaching question.

I think that what you're asking, specifically, could be rephrased as "If I call a Role class-level method ("method1") on an array of Role objects, returned from User.first.roles then it returns the same result as calling Role.method1. Shouldn't it fail, because Array doesn't have that method?"

The answer is that when rails returns the results of an association, it mixes in a module (or more likely a bunch of modules) which add extra methods. One of those methods is method_missing, which tries to call the method on the class which has been collected. So, it's still an Array, but it's an array with extra methods.

If you want to see where it happens, put raise "BANG!" in your method1 method, then call your code again: in the stack trace you'll be able to see where the magic happens.

Upvotes: 2

Oleksandr Slynko
Oleksandr Slynko

Reputation: 787

Actually it is not an Array. it is ActiveRecord::Associations::CollectionProxy You can check definition here activerecord/lib/active_record/associations/collection_proxy.rb

It has changed method_missing which sends all requests to parent class.

Upvotes: 3

Max Williams
Max Williams

Reputation: 32933

In Ruby, you can call the same method (or, more generally, run the same block of code) on an array of objects with the "map" method (aka "collect" - i prefer "collect" myself because in Java a "map" is what we call a "hash" in ruby).

eg

User.first.roles.map{|role| role.method1}
=> #an array of the values of method1

I'm not sure if this is what you want to do though, your question isn't very clear.

EDIT: it just occurred to me that you want to call method1 on a single role, instead of the array. In that case, you could say

User.first.roles.first.method1

assuming that you actually want the first role object.

Upvotes: 0

LHH
LHH

Reputation: 3323

User.first.roles will return you array, so you can not call your method on an array

You can call method like this

User.first.roles.collect{|role| role.method1}

Upvotes: 1

Related Questions