views:

204

answers:

1

I have a card-game application which makes use of Single Table Inheritance. I have a class Card, and a database table cards with column type, and a number of subclasses of Card (including class Foo < Card and class Bar < Card, for the sake of argument).

As it happens, Foo is a card from the original printing of the game, while Bar is a card from an expansion. In an attempt to rationalise my models, I have created a directory structure like so:

app/
+ models/
  + card.rb
  + base_game/
    + foo.rb
  + expansion/
    + bar.rb

And modified environment.rb to contain:

Rails::Initializer.run do |config|
  config.load_paths += Dir["#{RAILS_ROOT}/app/models/**"]
end

However, when my app reads a card from the database, Rails throws the following exception:

ActiveRecord::SubclassNotFound (The single-table inheritance mechanism failed to locate the subclass: 'Foo'. This error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this column if you didn't intend it to be used for storing the inheritance class or overwrite Card.inheritance_column to use another column for that information.)

Is it possible to make this work, or am I doomed to a flat directory structure?

+1  A: 

Probably the best way to do this would be to nest the Foo class inside a BaseGame module.

A ruby module is roughly similar to a package structure in other languages, it's a mechanism for partitioning related bits of code into logical groups. It has other functions such as mixins (which you can find explained here: http://www.rubyfleebie.com/an-introduction-to-modules-part-1/) but in this instance they're not relevant.

You'll need to reference and instantiate the class slightly differently. For example you'd query it like this:

BaseGame::Foo.find(:all,:conditons => :here)

Or create an instance of it like this:

BaseGame::Foo.new(:height => 1)

Rails supports modularized code for Active Record models. You just need to make a few changes to where the class is stored. say for example you move the class Foo into a module BaseGame (as in your example), you need to move apps/models/foo.rb to apps/models/base_game/foo.rb.So you're file tree will look like:

app/
 + models/
  + card.rb #The superclass
   + base_game/
      + foo.rb

To declare this on the class define like so:

module BaseGame
  class Foo < Card
  end
end
Ceilingfish
Afraid I'm still a bit new to Ruby. Could you post a bit more explanation (some stub code would be good) for what you mean by "nest the Foo class inside a Card module"? You don't seem to reference that in the rest of your answer.
Chris
I think I'm understanding you - do you actually mean "inside a BaseGame module" in your first line?If I do this, what does the type column in the database need to contain? "Foo" or "BaseGame::Foo"?
Chris
@Chris, yes you're quite right, corrected that. in the type column it'll get recorded as "BaseGame::Foo". But you shouldn't have to set this explicitly rails will take care of all of that for you.
Ceilingfish
Nope, I'm happy with Rails taking care of it for me, it's just because I do display the type at various points in the view. So I'll need to do some work to retrieve the last part of the class name.
Chris
You don't have to do much to get the last name of the class. class Card ... snip ... def pretty_name name.demodulize end end. demodulize is from the Rails library. http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html#M001062
Samuel
I'm now getting "Expected path_to_app/app/models/base_game/foo.rb to define Foo", with foo.rb containing "class BaseGame::Foo < Card ... end". Any ideas?
Chris
@Chris you may need to declare the class inside a nested module block. I've updated the example above to show you how to do that.
Ceilingfish
Actually, thinking about it, it's probably more likely I simply have existing data still in the DB.
Chris
Yep, that's the problem. This answer is fine.
Chris