Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Rails method call like "has_one" work?

I am PHP dev and at the moment I am learning Rails (3) and of course - Ruby. I don't want to believe in magic and so I try to understand as much as I can about things that happen "behind" Rails. What I found interesting are the method calls like has_one or belongs_to in ActiveRecord models.

I tried to reproduce that, and came with naive example:

# has_one_test_1.rb
module Foo
  class Base
    def self.has_one
      puts 'Will it work?'
    end
  end
end

class Model2 < Foo::Base
  has_one
end

Just running this file will output "Will it work?", as I expected.

While searching through rails source I found responsible function: def has_one(association_id, options = {}).

How could this be, because it is obviously an instance (?) and not a class method, it should not work.

After some researching I found an example that could be an answer:

# has_one_test_2.rb
module Foo
  module Bar
    module Baz
      def has_one stuff
        puts "I CAN HAS #{stuff}?"
      end
    end
    def self.included mod
      mod.extend(Baz)
    end
  end
  class Base
    include Bar
  end
end

class Model < Foo::Base
  has_one 'CHEEZBURGER'
end

Now running has_one_test_2.rb file will output I CAN HAS CHEEZBURGER. If I understood this well - first thing that happens is that Base class tries to include Bar module. On the time of this inclusion the self.included method is invoked, which extends Bar module with Baz module (and its instance has_one method). So in the essence has_one method is included (mixed?) into Base class. But still, I don't fully get it. Object#extend adds the method from module but still, I am not sure how to reproduce this behaviour using extend. So my questions are:

  1. What exactly happened here. I mean, still don't know how has_one method become class method? Which part exactly caused it?

  2. This possibility to make this method calls (which looks like configuration) is really cool. Is there an alternative or simpler way to achieve this?

like image 460
Ernest Avatar asked Nov 22 '10 22:11

Ernest


2 Answers

You can extend and include a module.

extend adds the methods from the module as class methods A simpler implementation of your example:

module Bar
  def has_one stuff
    puts "I CAN HAS #{stuff}?"
  end
end

class Model
  extend Bar
  has_one 'CHEEZBURGER'
end

include adds the methods from the module as instance methods

class Model
  include Bar
end
Model.new.has_one 'CHEEZBURGER'

Rails uses this to dynamically add methods to your class.

For example you could use define_method:

module Bar
  def has_one stuff
    define_method(stuff) do
      puts "I CAN HAS #{stuff}?"
    end
  end
end

class Model
  extend Bar
  has_one 'CHEEZBURGER'
end

Model.new.CHEEZBURGER # => I CAN HAS CHEEZBURGER?
like image 192
Petrik de Heus Avatar answered Oct 21 '22 04:10

Petrik de Heus


I commend you for refusing to believe in the magic. I highly recommend you get the Metaprogramming Ruby book. I just recently got it and it was triggering epiphanies left and right in mah brainz. It goes over many of these things that people commonly refer to as 'magic'. Once it covers them all, it goes over Active Record as an example to show you that you now understand the topics. Best of all, the book reads very easily: it's very digestible and short.

like image 35
Jorge Israel Peña Avatar answered Oct 21 '22 03:10

Jorge Israel Peña