1

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
2
  • 1
    Personally I'd have different models for each table in your case. You can extract the similar code in ListenLoc into a concern Commented Jan 29, 2016 at 13:07
  • In case someone needs to play with something similar in the future, see this Q&A: stackoverflow.com/questions/52537951/… Commented Sep 27, 2018 at 15:43

2 Answers 2

2

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
Sign up to request clarification or add additional context in comments.

Comments

1

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).

16 Comments

how did I add more lines than I saved? The class listenLoc has only a small extraction of methods included that would otherwise reside in the other models.
@mahatmanich I only meant the table_name/class variable part, other is actually fine (even though I might have as well chosen concern over inheritance, as j-dexx suggests in comments, but that is up to you).
self.table_name = in the actual models does not work, that is the issue. It is not propagated to ListenLoc even though it should! So the solution above is what I opted for.
@mahatmanich why is it putting table_name in each model not working?
See update, I need to actually reproduce the error to show the actual error message. self.table_name is not passed to ListenLoc properly.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.