Dynamic class creation at runtime

I have a scenario where I need to dynamically create classes from existing application (external to app) databases, to pull data for reports.

# Table definitions APP_TABLES = %w(#list of tables for a given app - I'm sure I could query this on a simple connect. But let's stick to this for now.)

APP_TABLES.each do |table_name|   # camelize for class creation   class_name = table_name.camelize

  klass = Class.new(App) # uses App class to establish_connection to externalDB

klass = class_eval do       #unsure about this. I need to set table_name to same as class name       ActiveRecord::Base.set_table_name("#{class_name}")       ActiveRecord::Base.const_set(constant_name, klass) end # class_eval

# After creating the class - I want to establish an instance - but it says that the class does not exist - more specifically constant doesn't exist. eval("#{table_name} = #{class_name}.new")

I would love to hear thoughts on this, especially if I'm approaching this incorrectly.

klass = Class.new(App) # uses App class to establish_connection to externalDB

klass = class_eval do #unsure about this. I need to set table_name to same as class name ActiveRecord::Base.set_table_name("#{class_name}") ActiveRecord::Base.const_set(constant_name, klass) end # class_eval

This sounds fishy, you're calling set_table_name on AR::Base, not your class. just klass.set_table_name 'blah' should be enough

# After creating the class - I want to establish an instance - but it says that the class does not exist - more specifically constant doesn't exist. eval("#{table_name} = #{class_name}.new")

I would love to hear thoughts on this, especially if I'm approaching this incorrectly.

You haven't created a constant called class_name - ActiveRecord::Base.const_set(constant_name, klass) would have created ActiveRecord::Base::Foo. You probably wanted Object.const_set class_name, klass

Fred

Thanks for the reply.

Frederick Cheung wrote in post #975422:

klass = class_eval do    #unsure about this. I need to set table_name to same as class name    ActiveRecord::Base.set_table_name("#{class_name}")    ActiveRecord::Base.const_set(constant_name, klass) end # class_eval

This sounds fishy, you're calling set_table_name on AR::Base, not your class. just klass.set_table_name 'blah' should be enough

True. I receive an unknown method error though.

eval("#{table_name} = #{class_name}.new")

You haven't created a constant called class_name - You probably wanted Object.const_set class_name, klass

Sorry - I seem to have missed posting the   class_name = table_name.camelize line in there (have large sections of comments that I didn't want to clutter the post).

I've changed the above code to:

APP_TABLES.each do |table_name|   class_name = table_name.camelize     klass = Class.new(App)     constant_name = "#{class_name}"

    klass = class_eval do       set_table_name("#{class_name}") or puts "Class set table name failed"       Object.const_set(constant_name, klass) or puts "Class name constant set failed"     end # class_eval

but get an unknown method error on the set_table_name call.

The App class (below) inherits from ActiveRecord::Base which in turn should have that method.

class App < ActiveRecord::Base   abstract_class = true

  def initialize     establish_connection :app_development   end

  # to avoid trying to connect to the Spydirdb table by default   def self.columns() @columns ||= ; end end

klass = class\_eval do
  set\_table\_name\(&quot;\#\{class\_name\}&quot;\) or puts &quot;Class set table name

failed" Object.const_set(constant_name, klass) or puts "Class name constant set failed" end # class_eval

but get an unknown method error on the set_table_name call.

you don't need the class_eval . just klass.set_table_name should work (I don't think the method is protected, if it is you might have to use send)

The App class (below) inherits from ActiveRecord::Base which in turn should have that method.

class App < ActiveRecord::Base abstract_class = true

this should be self.abstract_class = true (right now you're just setting a local variable)

def initialize establish_connection :app_development end

This is dodgy. First of all you're overriding initialize, but without calling through to the super class, so your activerecord objects won't get initialized properly. You've also change the signature of initialize. Also, do you really want to be reconnecting to the database everytime a new instance is created? lastly this establish_connection is a class method

Frederick Cheung wrote in post #975445:

class App < ActiveRecord::Base abstract_class = true

this should be self.abstract_class = true (right now you're just setting a local variable)

Point taken.

def initialize   establish_connection :app_development end

This is dodgy. First of all you're overriding initialize, but without calling through to the super class, so your activerecord objects won't get initialized properly.

Taken from online suggestions for handling multiple DBs.

You've also change the signature of initialize. Also, do you really want to be reconnecting to the database everytime a new instance is created? lastly this establish_connection is a class method

Thanks. I'll fix all that and get back to you.

# APP_TABLES contain a list of table names - would be better to dynamically read them after connecting

APP_TABLES.each do |table_name|   class_name = table_name.camelize   klass = Class.new(App)   constant_name = "#{class_name}"

  # even though klass is created with App as superclass (inheriting from ActiveRecord::Base, the set_table_name seems to be unavailable - what am I missing here?   klass.set_table_name ("#{class_name}") or puts "Class set table name failed"   klass.const_set(constant_name, klass) or puts "Class name constant set failed"   eval("#{table_name} = #{class_name}.new") or puts "Class instantiation failed" end

How can I determine if the klass has actually been created? How long will the dynamic classes exist in memory? For the duration of this rake task execution? Is there a way to list classes created at runtime?

# APP_TABLES contain a list of table names - would be better to dynamically read them after connecting

APP_TABLES.each do |table_name| class_name = table_name.camelize klass = Class.new(App) constant_name = "#{class_name}"

# even though klass is created with App as superclass (inheriting from ActiveRecord::Base, the set_table_name seems to be unavailable - what am I missing here?

what happens when you try?

klass.set_table_name ("#{class_name}") or puts "Class set table name failed" klass.const_set(constant_name, klass) or puts "Class name constant set failed" eval("#{table_name} = #{class_name}.new") or puts "Class instantiation failed" end

How can I determine if the klass has actually been created?

I don't think Class.new can actually fail

How long will the dynamic classes exist in memory?

For the duration of this rake task execution? Is there a way to list classes created at runtime?

I think that klass exists much like any variable - it may be garbage collected if nothing references it (classes might be special though) . You can step through objects of any class (including Class) via objectspace, although you probably won't be able to distinguish classes created like this from other classes.

Fred

Thanks for your repeated answers.

Frederick Cheung wrote in post #975619:

what happens when you try?

Class set table name failed

Thanks for your repeated answers.

Frederick Cheung wrote in post #975619:

> what happens when you try?

Class set table name failed

That's just what you're printing out - the return value of set_table_name isn't documented, so i wouldn't assume nil or false to mean failure

Fred

Frederick Cheung wrote in post #975672:

That's just what you're printing out - the return value of set_table_name isn't documented, so i wouldn't assume nil or false to mean failure

OK, so I'm down to:

class ExternalApp < ActiveRecord::Base   self.abstract_class = true   establish_connection :external_app   def self.columns() @columns ||= ; end end

#App tables has a list of tables in external app, until I get this bit working APP_TABLES.each do |table_name|   constant_name = class_name = table_name.camelize   klass = Class.new(ExternalApp)   klass.set_table_name ("#{class_name}")   klass.const_set(constant_name, klass)   eval("#{table_name} = #{class_name}.new") or puts "Class instantiation failed" end

On running this rake task I get the following error:

lib/rspec/core/backward_compatibility.rb:20:in `const_missing': uninitialized constant Services

Is const_set where the klass name is 'set in stone'?

#App tables has a list of tables in external app, until I get this bit working APP_TABLES.each do |table_name| constant_name = class_name = table_name.camelize klass = Class.new(ExternalApp) klass.set_table_name ("#{class_name}") klass.const_set(constant_name, klass) eval("#{table_name} = #{class_name}.new") or puts "Class instantiation failed" end

On running this rake task I get the following error:

lib/rspec/core/backward_compatibility.rb:20:in `const_missing': uninitialized constant Services

Is const_set where the klass name is 'set in stone'?

It is, but you're using it back to front: klass.const_set sets a constant on klass, whereas you want to set a top level constant ie Object.const_set

Fred.

Frederick Cheung wrote in post #975964:

It is, but you're using it back to front: klass.const_set sets a constant on klass, whereas you want to set a top level constant ie Object.const_set

That makes sense, thought it might be a scope problem.

Thank you.

Ok, so this seems to work

1) Create dynamic classes from a list (some rework on getting that from tables automatically needed) 2) Instantiate those objects

A minor error pops up now.

The tables exist, as do the fields.

However, with:

service = Services.new(:portID => 333, :method => "PUT")

But I get the error:

unknown attribute: portID

Frederick Cheung wrote in post #975964:

It is, but you're using it back to front: klass.const_set sets a constant on klass, whereas you want to set a top level constant ie Object.const_set

You are my personal hero, today. Thank you so much for helping out on this thread. It works.

The tables exist, as do the fields.

However, with:

service = Services.new(:portID => 333, :method => "PUT")

But I get the error:

unknown attribute: portID

Probably the result of you suppressing the columns method. If AR things there are no columns, it will think there are no attributes

Fred

Frederick Cheung wrote in post #975987:

Probably the result of you suppressing the columns method. If AR things there are no columns, it will think there are no attributes

From the superclass. Right. Thanks.

Everything's perfect now. Thanks again for the repeated replies.