Dr Nic

[BTS] Magic Models – Class creation

[BTS] = Behind the Scenes; also a news-like TV show we used to watch as school kids (in Australia) that explored world news events and then our teachers would make us write reports about it. (UPDATE: this is a lie; it was BTN – Behind the News) Anyway….

Class creation – the magical way

As an educational exercise, it is definitely interesting to look at how the Magic Model’s Invisible Classes [1] are generated. Inside the gem [2], please turn to the file /lib/module.rb I’ve broken it up below too.

class Module
  alias :normal_const_missing :const_missing

  def const_missing(class_id)
    begin; return normal_const_missing(class_id); rescue; end

If you open the Irb/Console and type Foo, you’ll get an “unitialized constant Foo” error. This is raised by the const_missing method in the Module class. Yes, there’s a core class called Module. Type Module.methods.sort to see all the fun things it can do, or look up the API.

Like so many things in Ruby you can override this method, just like you might override the method_missing on a class (Rails does this on its ActiveRecord to support the find_by_... magical methods.)

When you load Magic Models (via the require 'dr_nic_magic_models' statement) it renames/aliases the current const_missing method and then overrides it.

The first thing it does it call the original const_missing to see if it returns anything useful. Why? Rails also overrides this method. Rails doesn’t preload all your model classes at start up. Instead when you first ask for a model class, it loads it via the const_missing method call. So Magic Models won’t do anything until Rails has had a chance to try an load the class from a app/models/foo.rb file, for example.

    unless table_name = DrNicMagicModels::Schema.models[class_id]
      raise NameError.new("uninitialized constant #{class_id}") if
                    DrNicMagicModels::Schema.models.enquired? class_id
    end

The DNMM::Schema class is in charge of mapping an unknown class name (class_id) to one of your database’s tables. To prevent nasty infinite recursion, we raise the “uninitialized constant” error if another const_missing request comes in for the same class_id.

    klass = Class.new ActiveRecord::Base
    const_set class_id, klass

The Class API takes a superclass as a parameter. Here, we want our Invisible Model to be an ActiveRecord.

By setting the anonymous class object to a constant value, e.g. Foo, it automatically names the class. E.g. klass.name == 'Foo'

    klass.set_table_name table_name

Finally, we force the new ActiveRecord class to use the table name that DNMM::Scheme found. You just can’t trust that the class will know what its table name is.

If you find yourself using this sort of code, then here’s some bonus code. When you create your class, you can install some code at the same time.

    klass_code = %q{def to_s; 'I''m magically generated'; end }
    klass_code = lambda {klass_code}
    klass = Class.new ActiveRecord::Base, &klass_code

Feedback

Was this easy to read? Too compact/too long? Let me know below.

If you go on and build something magically cool after reading this, definitely let me know.

Moral of the Story

You can live as a programmer without ever knowing any meta-programming features of Ruby. Heck, we all did it in Java/C++/C# and never knew we needed them. A lot of powerful API features exist in Rails that you’d never be able to build in many other languages; the ActiveRecord library is a tribute to meta-programming magic.

UPDATE: I had previously called Module a module, when its a class. Fixed.

[1] I just like calling them Invisible Models.

[2] Gems are installed inside your Ruby installation folder. Try something like ....\lib\ruby\gems\1.8\gems\ and you'll see all your favourite gems, like rails-1.1.5, active-record-1.14.4, etc.

Related posts:

  1. First look at rails 3.0.pre This article is out of date in some aspects....
  2. Cucumber: building a better World (object) How to write helper libraries for your Cucumber step definitions...
  3. TextMate bundles for Merb If you are using TextMate (OS X) or E Text...
  4. The explicit Ruby metaclass you know you always wanted When you define a “static” or “class” method on a...
  5. Find objects in IRB directly from browser URLs A long time ago, I tired of going into the...

12 Responses to “[BTS] Magic Models – Class creation”

  1. Daniel Baird says:

    Wasn’t it BTN — Behind The News?

  2. Dr Nic says:

    Damn it. Yes it was.

    Ahh the memories.

  3. Brett says:

    Great post, Not too long, but still just enough to make people want to look BTS.

  4. Hammed says:

    Dr. Nic,

    Nice writeup. Looking forward to more articles like this one. Thanks!

  5. andrew says:

    Just beutiful. I had been creating ‘Generic’ ActiveRecords like ActiveRecord::Base.find_by_sql("SELECT * FROM #{table}) in throwaway scripts. And then accessing the results with the ‘_before_type_cast’ magic method. But this is so elegant.

  6. Tony Martin says:

    Being still a bit new to Ruby and Rails, I find the article useful but would have found a slightly more detailed explanation helpful.

  7. [...] After reading a lot about Rails and it’s former inclusion (thanks to Tom and Ryan) of a bunch of REST functionality into the next release and a blog by Dr. Nic about his Magic Models project I’ve decided to embark on a new project. [...]

  8. Very interesting Ruby meta-programming tips!

    I think that there’s an error in proposed code:
    klass_code = %q{def to_s; “I’m magically generated’; end }

    The closing quotation mark need to be double instead of simple, like:
    klass_code = %q{def to_s; “I’m magically generated”; end }

  9. Dr Nic says:

    @sergio – thanks for that. Fixed to use lots of single quotes. WordPress is converting all double quotes to fancy-pants curly quotes, and these don’t copy+paste well!

  10. Adam says:

    The memories indeed. I think I might still have my BTN report books. I never used so much coloured pen to make those reports look purdy.

    Great post. Its emboldens me to see successful plugins demonstrate some deep class overiding. Something I might normally avoid thinking I should find another way.

    Sorry for the late comment, but when I saw BTN i had to.

  11. [...] It was this old Dr. Nic post that got me over the hurdle solving this one. Thanks Dr. Nic. [...]

  12. Anil says:

    I want to create an internal admin view to display the output of “show variables” from mysql. What’s the best approach do this?

    For example, I’m doing something like this:

    @variables = ActiveRecord::Base.find_by_sql “show variables;”

    Then, I get stuff like this back (227 elements in @variables) in script/console:

    >> @variables[0]
    => #”auto_increment_increment”, “Value”=>”1″}>

    However, I’m not sure how to get Variable_name and Value; do I access this as a Hash element? An attribute?

    Should I create a custom Model class such as this?
    class MySql