Rails has_one/belongs_to associations not working as expected - ruby-on-rails

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.

Related

Rails 5 how to form association between tables on multiple shared attributes

In Rails 5, given a relationship between two tables that involves joining them on multiple shared attributes, how can I form an association between the models corresponding to these tables?
SQL:
SELECT *
FROM trips
JOIN stop_times ON trips.guid = stop_times.trip_guid AND trips.schedule_id = stop_times.schedule_id
I tried the following configuration, which works in general...
class Trip < ApplicationRecord
has_many :stop_times, ->(trip){ where("stop_times.schedule_id = ?", trip.schedule_id) }, :inverse_of => :trip, :primary_key => :guid, :foreign_key => :trip_guid, :dependent => :destroy
end
class StopTime < ApplicationRecord
belongs_to :trip, :inverse_of => :stop_times, :primary_key => :guid, :foreign_key => :trip_guid
end
Trip.first.stop_times.first #> StopTime object, as expected
Trip.first.stop_times.first.trip #> Trip object, as expected
... but when I try to use it in more advanced queries, it triggers ArgumentError: The association scope 'stop_times' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported....
Trip.joins(:stop_times).first #=> the unexpected ArgumentError
StopTime.joins(:trip).first #> StopTime object, as expected
I understand what the error is referencing, but I'm unsure of how to fix it.
EDIT:
I was hoping a single association would be sufficient, but it has been noted two different associations can do the job:
class Trip < ApplicationRecord
has_many :stop_times,
->(trip){ where("stop_times.schedule_id = ?", trip.schedule_id) },
:primary_key => :guid,
:foreign_key => :trip_guid # use trip.stop_times instead of trip.joined_stop_times to avoid error about missing attribute due to missing join clause
has_many :joined_stop_times,
->{ where("stop_times.schedule_id = trips.schedule_id") },
:class_name => "StopTime",
:primary_key => :guid,
:foreign_key => :trip_guid # use joins(:joined_stop_times) instead of joins(:stop_times) to avoid error about instance-specific association
end
Trip.first.stop_times
Trip.eager_load(:joined_stop_times).to_a.first.joined_stop_times # executes a single query
If anyone reading this knows how to use a single association, please at-mention me.
I don't think it is the right solution, but it can help. You can add another similar instance independent association that will be used for preloading only. It will work with :joins and :eager_load but not with :preload.
Note that :includes might internally use either :eager_load or :preload. So, :includes will not always work with that association. You should explicitly use :eager_load instead.
class Trip < ApplicationRecord
has_many :preloaded_stop_times,
-> { where("stop_times.schedule_id = trips.schedule_id") },
class_name: "StopTime",
primary_key: :guid,
foreign_key: :trip_guid
end
# Usage
trips = Trip.joins(:preloaded_stop_times).where(...)
# ...
# with :eager_load
trips = Trip.eager_load(:preloaded_stop_times)
trips.each do |trip|
stop_times = trip.preloaded_stop_times
# ...
end

Ruby on Rails associations not taking effect

I'm brand new to Ruby on Rails and I'm having a heck of a time making sense of my associations.
At my company we rent out Scanner Packs that include scanners and servers.
When we receive a request for a scanner pack, ideally I'd create a new scanner package with the customer info and attach however many scanners and servers are needed.
Here is what I have for my three models, scanner_pack, server and scanner:
scanner_pack.rb:
class ScannerPack < ActiveRecord::Base
attr_accessible :producer, :reserved_from, :reserved_to, :scanner_id, :server_id, :scanner_pack_id
has_many :scanners, :foreign_key => "scanner_id"
has_many :servers, :foreign_key => "server_id"
end
server.rb:
class Server < ActiveRecord::Base
attr_accessible :cat5, :power_cable, :router, :name, :status, :location, :id, :notes, :reserved_from, :reserved_to, :scanner_pack_id
belongs_to :scanner_pack, :class_name => "Server", :foreign_key => "server_id"
end
scanner.rb:
class Scanner < ActiveRecord::Base
attr_accessible :id, :location, :name, :notes, :serial, :status
belongs_to :scanner_pack, :class_name => "Scanner", :foreign_key => "scanner_id"
end
I've googled and searched for quite a while now and I've noticed sometimes people say to remove the attr_accessible for scanner_id and server_id in the scanner_pack model because it will overwrite the association. When I do that, I get the error:
ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes:
In the rails console when I attempt to create a new ScannerPack I'm doing something like this:
scanner = Scanner.find(1)
ScannerPack.create(:producer => 12345, :scanner_id => scanner.id)
Then if I try something like:
scannerpack = ScannerPack.find(1)
scannerpack.scanner_id
it returns the correct value for scanner_id
When I try: scannerpack.scanner.id It gives me an undefined method error (I've also tried scannerpack.scanners.id). In my mind it should return the id of the scanner from the scanner object
I'm thinking that I'm either missing something very simple or that I'm completely misunderstanding how to do this. Maybe I should be using a has_and_belongs_to_many assocation? I'd appreciate any help anyone can give!
thanks!
Edit: Here is the whole project on github.
Since you want to have many-to-many association, here is how to achieve this, step by step.
We should create model that will contain scanner_id and scanner_pack_id. Let's name it Pack (I'm sure you will be able to name it better).
rails g model Pack scanner_id:integer scanner_pack_id:integer
rake db:migrate
When you run it, you have to write appripriate "declarations" in your models:
class Pack
belongs_to :scanner
belongs_to :scanner_pack
end
class ScannerPack
has_many :packs
has_many :scanners, through: :packs
end
class Scanner
has_many :packs
has_many :scanner_packs, through: :packs
end
When you're done, you can easily bind scanner_packs with scanner (and vice versa) with (for example):
scanner.scanner_packs << scanner_pack
Remove all other params from belongs_to and has_many methods, leaving only association names.
Error is exactly here
:class_name => "Server"
and here
:class_name => "Scanner"
Reference http://guides.rubyonrails.org/association_basics.html#belongs_to-association-reference (scroll down to 4.1.2 Options for belongs_to and 4.1.2.2 :class_name

Rails 3.1 Advanced Has_many and belongs_to model joins

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

Rails has_one :through Association for Self-Referential 1-to-1-to-1

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.

Multiple has_many_polymorphs in one model

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

Resources