Reputation: 11023
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
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
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