views:

500

answers:

4

Assuming I have 5 tables. Can ActiveRecord handle this? How would you set it up?

The hierarchy:

Account (Abstract)
  CorporateCustomer (Abstract)
    PrivateCustomer
    PublicCustomer
  GovernmentCustomer

Edit: In nhibernate and castle activerecord the method needed to enable this scenario is called "joined-subclasses".

A: 

Search for ActiveRecord Single Table Inheritance feature. Unfortunately I can't find a more detailed reference online to link to. The most detailed explanation I read was from "The Rails Way" book.

Simone Carletti
I have 5 tables so this doesn't look like its what I'm looking for. I think this method expects a single table with a type discriminator column. Do you know if ActiveRecords supports joined-subclasses? I think more along the lines of what I'm looking for.
TheDeeno
A: 

This is one way (the simplest) of doing it:

class Account < ActiveRecord::Base
   self.abstract_class = true
   has_many :corporate_customers
   has_many :government_customers
end


class CorporateCustomer < ActiveRecord::Base
   self.abstract_class = true
   belongs_to :account
   has_many :private_customers
   has_many :public_customers
end


class PrivateCustomer < ActiveRecord::Base
   belongs_to :corporate_customer
end

class PublicCustomer < ActiveRecord::Base
   belongs_to :corporate_customer
end

class GovernmentCustomer < ActiveRecord::Base
   belongs_to :account
end

NOTE: Abstract models are the models which cannot have objects ( cannot be instantiated ) and hence they don’t have associated table as well. If you want to have tables, then I fail to understand why it needs to an abstract class.

Swanand
I commented on my question. Does it make sense now? I don't plan on instantiating Account or CorporateCustomer - only the bottom 3 entities. But I think I need the 5 tables to correctly model this situation. No?
TheDeeno
A: 

Assuming most of the data is shared, you only need one table: accounts. This will just work, assuming accounts has a string type column.

class Account < ActiveRecord::Base
  self.abstract_class = true
end

class CorporateCustomer < Account
  self.abstract_class = true
  has_many financial_statements
end

class PrivateCustomer < CorporateCustomer
end

class PublicCustomer < CorporateCustomer
end

class GovernmentCustomer < Account
end

Google for Rails STI, and in particular Rails STI abstract, to get some more useful info.

Sarah Mei
Thanks for the response. If you look at the question comments and the comments @Swanand's answer I think you'll see the problem with the single table approach in my situation. I'm curious if ActiveRecord can handle this multitable inheritance (nhibernate and castle activerecord can) or if I'm sol if I choose Ruby.
TheDeeno
You can do it with one table. I see nothing in your question, nor in the comments, nor in the answers, nor in the answer comments, that suggests otherwise. What specifically do you think the above does not cover?
Sarah Mei
You can do it with five tables, as well, but you only NEED one.
Sarah Mei
I'm pretty sure STI will work it puts no restrictions on joining to other tables in fact each (non-abstract) class can have additional joins on top of those in the abstract classes.
Kris
@Sarah With the single table approach would it be possible to, in the db not the app, create a financial statement record for the gov employee? I'm trying to set this up so the DB is handling data integrity, not the app.
TheDeeno
@Kris would the STI setup look just like Sarah's above?
TheDeeno
Sure, in the database you could insert a financial statement for a government customer, unless you set the appropriate constraints. The exact mechanism for those will vary depending on your DB. But why? Then you have the logic in two places (the code and the db), and you have to maintain it both places. Why not instead set up your application database user with appropriate privs (i.e., limited) so it can't do anything awful, and then *write good code, once* so it doesn't.
Sarah Mei
+2  A: 

You could try something along the following lines.

class Account < ActiveRecord::Base
  belongs_to :corp_or_gov_customer, :polymorphic => true

  def account_id
    self.id
  end
end

class GovernmentCustomer < ActiveRecord::Base
  has_one :account, :as => :corp_or_gov_customer, :dependent => :destroy

  def method_missing( symbol, *args )
    self.account.send( symbol, *args )
  end
end

class CorporateCustomer < ActiveRecord::Base
  has_one :account, :as => :corp_or_gov_customer, :dependent => :destroy
  belongs_to :priv_or_pub_customer, :polymorphic => true

  def method_missing( symbol, *args )
    self.account.send( symbol, *args )
  end
end

class PrivateCustomer < ActiveRecord::Base
  has_one :corporate_customer, :as => :priv_or_pub_customer, :dependent => :destroy

  def method_missing( symbol, *args )
    self.corporate_customer.send( symbol, *args )
  end
end

class PublicCustomer < ActiveRecord::Base
  has_one :corporate_customer, :as => :priv_or_pub_customer, :dependent => :destroy

  def method_missing( symbol, *args )
    self.corporate_customer.send( symbol, *args )
  end
end

I've not tested this code (or even checked it for syntax). Rather it's intended just to point you in the direction of polymorphic relations.

Overriding method_missing to call nested objects saves writing code like

my_public_customer.corporate_customer.account.some_attribute

instead you can just write

my_public_customer.some_attribute


In response to the comment:

The problem is that concepts like "is a", "has many" and "belongs to" are all implemented by foreign key relationships in the relational model. The concept of inheritance is completely alien to RDB systems. The semantics of those relationships has to be mapped onto the relational model by your chosen ORM technology.

But Rails' ActiveRecord library doesn't implement "is_a" as a relationship between models.

There are several ways to model your class hierarchy in an RDB.

A single table for all accounts but with redundant attributes - this is supported by ActiveRecord simply by adding a "type" column to your table. and then creating your class hierarchy like this:

class Account < ActiveRecord::Base
class GovernmentCustomer < Account
class CorporateCustomer < Account
class PublicCustomer < CorporateCustomer
class PrivateCustomer < CorporateCustomer

Then if you call PrivateCustomer.new the type field will automatically be set to "PrivateCustomer" and when you call Account.find the returned objects will be of the correct class.

This is the approach I would recommend because it's by far the simplest way to do what you want.

One table for each concrete class - As far as I know there is no mapping provided for this in ActiveRecord. The main problem with this method is that to get a list of all accounts you have to join three tables. What is needed is some kind of master index, which leads to the next model.

One table for each class - You can think of tables that represent the abstract classes as a kind of uniform index, or catalogue of objects that are stored in the tables for the concrete classes. By thinking about it this way you are changing the is_a relationship to a has_a relationship e.g. the object has_a index_entry and the index_entry belongs_to the object. This can be mapped by ActiveRecord using polymorphic relationships.

There is a very good discussion of this problem in the book "Agile Web Development with Rails" (starting on page 341 in the 2nd edition)

Noel Walters
Can you explain this approach a little more? These are is-a relationships instead of has-a relationships. has_one/belongs_to models the has-a relationship so it doesn't seem like a good fit.
Sarah Mei
The problem is that relational databases in general are not a good fit for representing inheritance - but see the extended answer.
Noel Walters
I have the book and in the 3rd Edition it discusses joining multiple tables starting on page 373. This is the stuff that I've been looking for!!!
thaBadDawg