Topic: to STI or no TI

Hi guys,

I've been wrestling with the best way to do this for a while now I figured it's time to call in some outside perspective wink

I have a Product model which has a polymorphic connection to product types, like this:

class Product < ActiveRecord::Base
  belongs_to :item, :polymorphic => :true
end
class Dvd < ActiveRecord::Base
  has_one :product, :as => :item
  belongs_to :dvd_group
  has_and_belongs_to_many :dvd_categories
end
class Book < ActiveRecord::Base
  has_one :product, :as => :item
  belongs_to :book_group
  has_and_belongs_to_many :book_categories
end

There are more but they're all set up the same way more or less. Now it works but the polymorphic connection feels like overkill.


I was thinking, it might be a lot simpler to use STI, something like this:

class Product < ActiveRecord::Base
end
class Dvd < Product
  has_and_belongs_to_many :dvd_categories
end
class Book < Product
  has_and_belongs_to_many :book_categories
end

My problem are the belongs_to associations. Since STI models don't have their own table I can't make them belong_to any thing, right?

I was thinking it may be possible to move the association to the Product model itself and create a Group model rather than seperate DvdGroup and BookGroup models but then if I do Group.find :all how would I know which product type a group belongs to?

Ideally I'd like to drop all the different categories and groups tables but I'm not sure how to go about it.

Re: to STI or no TI

marsvin wrote:

There are more but they're all set up the same way more or less. Now it works but the polymorphic connection feels like overkill.

Have you considered removing the "Product" model? In that case you would just use polymorphic association whenever something belongs to a generic product (such as a cart item).


marsvin wrote:

I was thinking, it might be a lot simpler to use STI, something like this:

I'm assuming the products will need different attributes (number of pages in a book, length of a DVD, etc.) correct? In that case STI is not the best solution.

marsvin wrote:

My problem are the belongs_to associations. Since STI models don't have their own table I can't make them belong_to any thing, right?

It depends. Since they share the same table they both need to have the same foreign key columns, however you can still define "belongs_to" in only one model subclass.

marsvin wrote:

I was thinking it may be possible to move the association to the Product model itself and create a Group model rather than seperate DvdGroup and BookGroup models but then if I do Group.find :all how would I know which product type a group belongs to?

In the case of groups, STI may be a valid consideration because all groups pretty much have the same columns/attributes, correct?

Railscasts - Free Ruby on Rails Screencasts

Re: to STI or no TI

ryanb wrote:

Have you considered removing the "Product" model? In that case you would just use polymorphic association whenever something belongs to a generic product (such as a cart item).

You mean just split up the products into completely independent models? It's a possibility. I quite like having a single view that can handle a list of books or dvds because they all have the same base model but that wouldn't be too hard to turn into a partial or something.

Wouldn't it go against the idea of normalization though to have one table with Books and one with Dvds while they both have a number of similar columns? They share at least these columns: article_number, ean_code, description, title, price, weight.

ryanb wrote:

I'm assuming the products will need different attributes (number of pages in a book, length of a DVD, etc.) correct? In that case STI is not the best solution.

You're right there are quite a number of "freak" columns that would need a new place to live wink

ryanb wrote:

It depends. Since they share the same table they both need to have the same foreign key columns, however you can still define "belongs_to" in only one model subclass.

Would that mean having a book_category_id and a dvd_category_id in the products table and just leaving them nil for records where they don't apply? I figured that was being wasteful.

ryanb wrote:

In the case of groups, STI may be a valid consideration because all groups pretty much have the same columns/attributes, correct?

A title (well three if you count the translations), an id and that's pretty much it. Can you put an STI model behind a has_and_belongs_to_many relationship? I have to say I never thought of that but it sounds like it may be the best solution.

Just off the top of my head here, is this what you mean?

class Book < ActiveRecord::Base
  has_and_belongs_to_many :dvd_categories
end
class Dvd < ActiveRecord::Base
  has_and_belongs_to_many :book_categories
end

class Category < ActiveRecord::Base
end
class DvdCategory < Category
  has_and_belongs_to_many :dvds
end
class BookCategory < Category
  has_and_belongs_to_many :books
end


That leads me to a related question, is there a "send" type method that will let you send Model names rather than method names? So is there something that would let me do this:
a = "Book"
send( "#{a}Category" ).find :all

I'm probably going over board here but it's worth a shot. wink

Re: to STI or no TI

marsvin wrote:

Wouldn't it go against the idea of normalization though to have one table with Books and one with Dvds while they both have a number of similar columns? They share at least these columns: article_number, ean_code, description, title, price, weight.

Yes, but it's a balancing act. Normalization can sometimes bring complexity, and in this case I think the duplication of a few column names outweighs the complexity a separate Product model will bring.

Just to give an extreme example. If you had "created_at" and "updated_at" columns in 5 tables it doesn't mean you should normalize this into a separate table. That would bring a huge amount of complexity with very little benefit. I realize your case is different, but just making a point that 100% normalization isn't always the best approach.

marsvin wrote:

Would that mean having a book_category_id and a dvd_category_id in the products table and just leaving them nil for records where they don't apply? I figured that was being wasteful.

Yes. This is why I don't think STI is the best solution for the products.

marsvin wrote:

Can you put an STI model behind a has_and_belongs_to_many relationship? I have to say I never thought of that but it sounds like it may be the best solution.

Just off the top of my head here, is this what you mean?

class Book < ActiveRecord::Base
  has_and_belongs_to_many :dvd_categories
end
class Dvd < ActiveRecord::Base
  has_and_belongs_to_many :book_categories
end

class Category < ActiveRecord::Base
end
class DvdCategory < Category
  has_and_belongs_to_many :dvds
end
class BookCategory < Category
  has_and_belongs_to_many :books
end

I've never done this before, but I believe you would do this:

class Category < ActiveRecord::Base
end

class DvdCategory < Category
  has_and_belongs_to_many :dvds
end

class BookCategory < Category
  has_and_belongs_to_many :books
end

class Book < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

class Dvd < ActiveRecord::Base
  has_and_belongs_to_many :cateogries
end


In that case you would have 2 HABTM join tables (books_categories and categories_dvds). I think anyway.

marsvin wrote:

That leads me to a related question, is there a "send" type method that will let you send Model names rather than method names? So is there something that would let me do this:

a = "Book"
send( "#{a}Category" ).find :all

I'm probably going over board here but it's worth a shot. wink

You would do this:

a = "Book"
"#{a}Category".constantize.find :all

I'm not sure why you would need to do this. If you need to do this frequently it may mean you need a different database design.

Railscasts - Free Ruby on Rails Screencasts

Re: to STI or no TI

Ah constantize cool smile The main reason would be that I could do something like this in my controller:

  def index
    type = params[:type] || "Dvd"
    @products = type.constantize.find :all
    @categories = "#{type}Category".find :all
  end

And then just render the product/category lists using the common properties of the tables to avoid having seperate actions for each of the models.

Mostly the product-type specific information is only necessary in the detail views. Or I could build a few unless @product.pages.nil? type blocks into the view.

Is this really horribly ugly design? Can I ask how you'd go about it?

I'm going to have a go at that HABTM/STI combination and let you know how it works out smile

Re: to STI or no TI

marsvin wrote:

Is this really horribly ugly design? Can I ask how you'd go about it?

It's hard to say without knowing exactly how the interface should behave. Your approach does sound reasonable though. I would give it a try.

One thing I should mention. Without a generic Product model it will be difficult to show all of the products in one big list (independent of their type). You can use a UNION select statement or just join the different type of products together. However, if you frequently need to list all of the products together you may be better off with the separate Product model.

Railscasts - Free Ruby on Rails Screencasts

Re: to STI or no TI

Sounds good.. Normally I wouldn't mix and match products in the views, I just meant sharing in the sense of having one action and view to cover either a list of Dvds or a list of Books. So that shouldn't be a problem.

Thanks for the help so far Ryan, I'm going give remodeling everything a go.. (and probably be back with more questions soon tongue)

Last edited by marsvin (2007-03-02 15:54:10)

Re: to STI or no TI

How do you guy's bring in those nifty blue and white lined source code listings to demonstrate your points?
How do you highlight the portion of a post you're commenting on?
I'd really like to be a more productive member of this forum and learn how to use these tools.
Thank you,
David

Re: to STI or no TI

You can add that by surrounding the code with [ code ] and [ /code ] blocks (without the spaces). If you want the Ruby syntax highlighting, you would use [ code=ruby ] as the starting tag.

Railscasts - Free Ruby on Rails Screencasts