I have the following models
class Widget
has_one :widget_sprocket
has_one :sprocket, through: :widget_sprockets
end
class Sprocket
has_many :widget_sprockets
has_many :widgets, though: :widget_sprockets
end
class WidgetSprocket
belongs_to :widget
belongs_to :sprocket
end
This works fine in the console, but I'm struggling with view updates for Widget. has_many :through gives Sprocket widget_ids, which I believe can be treated like a local attribute for most purposes, but the Rails docs evidently expect a different table configuration for has_one :through and therefore doesn't define a sprocket_id on Widget. As a result code like this throws an unknown attribute error
<%= f.collection_select(:sprocket_id, Sprocket.all, :id, :sprocket_type) %>
Of course I could use has_many :through for both models, but I consider it a last resort.
I think you're falling for a classic trap and overcomplicating this. If you want a one to many assocation between Sprocket and Widget you should just be using belongs_to and adding a sprocket_id foreign key column to the widgets table:
class AddSprocketToWidgets < ActiveRecord::Migration[6.1]
def change
add_reference :widgets, :sprocket, null: false, foreign_key: true
end
end
class Widget
belongs_to :sprocket
end
This guarentees on the database level that a Widget can only have one Sprocket because the column can only hold one single value. Your join table gives no such guarentee. You're really just selecting the first matching row off the join table and its actually a many to many relation. Unless thats acceptable or you prevent it with unique indexes thats an invitation for some nasty bugs.
While there are scenarios where you actually need an intermediadary table that describes a one to many relation - YAGNI.
has_many :through gives Sprocket widget_ids, which I believe can be treated like a local attribute for most purposes
Its not an attribute in any way or form. Its a method which will actually do a SELECT id FROM other_tablequery unless the assocation is preloaded.
but the Rails docs evidently expect a different table configuration for has_one :through and therefore doesn't define a sprocket_id on Widget.
Classic noob trap caused by the confusing semantics of the method names. has_one means there is a foreign key column on the other models table. Its like has_many but with a LIMIT 1 tacked onto the end of the query. To get the id you would actually call other.id.
In the case of belongs_to its not the relations macro that creates the attribute. Its having an actual sprocket_id column on the widgets table.
If you actually wanted to go though creating an intermediary table you can't just assign an id. You would have to use nested attributes and fields_for to create or update a WidgetSprocket instance. Again YAGNI.
Related
My Seminar model has a column named teacher_id. That part of the relationship is working. I can call seminar.teacher to retrieve the User who teaches that seminar.
I'd like to be able to invert that query. In other words, I need to call teacher.own_seminars to get a collection of all the seminars where that User is listed as the teacher_id. I know that I could call Seminar.where(:teacher => teacher), but that's clunky. And I think that the performance is probably worse that way.
Note: Some of the Users are students who are linked to Seminar through the seminar_user join table, but I don't think that affects this question.
This is the model setup that isn't quite working yet
class User < ApplicationRecord
has_many :own_seminars, :class_name => "Seminar", foreign_key: 'own_seminar_ids'
end
class Seminar < ApplicationRecord
belongs_to :teacher, class_name: "User", foreign_key: 'teacher_id'
end
Cheers!
In foreign_key option, you specify the column which is, well, the foreign key.
The way has_many works, is it tries to guess, which one of the fields in the referenced entity corresponds to the primary key of this entity. By default, it's user_id (derived from name User). But since your column is actually called teacher_id, you should use that instead.
has_many :own_seminars,
class_name: "Seminar",
foreign_key: 'teacher_id'
TL;DR: How do I use the ID of the respective parent object in a has_many SQL clause to find child objects?
Long version:
I have the following example code:
class Person < AR::Base
has_many :purchases, -> {
"SELECT * from purchases
WHERE purchase.seller_id = #{id}
OR purchase.buyer_id = #{id}"
}
This was migrated from Rails 3 which worked and looked like
has_many :purchases, :finder_sql => proc { #same SQL as above# }
I want to find all purchases associated with a Person object in one association, no matter whether the person was the one selling the object or buying it.
Update: I corrected the SQL, it was inside out. Sorry! Also: The association only needs to be read-only: I am never going to create records using this association, so using id twice should be OK. But I do want to be able to chain other scopes on it, e.g. #person.purchases.paid.last_5, so creating the sum of two associations in a method does not work (at least it didn't in Rails 3) since it doesn't return an AM::Relation but a simple Array.
When using this above definition in Rails 4.2, I get
> Person.first.purchases
undefined method `id' for #<Person::ActiveRecord_Relation:0x...>
The error is clear, but then how do I solve the problem?
Since this is only an example for much more complicated SQL code being used to express has_many relationships (e.g. multiple JOINS with subselects for performance), the question is:
How do I use the ID of the parent object in a has_many SQL clause?
I don't think your code will work at all. You are defining an association with two foreign keys ... that'd mean that in case you want to create a new Person from a present Purchase, what foreign key is to be used, seller_id or buyer_id? That just don't make sense.
In any case, the error you are getting is clear: you are calling a variable id which is not initialized in the block context of the SQL code.
A better approach to the problem I understand from your question would be to use associations in the following way, and then define a method that gives you all the persons, both buyers and sellers that a product has. Something like this:
class Purchase < ActiveRecord::Base
belongs_to :buyer, class_name: 'Person'
belongs_to :seller, class_name: 'Person'
def persons
ids = (buyer_ids + seller_ids).uniq
Person.where(ids: id)
end
end
class Person < ActiveRecord::Base
has_many :sold_purchases, class_name: 'Purchase', foreign_key: 'buyer_id'
has_many :buyed_purchases, class_name: 'Purchase', foreign_key: 'seller_id'
end
Im my approach, buyer_id and seller_id are purchase's attributes, not person's.
I may have not understood correctly, in that case please clarify.
Assuming
class Kid < ActiveRecord::Base
has_one :friend
end
class Friend< ActiveRecord::Base
belongs_to :kid
end
How can I change this to
class Kid < ActiveRecord::Base
has_many :friends
end
class Friend< ActiveRecord::Base
belongs_to :kid
end
Will appreciate your insight...
Collection
The bottom line is that if you change your association to a has_many :x relationship, it creates a collection of the associative data; rather than a single object as with the single association
The difference here has no bearing on its implementation, but a lot of implications for how you use the association throughout your application. I'll explain both
Fix
Firstly, you are correct in that you can just change your has_one :friend to has_many :friends. You need to be careful to understand why this works:
ActiveRecord associations work by associating something called foreign_keys within your datatables. These are column references to the "primary key" (ID) of your parent class, allowing Rails / ActiveRecord to associate them
As long as you maintain the foreign_keys for all your Friend objects, you'll get the system working no problem.
--
Data
To expand on this idea, you must remember that as you create a has_many association, Rails / ActiveRecord is going to be pulling many records each time you reference the association.
This means that if you call #kind.friends, you will no longer receive a single object back. You'll receive all the objects from the datatable - which means you'll have to call a .each loop to manipulate / display them:
#kid = Kid.find 1
#kid.friends.each do |friend|
friend.name
end
If after doing this changes you have problem calling the save method on the order.save telling you that it already exists, and it not allowing you to actually have many order records for one customer you might need to call orders.save(:validate=> false)
You have answered the question. Just change it in model as you've shown.
Let's say I have three tables to accommodate a many-to-many relationship.
class User < ActiveRecord::Base
has_many :user_hobbies
has_many :hobbies, :through => :user_hobbies
end
class UserHobbies < ActiveRecord::Base
belongs_to :user
belongs_to :hobby
end
class Hobby < ActiveRecord::Base
has_many :user_hobbies
has_many :users, :through => :user_hobbies
end
And I wanted to have a form in which a user can input as many hobbies as they want, where each would be stored into the correct tables (in my situation, the 'Hobby' table is preset, the user may select from enumerated values, not add them)
How would I go about producing a form to achieve this? I would use JavaScript for Auto-Completion and dynamic fields (i.e. each time you enter a field, another appears).
In order to produce this form there are two options which are available to you..
a. The first option is to use the virtual attribute which would accept the comma separated values of the hobbies of the user and then when you save the user the virtual attribute would then set the hobbies corresponding to the respective user..See this railcast
http://railscasts.com/episodes/16-virtual-attributes
http://railscasts.com/episodes/167-more-on-virtual-attributes
b. The second option is to use the accepts_nested_attributes_for which would automatically include the attributes for the nested relationships in the parent model...Consult the API for more information on accepts_nested_attributes_for
Also I can see a bug in the above definitions as there should be has_many :users :through :user_hobbies relationship in the Hobbies table becoz we might want to know the user having the..
As far as the auto-completion is concerned, there is a gem called as auto-complete-rails and its documentation is pretty simple and standard to use and understand..
I have a table with id|patient_id|client_id|active. A record is unique by patient_id, client_id meaning there should only be one enrollment per patient per client. Normally I would make that the primary key, but in rails I have id as my primary key.
What is the best way to enforce this? Validations?
Sounds like you have a model relationship of:
class Client < ActiveRecord::Base
has_many :patients, :through => :enrollments
has_many :enrollments
end
class ClientPatient < ActiveRecord::Base
belongs_to :client
belongs_to :patient
end
class Patient < ActiveRecord::Base
has_many :clients, :through => :enrollments
has_many :enrollments
end
To enforce your constraint I would do it in ActiveRecord, so that you get proper feedback when attempting to save a record that breaks the constraint. I would just modify your ClientPatient model like so:
class Enrollment < ActiveRecord::Base
belongs_to :client
belongs_to :patient
validates_uniqueness_of :patient_id, :scope => :client_id
end
Be careful though because, while this is great for small-scale applications it is still prone to possible race conditions as described here: http://apidock.com/rails/v3.0.5/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of under "Concurrency and Integrity"
As they describe there, you should also add a unique index to the table in the database. This will provide two immediate benefits:
The validation check and any searches through this model based on these two id's will perform faster (since they're indexed)
The uniqueness constraint will be enforced DB-side, and on the rare occurrence of a race condition you won't get bad data saved to the database... although users will get a 500 Server Error if you don't catch the error.
In a migration file add the following:
add_index :enrollments, [:patient_id, :client_id], :unique => true
Hopefully this was helpful :)
Edit (fixed some naming issues and a couple obvious bugs):
It's then very easy to find the data you're looking for:
Client.find_by_name("Bob Smith").patients
Patient.find_by_name("Henry Person").clients
Validations would work (Back them up with a unique index!), but there's no way to get a true composite primary key in vanilla Rails. If you want a real composite primary key, you're going to need a gem/plugin - composite_primary_keys is the one I found, but I'm sure there are others.
Hope this helps!
Add a UNIQUE constraint to your table across the two columns. Here's a reference for MySQL http://dev.mysql.com/doc/refman/5.0/en/constraint-primary-key.html