Skip to content

Instantly share code, notes, and snippets.

@melriffe
Created February 26, 2012 00:54
Show Gist options
  • Save melriffe/1911966 to your computer and use it in GitHub Desktop.
Save melriffe/1911966 to your computer and use it in GitHub Desktop.
Notes on changing a Many-to-Many to a Self-Referential Many-to-Many
Currently I have Kits and Items. A Kit can contain many Items; an Item can be associated to many Kits.
I also have a join model, KitItem, that allows me to define how many of an Item is associated to a Kit.
Here are the model definitions:
class Kit < ActiveRecord::Base
belongs_to :category
has_many :kit_items, :dependent => :destroy
has_many :items, :through => :kit_items
end
class Item < ActiveRecord::Base
belongs_to :category
has_many :kit_items, :dependent => :destroy
has_many :kits, :through => :kit_items
end
class KitItem < ActiveRecord::Base
belongs_to :item
belongs_to :kit
end
# == Schema Information
#
# Table name: kit_items
#
# id :integer(4) not null, primary key
# quantity :integer(4)
# kit_id :integer(4)
# item_id :integer(4)
# created_at :datetime
# updated_at :datetime
#
# Indexes
#
# index_kit_items_on_item_id (item_id)
# index_kit_items_on_kit_id (kit_id)
#
However, I need to support the concept of a Kit containing other Kits.
I'm not sure how to implement this. The examples I've seen so far are for single table self-joins;
I've only confused myself when attempting to alter them for my purposes.
@saturnflyer
Copy link

could you make kit_items have polymorphic associations where it has an item_id and item_type (of either "Item" or "Kit")?

@melriffe
Copy link
Author

@saturnflyer: I had thought about that, but wasn't sure how to implement.

Would it be:

class KitItem < ActiveRecord::Base
belongs_to :kit
belongs_to :itemable, :polymorphic => true
end

class Item < ActiveRecord::Base
belongs_to :category
has_many :kit_items, :as => :itemable, :dependent => :destroy
has_many :kits, :through => :kit_items
end

class Kit < ActiveRecord::Base
belongs_to :category
has_many :kit_items, :as => :itemable, :dependent => :destroy
has_many :items, :through => :kit_items
end

@bokmann
Copy link

bokmann commented Feb 26, 2012

ok, at first I thought you wanted a Kit to be composed of many sub-kits... is that the case, or do you want a many to many of Kits?

I'll work an example for you either way... but if many to many, you need to use has_many :through...

@melriffe
Copy link
Author

@bokmann: Yes, I need a many to many between Kits. I already have the many to many between Kits and Items. Now the client wants a Kit to contain other Kits, sub or otherwise.

@bokmann
Copy link

bokmann commented Feb 26, 2012

ok, gimmie a sec and I'll work that up too. here is an example of my first example, a kit composed of sub-kits...

the migration-

class CreateKits < ActiveRecord::Migration
def change
create_table :kits do |t|
t.string :name
t.integer :parent_kit_id
t.timestamps
end
end
end

kit.rb

class Kit < ActiveRecord::Base
has_many :sub_kits, :class_name => "Kit", :foreign_key => :parent_kit_id
belongs_to :parent_kit, :class_name => "Kit"
end

exercising it from rails console -

k = Kit.create(:name => "Airplane")
k.sub_kits << Kit.create(:name => "Propeller")
k.sub_kits << Kit.create(:name => "left wing")
k.sub_kits << Kit.create(:name => "right wing")
k.sub_kits << Kit.create(:name => "tail rudder")
k.sub_kits << Kit.create(:name => "fuselage")
k.sub_kits.count
=> 5
Kit.last.parent_kit.name
=> airplane

The next solution is tricky, with a Kit on both sides of a has_many :through. I think I got that to work before, although it might have involved a custom query. Let me hunt through my projects for that.

@bokmann
Copy link

bokmann commented Feb 26, 2012

Mel, check out this gist:

https://gist.github.com/1912278

@bokmann
Copy link

bokmann commented Feb 26, 2012

Check out this one for a slight improvement:

https://gist.github.com/1912350

@bokmann
Copy link

bokmann commented Feb 26, 2012

After read Jim's comment, I realized my solutions might have missed part of what you need. I have the 'many kits' part, but thought you wanted them in a separate collection. Are you saying you want something like my_kit.contents, and contents will return a heterogenous collection containing kits and items?

@melriffe
Copy link
Author

I believe I do.

Without regard to implementation, I need the following 'behavior'

kit = Kit.new(:name => "Surf Rider Kit")
kit.kitables.build(:kitable => Item.find_by_name("Twin Set"), :quantity => 1)
kit.kitables.build(:kitable => Kit.find_by_name("Towel Set"), :quantity => 1)
kit.save
kit.items #=> [Twin Set]
kit.kits #=> [Towel Set]
kit.kits.first.items #=> [Bath Towel, Hand Towel, Wash Cloth]
kit.kits.first.kits #=> []
kit.kitables.size #=> 2

@bokmann
Copy link

bokmann commented Feb 26, 2012

Ok, your use is more of a tree, where the edges have a quantity and the nodes are either a kit or an item (another node or a leaf). I read too much of a previous problem into it and have you peer stuff. You could do this by either using STI and having Kit and Item share a table, or have the KitItem be polymorphic as Jim suggested (but that might mean not being able tO traverse back and answer 'what kits is this towel package in?'

More code to follow

@bokmann
Copy link

bokmann commented Feb 26, 2012

Mel, sorry to have spun you off in the wrong direction - I had read too much of a previous problem into your situation and thought you were looking for 'peer' kits, as if you were doing suggestions ("Here are Kits like the one you're looking at!"). Jim's comment points you in the right direction, but you can't do a has_many :through a polymorphic relationship. I think this example gives you what you need though:

https://gist.github.com/1915372

@saturnflyer
Copy link

I'm definitely studying this less for the code and more for the magical incantation to summon @bokmann

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment