I'm trying to define multiple polymorphic relations (has_many_polymorphs plugin) from a single parent to same children.
Note has many viewers
Note has many editors
Viewers could be either Users or Groups
Editors could be either Users or Groups
Permission is the association model with note_id, viewer_id, viewer_type, editor_id, editor_type fields
Everything works out as expect as long as I have only one has_many_polymorphs relation defined in Note model
class User < ActiveRecord::Base; end
class Group < ActiveRecord::Base; end
class Note < ActiveRecord::Base
has_many_polymorphs :viewers, :through => :permissions, :from => [:users, :groups]
end
class Permission < ActiveRecord::Base
belongs_to :note
belongs_to :viewer, :polymorphic => true
end
Note.first.viewers << User.first # => [#<User id: 1, ....>]
Note.first.viewers << Group.first # => [#<User id: 1, ....>, #<Group ...>]
Note.first.viewers.first # => #<User ....>
Note.first.viewers.second # => #<Group ....>
Now, problems start to appear when I add the second relation
class Note < ActiveRecord::Base
has_many_polymorphs :viewers, :through => :permissions, :from => [:users, :groups]
has_many_polymorphs :editors, :through => :permissions, :from => [:users, :groups]
end
class Permission < ActiveRecord::Base
belongs_to :note
belongs_to :viewer, :polymorphic => true
belongs_to :editor, :polymorphic => true
end
Note.first.viewers << User.first # => [#<User id: ....>]
# >>>>>>>>
Note.first.editors << User.first
NoMethodError: You have a nil object when you didn't expect it!
The error occurred while evaluating nil.constantize
... vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/base.rb:18:in `instantiate'
I've tried refining the definition of has_many_polymorphs but it didn't work. Not even with an STI model for ViewPermission < Permission, and EditPermission < Permission.
Any thoughts / workarounds / issue pointers are appreciated.
Rails 2.3.0
Dont you need to add
has_many :permissions
to your Note.
FYI. I used has_many_polymorphs once but then dropped it, it wasn't working as expected.
Can you post the schema that you are using for Permission? My guess is the root of the problem lies there, you need to have multiple type, id pairs in the schema since you have two different belongs_to in the definition.
Edit:
I see you have posted the question on github as well. Not sure if you tried using the Double sided polymorphism. You probably have... like I said, I was not impressed by this plugin, as it brought in some instability when I used it.
== Double-sided polymorphism
Double-sided relationships are defined on the join model:
class Devouring < ActiveRecord::Base
belongs_to :guest, :polymorphic => true
belongs_to :eaten, :polymorphic => true
acts_as_double_polymorphic_join(
:guests =>[:dogs, :cats],
:eatens => [:cats, :birds]
)
end
Now, dogs and cats can eat birds and cats. Birds can't eat anything (they aren't <tt>guests</tt>) and dogs can't be
eaten by anything (since they aren't <tt>eatens</tt>). The keys stand for what the models are, not what they do.
#Tamer
I was getting the same error. The problem was that has_many_polymorphs creates the record in the join table using mass association and was failing. I added attr_accessible :note_id, :editor_id, and :editor_type to my Permission class and it worked afterwards. (Note: I substituted your model names for mine.)
I haven't looked too much into it, but I'd be curious if there's a way to alter this behavior. I'm fairly new to this framework and letting anything sensitive (like an Order-Payment association) be mass-assigned seems like asking to shoot myself in the foot. Let me know if this fixed your problem, and if you figure anything else out.
Best,
Steve
Related
I'm in the process of teaching myself Rails and I'm stumped as to why an association isn't working correctly. I think I'm missing something pretty basic but I can't snuff out exactly what.
I have two classes -- Builds and Equipment. Builds are made of 2 pieces of equipment, deviously titled position_1 and position_2. Here's what my definitions look like:
class Build < ActiveRecord::Base
has_one :position_1, :class_name => "Equipment"
has_one :position_2, :class_name => "Equipment"
attr_accessor :position_1, :position_2
end
and
class Equipment < ActiveRecord::Base
belongs_to :build, :foreign_key => :position_1
belongs_to :build, :foreign_key => :position_2
end
(Ignore for the moment that this could be handled by a relationship table to support any number of positions -- I'm basically trying to figure out how to have a class with two has_one relationships to another class.)
Now if I try and do something simple like this....
position_1 = Equipment.find(params[:build][:position_1])
position_2 = Equipment.find(params[:build][:position_2])
#build = Build.new
#build.position_1 = position_1
#build.position_2 = position_2
logger.debug("THE BUILD IS #{#build.inspect}")
I will have successfully created a build object with the equipment objects correctly assigned to the position_1 parameter, but the position_1 and position_2 fields of the build parameter are left nil.
logger.debug("THE EQUIPMENT IS #{#build.position_1}")
> EQUIPMENT IS #<Equipment:0x007fa0581705c0>
logger.debug("THE BUILD IS #{#build.inspect}")
> THE BUILD IS #<Build id: nil, position_1_id: nil, position_2_id: nil, created_at: "2013-05-27 18:00:32", updated_at: "2013-05-27 18:00:32">
What am I getting wrong here?
First, I would not include the associations as "attr_accessors" in the "parent" ActiveRecord. Rails creates the correct association field for Build for you.
When you create instances of the positions, like an instance of Equipment, the correct way to assign them to a new Build would be:
position_1 = Equipment.find(params[:build][:position_1])
position_2 = Equipment.find(params[:build][:position_2])
#build = Build.new
#build.position_1 << position_1
#build.position_2 << position_2
Here's good guide on Rails associations.
Figured it out -- I had misunderstood part of how Rails does associations -- what I really needed was...
class Equipment < ActiveRecord::Base
has_many :builds, :foreign_key => :position_1
has_many :builds, :foreign_key => :position_2
end
class Build < ActiveRecord::Base
belongs_to :position_1, :class_name => "Equipment", :foreign_key => "position_1_id"
belongs_to :position_2, :class_name => "Equipment", :foreign_key => "position_2_id"
end
With this in place everything works as I expected.
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!
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
I have a Factsheet model which holds an assortment of publications, including alternate language versions. The different language versions should be kept separate as individual records (because they can be ordered/updated/etc. separately), but I'm trying to associate them to each other so that you can easily tell when one publication is the Spanish (or Chinese, etc.) version of the other.
I would like to use a :through association so that the relationship is symmetric, e.g. if English Factsheet A has a Spanish version Factsheet B, then similarly Factsheet B has an English version Factsheet A.
Here are my models:
class Factsheet < ActiveRecord::Base
has_many :publications_language_relationships
has_one :en, :through => :publications_language_relationships
has_one :es, :through => :publications_language_relationships
has_one :zh, :through => :publications_language_relationships #zh = Chinese
# other stuff
end
and...
# Table name: publications_language_relationships
#
# en_id :integer
# es_id :integer
# zh_id :integer
#
class PublicationsLanguageRelationship < ActiveRecord::Base
belongs_to :en, :class_name => 'Factsheet'
belongs_to :es, :class_name => 'Factsheet'
belongs_to :zh, :class_name => 'Factsheet'
end
But when I fire up a Rails console to check to see if that works at all...
$ fs = Factsheet.last
=> #<Factsheet id: 5, title: "Despu\xC3\xA9s de un diagn\xC3\xB3stico de c\xC3\
xA1ncer de seno: Con...", backend_code: "fs_after_bc_diagnosis_es", language:
"es", created_at: "2010-11-30 21:23:01", updated_at: "2010-12-06 16:13:23">
$ fs.en
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: publicati
ons_language_relationships.factsheet_id: SELECT "factsheets".* FROM "factsheets"
INNER JOIN "publications_language_relationships" ON "factsheets".id = "publicat
ions_language_relationships".en_id WHERE (("publications_language_relationships"
.factsheet_id = 5)) LIMIT 1
So something's amiss with my associations, but I'm not quite sure what. Thoughts?
Additionally, is this even a sound design for the data, or should I be doing something differently here?
I haven't tested this solution, but I think the general architectural direction you want to take is not really through the :through relationship, but rather something like:
class Factsheet < ActiveRecord::Base
has_many :publications_language_relationships
named_scope :translation, lambda { |trans|
{ :conditions => ["publications_language_relationships = ?", trans.to_s ] ,
:joins => :publications_language_relationships
}
}
# other stuff
end
class PublicationsLanguageRelationship < ActiveRecord::Base
belongs_to :fact_sheet
end
And then, I think you'd call the translations in your controller/views something like this:
#controller
def show
#fact_sheet = FactSheet.find( params[:id] ) # to load up the FactSheet
end
#view (to get the right translation)
#fact_sheet.translation(:en) #for english
I don't think this is absolutely right, but it should get you on the path anyway.
(This is not the actual code I'm using, although this sums up the idea of what I want to do)
class Connection < ActiveRecord::Base
belongs_to :connection1, :polymorphic => true
belongs_to :connection2, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :followers, :class_name => 'Connection', :as => :connection1
has_many :followings, :class_name => 'Connection', :as => :connection2
end
My question is that I want to know how I will be able to create a method called "network" such that what is returned isn't an array. Like so,
u = User.first
u.network # this will return a merged version of :followings and :followers
So that I'll still be able to do this:
u.network.find_by_last_name("James")
ETA:
Or hmm, I think my question really boils down to if it is possible to create a method that will merge 2 has_many associations in such a way that I can still call on its find_by methods.
Are you sure that you want a collection of Connections, rather than a collection of Users?
If it's a collection of Connections that you need, it seems like you'll be well served by a class method on Connection (or scope, if you like such things).
connection.rb
class Connection < ActiveRecord::Base
class << self
def associated_with_model_id(model, model_id)
include([:connection1, :connection2]).
where("(connection1_type IS #{model} AND connection1_id IS #{model_id})
OR (connection2_type IS #{model} AND connection2_id IS #{model_id})")
end
end
end
user.rb
class User < ActiveRecord::Base
def network
Connection.associated_with_model_id(self.class.to_s, id)
end
end
Probably not as useful as you'd like, but maybe it'll give you some ideas.
I have some geography related classed defined as follows:
class Location < ActiveRecord::Base
end
class State < Location
has_many :geographies
has_many :cities
has_many :counties
end
class County < Location
belongs_to :state
has_many :geographies
has_many :cities, :through => :geographies, :uniq => true
end
class City < Location
belongs_to :state
has_many :geographies
has_many :counties, :through => :geographies, :uniq => true
end
class Geography < ActiveRecord::Base
belongs_to :state
belongs_to :city
belongs_to :county
end
The following console output demonstrates the problem that I'm having.
>> County.first.cities.loaded?
=> false
>> County.first.state.loaded?
=> true
Looking in the logs I see that when I call state.loaded? it runs a SQL statement to load the state, i.e., the state was not loaded until I "touched" it. However, when I call cities.loaded? no SQL is executed and false is returned. This behavior seems inconsistent to me. I searched around a bit on the interwebs and couldn't find anything regarding this so I'm guessing it's my mistake, though, I don't see how.
Any help is much appreciated.
Thanks in advance.
I think it comes down to the type of relationship being used and the interaction with lazy loading.
When you call County.first.state, Rails will load the state object that belongs to the County - there is only one, and therefore a call to .state is actually a call to a 'concrete' object in the database that can be loaded.
However, when you call County.first.cities, you are actually calling the relationship collection that belongs to the county object. As you don't actually call on a specific object or set of conditions, Rails is smart enough not to load the collection.
If you said something like:
County.first.cities.all
County.first.cities.each
County.first.cities.first
Rails would then kick-off the SQL statement and load the data.
I found that you can use the Object method instance_variable_get to check to see if a belongs_to association is loaded without triggering the association to be loaded during your check. So...
>> c = County.first
=> #<County id: 1, ...>
>> c.instance_variable_get("#state")
=> nil
>> c.state
=> #<State id: 1, ...>
>> c.instance_variable_get("#state")
=> #<State id: 1, ...>
>> c = County.first(:include => :state)
=> #<County id: 1, ...>
>> c.instance_variable_get("#state")
=> #<State id: 1, ...>