15 Jul 2008

Using Ruby’s Eval method to dynamically declare Active Record classes for a database schema

Active Record is useful outside of Rails as a general database access layer. This post shows how to dynamically create ActiveRecord classes for a given database schema using the eval method.

When faced with a lot of repetitive code, meta-programming can save significant time and effort. Meta-programming involves writing code that dynamically creates or executes other code. This can be done via reflection or dynamically creating then evaluating strings into Ruby code. String evaluation has dangers but is very powerful.

I want to create a script to generate a simple report listing the number of rows in each table in a given MySql database. While I can do this easily by creating an ActiveRecord for each table, but it gets tedious when I have lots of tables:


require ‘active_record’
 
def connect_to database
ActiveRecord::Base.establish_connection(
:adapter=>"mysql", :database=>database,
:username=>"admin", :password =>"secret")
end
 
def rowcount klass
puts "#{klass.table_name}: #{klass.count} records"
end
 
connect_to "projects"
class User < ActiveRecord::Base
end
class Project < ActiveRecord::Base
end
#this is going to get boring quickly...
rowcount User
rowcount Project
#very quickly..
 


For each table in the database, we need to create an active record and pass it to the row method. With the eval method we can create those ActiveRecord classes dynamically, which allows us to write:




def declare_active_record_for entity_name
eval %"
class #{entity_name} < ActiveRecord::Base
end
";
end

 
connect_to "projects"
declare_active_record_for "User"
declare_active_record_for "Project"
rowcount User
rowcount Project
 


We can make an array of entity names we want to be declared as ActiveRecord classes. Additionally, we can use the eval method to handle the call to rowcount, this way we reference our active records only from the list of entity names.


connect_to "projects"

active_records = ["User", "Project"]
active_records.each {|i| declare_active_record_for i}
active_records.each {|i| eval "rowcount " + i}

 


From there we can extend our script to find all the tables in a schema by creating an ActiveRecord for MySql’s table meta-data table. Giving us the final script:


require 'active_record'
 
def connect_to database
ActiveRecord::Base.establish_connection(
:adapter=>"mysql", :database=>database,
:username=>"admin", :password =>"secret")
end
 
def rowcount klass
puts "#{klass.table_name}: #{klass.count} records"
end
 
def declare_active_record_for entity_name
eval %"
class #{entity_name} < ActiveRecord::Base
end
";
end

def as_entity_name table_name
table_name.singularize.capitalize
end

 
def report_rowcounts_for database_name

connect_to "information_schema"
declare_active_record_for "Table"
table_names = Array.new
Table.find(:all, :conditions =>{
:table_schema=>database_name}).each {
|i| table_names.push i.TABLE_NAME}

connect_to database_name
table_names.each {
|table| declare_active_record_for as_entity_name(table)}
table_names.each {
|table| eval "rowcount #{as_entity_name(table)}"}
end
 
report_rowcounts_for "projects"