Forum: Ruby on Rails dynamic table names for AR models

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Pete H. (Guest)
on 2009-05-13 05:10
Hi All,

I have an interesting Active Record problem and I'm not quite sure what
the cleanest solution is. The legacy database that I am integrating with
has a strange wrinkle in its schema where one logical table has been
'partitioned' into several physical tables. Each table has the same
structure, but contains data about different items.

I'm not great at explaining this clearly (as you can tell!). Let me try
and explain with a concrete example. Let's say we have a Car, which has
one or more Wheels. Normally we'd represent that with a Car table and a
Wheels table like so:


CREATE TABLE cars (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255),
  'etc
)


CREATE TABLE wheels (
  `id` int(11) NOT NULL auto_increment,
  `car_id` int(11) NOT NULL,
  `color` varchar(255),
  'etc
)

So far, so good. But with the 'partioning' strategy that is in my legacy
database it would look more like:

CREATE TABLE cars (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255),
  'etc
)

CREATE TABLE car_to_wheel_table_map (
  `car_id` int(11) NOT NULL,
  `wheel_table` varchar(255)
)

CREATE TABLE wheels_for_fords (
  `id` int(11) NOT NULL auto_increment,
  `car_id` int(11) NOT NULL,
  `color` varchar(255),
  'etc
)

CREATE TABLE wheels_for_buicks (
  `id` int(11) NOT NULL auto_increment,
  `car_id` int(11) NOT NULL,
  `color` varchar(255),
  'etc
)

CREATE TABLE wheels_for_toyotas (
  `id` int(11) NOT NULL auto_increment,
  `car_id` int(11) NOT NULL,
  `color` varchar(255),
  'etc
)

So here we have a set of wheels_for_x tables, and a
car_to_wheel_table_map table which contains a mapping from car_id to the
specific wheels_for_x which contains the wheels for a specific car. If I
want to find the set of wheels for a car I first have to find out which
wheels table to use via the car_to_wheel_table_map table, and then look
up records in the wheel table specified in the car_to_wheel_table_map.

Firstly, can someone enlighten me as to if there is a standard name for
this technique?

Secondly, does anyone have any pointers on how I can make this work in
Active Record in a nice clean way. The way I see it I can either have a
Wheel model where the table name can be defined per instance, or I can
dynamically create Model classes at runtime with the correct table name
as specified in the mapping table.

Any advice gratefully received!

Cheers,
Pete
Gabriel S. (Guest)
on 2009-05-13 05:29
> Secondly, does anyone have any pointers on how I can make this work in
> Active Record in a nice clean way.

In a domain i'm workin in, this type of thing also came up, and I chose
to split it in a somewhat similar way using abstract classes. Create an
abstract class, Wheels, put all common code in there.  I chose to
include the equivalent of that mapping table as a constant, so in your
example a "CarToWheelsClassHash" Constant, and then create sub-classes
for each of the wheels table.  Override the ActiveRecord Methods for the
Wheel class, and then re-override them in the subclasses to bring them
back.

Wheel < ActiveRecord::Base
  self.abstract_class = true

  CarToWheelsClassHash = {:ford => "FordWheel"}

  def self.abstract_find(car_brand, *normalfindargs)
    CarToWheelsClassHash[car_brand].constantize.find(*normalfindargs)
  end

  [other code common to all wheels models]
end

FordWheel < Wheel
  code specific to the ford wheels model
end

hope that makes sense to you, i actually really like this solution so
far on my end.
Pete H. (Guest)
on 2009-05-13 06:11
Gabriel S. wrote:
>
>
> Wheel < ActiveRecord::Base
>   self.abstract_class = true
>
>   CarToWheelsClassHash = {:ford => "FordWheel"}
>
>   def self.abstract_find(car_brand, *normalfindargs)
>     CarToWheelsClassHash[car_brand].constantize.find(*normalfindargs)
>   end
>
>   [other code common to all wheels models]
> end
>
> FordWheel < Wheel
>   code specific to the ford wheels model
> end
>
> hope that makes sense to you, i actually really like this solution so
> far on my end.

Thanks Gabriel, but unfortunately this exact solution wouldn't work in
our case. The names of the various Wheel tables are not known at design
time. In other words, there's nothing to stop someone creating a new
table and adding it to the mapping table during runtime.

Given the wonderful dynamic nature of ruby I suppose I could get past
that fairly easily by creating the Wheel subclasses dynamically, but I'm
not sure that that is the best approach.
Gabriel S. (Guest)
on 2009-05-13 06:56
> The names of the various Wheel tables are not known at design
> time.

well..if this is the case, i don't see how you're really going to keep
from some form of dynamic creation/metaprogramming/code generation and
also keep the design clean...

maybe the option with the least dynamic change that I can think of is to
keep a similar setup: one AbstractWheel class, and one ConcreteWheel
class, but then have a method in the abstract class dynamically
overwrite the table name of the ConcreteWheel Class based upon the fetch
from the mapping table...of course, this means you have to reload the
ConcreteWheel class every time....

of course, this raises the question for me, will all wheel tables have
the exact same attributes and methods?  If not, then this method won't
work...

As far as whether this has a standard name, what you're trying to do
sounds like a problem that is solved by one of either a factory or a
proxy, i'm not sure which...

-Gabe
Pete H. (Guest)
on 2009-05-13 08:26
Gabriel S. wrote:
>> The names of the various Wheel tables are not known at design
>> time.
>
> well..if this is the case, i don't see how you're really going to keep
> from some form of dynamic creation/metaprogramming/code generation and
> also keep the design clean...
>

I guess the best I can come up with right now is to add a
fetch_wheel_model method somewhere that looks something like:

def get_wheel_class(table_name)
  @cached_wheel_classes ||= {}
  unless @cached_wheel_classes.has_key? table_name
    @cached_wheel_classes[table_name] = Class.new(AbstractWheel) do
      set_table_name table_name
    end
  end

  @cached_wheel_classes[table_name]
end

but for some reason that doesn't quite feel like a good solution.
Gabriel S. (Guest)
on 2009-05-13 20:09
> but for some reason that doesn't quite feel like a good solution.

that, overall, makes sense, and is very little code..combine it with a
before callback or method_missing?

what about it seems like a poor solution?

I come back to asking, will all wheel classes have the same attributes
and methods?
Marnen L. (Guest)
on 2009-05-13 22:31
Pete H. wrote:
> Hi All,
>
> I have an interesting Active Record problem and I'm not quite sure what
> the cleanest solution is. The legacy database that I am integrating with
> has a strange wrinkle in its schema where one logical table has been
> 'partitioned' into several physical tables. Each table has the same
> structure, but contains data about different items.
[...]

Why not create a "wheels" view which combines the data from all the
"wheels_for_*" tables, then tie your Wheel model to that?

Best,
--
Marnen Laibow-Koser
http://www.marnen.org
removed_email_address@domain.invalid
Pete H. (Guest)
on 2009-05-13 22:44
Gabriel S. wrote:
>> but for some reason that doesn't quite feel like a good solution.
>
> that, overall, makes sense, and is very little code..combine it with a
> before callback or method_missing?
>
> what about it seems like a poor solution?
>
> I come back to asking, will all wheel classes have the same attributes
> and methods?

Sorry, I neglected to answer your question. Yes, all the wheel models
should have the same attributes and methods. So there would be an
AbstractWheel class something like this which each dynamically created
model would inherit from:

class AbstractWheel < ActiveRecord:Base
  abstract_class = true

  # common functionality here
end
Pete H. (Guest)
on 2009-05-13 22:46
Marnen Laibow-Koser wrote:

>
> Why not create a "wheels" view which combines the data from all the
> "wheels_for_*" tables, then tie your Wheel model to that?
>
> Best,
> --
> Marnen Laibow-Koser
> http://www.marnen.org
> removed_email_address@domain.invalid

I think that would have some performance issues. If there were 20
tables, but only 1 of them contained wheels for the car I was working on
then all 20 tables would have to be checked during a join, no?
E. Litwin (Guest)
on 2009-05-14 00:53
(Received via mailing list)
Also, that view would not be updatable (updateable?)
Not sure if that is a requirement but something to be aware of.

On May 13, 11:46 am, Pete H. <removed_email_address@domain.invalid>
This topic is locked and can not be replied to.