[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.
Trackbacks
Use this link to trackback from your own site.





Wasn’t it BTN — Behind The News?
Damn it. Yes it was.
Ahh the memories.
Great post, Not too long, but still just enough to make people want to look BTS.
Dr. Nic,
Nice writeup. Looking forward to more articles like this one. Thanks!
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.Being still a bit new to Ruby and Rails, I find the article useful but would have found a slightly more detailed explanation helpful.
[...] 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. [...]
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 }
@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!
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.
[...] It was this old Dr. Nic post that got me over the hurdle solving this one. Thanks Dr. Nic. [...]
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