Reputation: 55
I have two tables. Items, and Vendors. Items are sold by Vendors. So Item belongs_to :vendor and Vendor has_many :items. That works fine.
However, Items are not always manufactured by the Vendors that sell them, but sometimes they are. So I have a new column in my Item table called "manufacturer_id". Rather than generate a new model called Manufacturer that duplicates Vendor identically, I tried to do a complex has_many and belongs_to to define manufacturer.
See here:
class Item < ActiveRecord::Base
belongs_to :vendor
belongs_to :manufacturer, :class_name => "Vendor", :foreign_key => "manufacturer_id"
end
class Vendor < ActiveRecord::Base
has_many :items
has_many :manufactured_items, :class_name => "Item", :foreign_key => "manufacturer_id"
end
Populating manufacturer_id in the items table works as expected on Create commands:
Item.create(:manufacturer => Vendor.find_by_abbrev("INV"))
And I can even get the manufacturer as the operation
item.manufacturer
which returns:
<Vendor:0x007ff06684e398>
HOWEVER:
item.manufacturer.name
fails completely with a hard exeption and I get the error:
undefined method `name' for nil:NilClass
running
debug item.manufacturer
gives
--- !ruby/object:Vendor
attributes:
id: 181
name: Invitrogen
website: http://www.invitrogen.com/
created_at: 2012-01-08 01:39:07.486375000Z
updated_at: 2012-01-08 01:39:07.486375000Z
abbrev: INV
so item.manufacturer.name should return the name for that vendor object above, Vendor: 0x007ff06684e398.
What am I doing wrong here?
Also, once I get this working I'd like to be able to similarly call:
vendor.manufactured_items
to get all the items that have the manufacturer_id of that vendor. is there a straightforward way to do that too?
My last ditch effort may involve having to do:
manufacturer = Vendor.new(item.manufacturer)
But that seems totally wrong, and goes against the rails documentation here: http://guides.rubyonrails.org/association_basics.html#self-joins
please help!
Upvotes: 3
Views: 626
Reputation: 33954
Okay, I've actually built a demo Rails 3.1 project for you and posted it on GitHub. I've included the console output in the README
file to prove that calls like item.seller.name
and item.manufacturer.name
work, as well as round-trip calls like vendor.sold_items.first.manufacturer.name
, which allow you to get the name of the manufacturer of the first sold item for a particular vendor, for example.
I think the root of the thing, as you noted, is that a vendor
and a manufacturer
, for all intents and purposes, are identical. For that reason I combined them simply into the Vendor
class, and setup the foreign key relationships in such a way that it should work the way I think you want it to.
In particular, you should pay attention to the README file, which has the console session output that I ran to show it working. You'll also want to take a look at the two model classes and how their associations are defined, as well as the spec/factories.rb
file for how it sets up the fake database data (I've included those below).
In re-reading your question this morning, I'm not sure what you were doing wrong, but you can probably chalk it up to a subtle error in your associations somewhere. It's probably a case of you being really close, but not quite there. :D
Here's some snipets from the code:
app/models/item.rb
class Item < ActiveRecord::Base
belongs_to :seller, :class_name => "Vendor"
belongs_to :manufacturer, :class_name => "Vendor"
end
app/models/vendor.rb
class Vendor < ActiveRecord::Base
has_many :sold_items, :class_name => "Item", :foreign_key => :seller_id
has_many :manufactured_items, :class_name => "Item", :foreign_key => :manufacturer_id
end
spec/factories.rb
require 'populator'
require 'faker'
FactoryGirl.define do
factory :vendor do |v|
v.name {Populator.words(1..3)}
v.website {Faker::Internet.domain_name}
v.abbr {(["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VWX", "YZ1"])[rand(9)]}
end
factory :item do |i|
i.association :seller, :factory => :vendor
i.association :manufacturer, :factory => :vendor
i.name {Populator.words(3..5)}
end
end
lib/tasks/populator.rake
namespace :db do
desc "Erase database"
task :erase => :environment do
puts "Erasing..."
[Vendor, Item].each(&:delete_all)
end
desc "Erase and fill database"
task :populate => [:environment, :erase] do
require 'populator'
require 'faker'
puts "Populating: enjoy this random pattern generator while you wait..."
50.times{Factory.create(:vendor)}
Vendor.all.each do |v|
# This line actually has a bug in it that makes all `seller_id` and `manufacturer_id`
# columns always contain a value in the range 0..50. That means
# `rake db:populate` will only actually work the first time, but
# I think you get the idea of how this should work.
10.times{Factory.create(:item, :seller_id => (rand(50) + 1), :manufacturer_id => (rand(50) + 1))}
print (['\\', '/', '_', '|'])[rand(4)]
end
puts ""
end
end
Upvotes: 3