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.