mahatmanich
mahatmanich

Reputation: 11023

How to dynamically set model's table_name?

I have a legacy db (oracle), in that db I have several tables that hold different data but are structurally the same. I am not allowed to change the DB schema in any way!

I wanted a DRY ActiveRecord Model to fetch the right data from the right tables. The problem was that I needed to dynamically overwrite self.table_name in order for it work.

Here is my code:

ActiveRecord:Base Class which will be inherited by all similar tables

class ListenLoc < ActiveRecord::Base
  @@table_name = nil

  def self.table_name
    @@table_name
  end    

  default_scope { where(validated: 1).where("URL_OK >= 0") }
  scope :random_order, -> { order('DBMS_RANDOM.VALUE') }
  scope :unvalidated, -> { unscope(:where).where(validated: 0) }


  def self.get_category(cat_id)
    where("cat_id = ?", cat_id)
  end    

  def self.rand_sample(cat_id, lim)
    where("cat_id = ?", cat_id).random_order.limit(lim)
  end    
end

Child Classes look as such:

A

class ListenLocA < ListenLoc
  @@table_name = 'LISTEN_ADDR_A'
  self.sequence_name = :autogenerated
  belongs_to :category, class_name: 'ListenLocCatA', foreign_key: 'cat_id'
  belongs_to :country, class_name: 'Country', foreign_key: 'country_id'
end

B.

class ListenLocB < ListenLoc
  @@table_name = 'LISTEN_ADDR_B'
  self.sequence_name = :autogenerated
  belongs_to :category, class_name: 'ListenLocCatB', foreign_key: 'cat_id'
  belongs_to :country, class_name: 'Country', foreign_key: 'country_id'
end

The above works, however I have already noticed that there are some pitfalls when doing specific select lookups.

Is this a good approach? Is there a better way to pass the self.table_name dynamically?

Update:

One would think that this should work, but I get an error that the table does not exist since ActiveRecord tries to validate the table before creating an Object, and self.table_name is not set on the ListenLoc Model dynamically.

class ListenLocB < ListenLoc
  self.table_name = 'LISTEN_ADDR_B'
  self.sequence_name = :autogenerated
  belongs_to :category, class_name: 'ListenLocCatB', foreign_key: 'cat_id'
  belongs_to :country, class_name: 'Country', foreign_key: 'country_id'
end

Upvotes: 1

Views: 2740

Answers (2)

mahatmanich
mahatmanich

Reputation: 11023

What I realized is, that I could just use superclass without using globals, which I ended up using. This does not pose a race condition issue as with globals.

class ListenLocB < ListenLoc
  superclass.table_name = 'LISTEN_ADDR_B' # or ListenLoc.table_name
  self.sequence_name = :autogenerated
  belongs_to :category, class_name: 'ListenLocCatB', foreign_key: 'cat_id'
  belongs_to :country, class_name: 'Country', foreign_key: 'country_id'
end

Upvotes: 2

Andrey Deineko
Andrey Deineko

Reputation: 52357

In Ruby class variables are shared across whole hierarchy, so your approach will not work. General idea behind class variables - don't use it unless you are 100% sure you know what you are doing. And even when you are - there is most likely better approach.

As to actual issue - what you've done with table_name is not DRYing up, since you added more lines than you saved. Moreover, it makes it more difficult to read.

Just put

self.table_name =

where it should be in every model - it would be concise and readable.

Another option is to use localized constants instead which are bound to the ListenLoc class:

class ListenLoc
  def self.table_name
    TABLE_NAME
  end
end

class ListenLocB < ListenLoc
  ::TABLE_NAME = 'LISTEN_ADDR_B'
end

Why this works?

My understanding is the following:

By writing ::TABLE_NAME you define the constant TABLE_NAME in the global scope.

When your call propagates to ListenDoc class, it tries to resolve the constant ListenDoc::TABLE_NAME, and it does not find it it's scope. Then it looks if the constant TABLE_NAME is defined in the outer scope, and it finds that ::TABLE_NAME is indeed defined, and it's value is 'LISTEN_ADDR_B'. Thus, works.

I might have been unclear, since my understanding on the topic still floats, but it is definitely related to how Ruby searches for constants.

It is not very straight forward, since few caveats exist (as with all, after all).

Upvotes: 1

Related Questions