Rails association has_many and has_many through on same STI model - ruby-on-rails

I have a model Note.
Notes are very useful and are used for lots of purposes throughout the application.
We have another model User, and these are STI split between
User1s
and
User2s
We have a third model Thing, which are things belonging to Users... I'll leave it there for the moment as it gets a touch more complicated, (there are other models a bit like Things which one can likewise make notes on).
Note.rb
belongs_to :user #This signifies that the user wrote the note
belongs_to :notable, polymorphic: :true #This is what the note is referring to
User.rb
has_many :user_notes, class_name: "Note", foreign_key: "user_id"
#signifies the note was written by the User
User1.rb < User
has_many :notes, as: :notable
#Signifies the note was about the User1
has_many :things
User2.rb < User
has_many :notes, as: :notable
#Signifies the note was about the User2
Thing.rb
belongs_to :user1
has_many :notes, as: :notable
#The note is about the thing
These can be, but are not generally, self-referential (I write a note about myself).
Often User1s will write notes about User2s, or about User2s Things so that User2 can see them.
I think I've taken the correct route in renaming the user_notes.
I'm struggling with how one collects together all the notes that might be related to me.
i.e. Let's say I'm a user1.
I'd like to see the notes I've written, plus notes other people have written about me, plus the notes that have been written about my things.
Is there an easy way to do that?
I'm struggling with two points.
How can I connect the notes about my things?
has_many :thing_notes, through: :things, source: :notes
works, but I wonder if it's the correct way of approaching it.
Also, I'm interested in the reverse (which is also sort of useful), in this case ignoring who might have written the note, to return the relevant user1, if there is one. But...
delegate :user1, to: :notable
doesn't make sense if user1 is already a potential notable_type.
How can I collate all my notes (of whatever type) and return them?
i.e.
#user1.notes
will return just the notes about me
#user1.user_notes
will return notes I've written
#user1.thing_notes
will return notes about my things
I could merge them together, but I'll risk there being annoying duplicates, and it seems a shame to make multiple database calls and lose db ordering when they're all of one type...
Is there an obvious way to get all my notes...?
Note.where("(user_id = :user1_id) OR (notable_id = :user1_id AND notable_type = 'User') OR (notable_type = 'Thing' AND notable_id IN (:things))", user1_id: user1.id, things:user1.thing_ids)
is a touch on the ugly side and will only get uglier...
Hope that makes sense to someone who loves to solve such puzzles...

For the collation, if you're using Rails 5, you could do something like this to simplify the raw SQL:
Note.where(user: user1).or(Note.where(notable: user1)).or(Note.where(notable: user1.things))
If you're not using Rails 5, you can include this gem for the same functionality: https://github.com/Eric-Guo/where-or

Related

Object that belongs to multiple other objects

Take the following associations based on a Feedback model:
feedback.rb
belongs_to :buyer, :foreign_key => :buyer_id, :class_name => "User"
belongs_to :seller, :foreign_key => :seller_id, :class_name => "User"
belongs_to :listing
The reverse associations are has_many
When creating the feedback object it could be 'anchored' to any of the above other objects (e.g. buyer.feedbacks, seller.feedbacks, listing.feedbacks)
Say in this example we want to primarily link it with the buyer and so nest the feedback routes within user and then build a create action in the Feedbacks controller that looks something like:
current_user.feedbacks.new(feedback_params)
or is it more appropriate/correct to reference?
buyer.feedbacks.new(feedback_params)
Is this even valid without an explicit buyer model?
What is the Rails way to then incorporate the other belongs_to relationships?
It's not a nested attribute (as the other objects already exist).
Should I be merging in the other params with something like?
params.require(:feedback).permit(:rating, :comment).merge(seller_id: #seller.id, listing_id: #listing.id)
One approach I have seen is the use of a before_validation filter in this sort of manner (from a different domain) like this:
class Feedback < ActiveRecord::Base
belongs_to :user
belongs_to :host, :class_name => "User"
belongs_to :inquiry
belongs_to :room
before_validation :set_room_and_guest_id
def set_room_and_guest_id
self.room ||= self.inquiry.room if self.inquiry
self.user ||= self.inquiry.user if self.inquiry
end
I have spent a long time reading related posts about this today as well as the Rails documentation and have not been able to find a conclusion.
There's no "right" way here - just build the system you want. It sounds like a Buyer has Feedback about a Listing sold by a Seller, so it's completely reasonable to link all of those things together (or at least Amazon hasn't discovered a reason not to yet, and that's a pretty good sign).
The simplest "Rails way" you're probably looking for is to include all the relevant IDs in the feedback form. That does make them accessible to people who might want to fiddle, eg. by setting the seller/listing IDs to their own and leaving fraudulent positive feedback. You can combat that by merging #seller and #listing in the controller like you've mentioned, or with validations that the reviewer has bought the item, can't leave more than one, etc.
It also sounds like there might be room for a Transaction object here, representing the link between Buyer, Seller and Listing. Maybe the Feedback belongs on the Transaction.
Overall, I'd say you've been paralyzed by all the options available to you, and the best thing to do is pick one and run with it. When it's not clear which direction is best, that's often because you don't have all the information you need yet, but once you try something out you'll quickly learn more. Once you realize "Oh, I should have done this this other way", the beauty of code is you can change it.
I assume every listing has exactly one buyer and one seller, and there can be exactly zero or one feedbacks per listing. Then you might be confusing things a bit by linking the feedback directoy to buyer and seller. The normalized way would be to relate the feedback to the listing and nothing else. Then all questions go away.
(Note, you can then still get the buyer (or seller) directly from the feedback by using a :through rails relation.)
Maybe you are looking for Polymorfic Associations.
A slightly more advanced twist on associations is the polymorphic association. With polymorphic associations, a model can belong to more than one other model, on a single association.

Messaging Associations in Rails

I'm creating a Rails messaging application (using Twilio, which isn't super relevant, I suppose) which has the following models:
Contacts
Phone Numbers
Messages
Contacts has many Phone Numbers, easy enough.
Messages has many phone numbers, except it's not many, it's exactly two. A from and a to.
I'd like to be able to make calls such as:
#message.from.contact
Here are some approaches I've thought about:
I found the answer to this prior question. If this is the way to do, I'm not certain what the right migration would be to get the messages table to have the necessary columns. Do I need to migrate by both adding a column and a foreign key? Tried this and got stuck.
Another approach I thought of was to create a join table with a string attribute containing either "from" or "two." I assume then then I'd need to create my own Message.from method that searches for message_phone_numbers with "from" in some string? That feels unwieldy.
Or a third possible way... a FromPhoneNumber class and ToPhoneNumber class that has the join info.
I know I'm not posting any code. I've done some migrating and rolling back and am back to square one now...
Thanks for any help. I'm new to Stack Overflow and new to Rails, so your assistance will mean a lot!
Yes, your first bullet point is the correct answer. For your Message model have attributes from_id and to_id which both reference a particular phone number. I have inferred the names of your models so you may need to correct those but in summary:
Migration should look like:
add_column :messages, :from_id, :integer
add_column :messages, :to_id, :integer
Message class should look like:
belongs_to :from_phone_number, class_name: 'PhoneNumber'
belongs_to :to_phone_number, class_name: 'PhoneNumber'
PhoneNumber class should look like:
has_many :sent_messages, class_name: 'Message', foreign_key: 'from_id'
has_many :received_messages, class_name: 'Message', foreign_key: 'to_id'
You could stub out some methods in your message class to give you the short hand methods you are looking for:
def from
from_phone_number
end
In my view, just having from as an attribute on the model itself is not very descriptive and could mean the user or phone number.

Relationship advice/association logic in rails

I'm designing an application to create goals for a classroom - at the moment, the data relationships are modeled like this
I knew that out of inexperience I might run into problems and now I'm beginning to - specifically in my routes file - but also just for generally organizing the site and the data relationships.
When a user signs up, they add a student_group (or class), populate it with students and then add subjects. Later they add goals for each subject - although there should also be goals for a student_group, a student, or even the user. I was thinking of something like this - but would it be better as a has_many, through relationship?
Right now, I've only really done work on the User, Student_group, and Student models and these are fairly straight-forward. A user has many student_groups, and a student_group has many students. I'd like a second opinion before I proceed however, so that I don't have to end up going back and doing things over. Thanks!
I think you might be thinking too far ahead. Once you have your app built around your current data model, you'll know better whether you even want to expand it to include the concept of a goal that isn't part of a student's subject. If you decide that it is, then making goals belong_to a subject, student, or user will be pretty simple. At that point, you could also do something like
Class Student
has_many :personal_goals, class_name: "Goal"
has_many :goals, through: :subjects
def all_goals
goals + personal_goals
end
There's probably a more elegant way to model that last relationship. Would you need to go beyond that? Does it make sense to talk about a student group having a goal of its own? I don't know.
As I gone through your database design, I have found that you should have to use different type of relationships, that rails has provided us. I tried my best to design your schema as per my knowledge. you should define relationship in your model as I suggested below. Any good modification are highly appreciated.
User
has_many :student_groups
has_many :students, through: :student_groups
has_many :goals, as: :goalable
StudentGroup
belongs_to :user
has_many :students
has_many :goals, as: :goalable
Student
belongs_to :student_group
has_many :subjects
has_many :characteristics
has_many :goals, as: :goalable
Characteristic
belongs_to :student
Goal
belongs_to :goalable, polymorphic => true
I have defined some polymorphic associations in your schema. If you need any reference related to these association. visit http://guides.rubyonrails.org/association_basics.html
Hope it will help you. Thanks.

Should I use `has_and_belongs_to_many` or `has_many :through` in my model?

I have read the Choosing Between has_many :through and has_and_belongs_to_many on the Rails website, however I am a bit confused since I have a different case to the ones given on the website.
I have two models: Prop and CharacterCostume, and the character's costume can have multiple props associated to it, but a prop doesn't belong to that character and it can be used by any number of characters in the scene, too.
Right now I have has_and_belongs_to_many :props inside my CharacterCostume model, which does exactly what I want it to do: it fetches all the props associated with the costume using a table named character_costumes_props when I call CharacterCostume#props
However the association name is putting me off because of the "belongs to many" part. The costume does not belong to any of the props, so there's no has_and_belongs_to_many :character_costumes inside the Prop model.
I know that it can all function fine without it, but it got me thinking that maybe I should use a has_many :through association, but that requires me to create a superfluous model (it is superfluous, right?) and the model would look like this:
class CharacterCostumeProp < ActiveRecord::Base
belongs_to :character_costume
has_one :prop
end
Also, would has_one instead of belongs_to work here?
I want the code to be as semantic as possible, but I am not sure if this will increase the requirement for resources or decrease performance in some way, since there's an intermediate model.
Are there certain quirks/benefits attached to either approach? Is mine good enough? Or is my thinking completely wrong from what I need to do?
Thanks!
I think you want to use a :has_many, :through because you're going to want to work directly with the relation model - what scene(s), consumed or damaged, etc.
But, the reason it reads funny to you is that, for the most part, has_many and belongs_to don't really mean what they mean in English. What they really mean is "They have the foreign keys" and "I have the foreign key", respectively; the exception being the :dependent => :destroy behavior.
That still doesn't really help with has_and_belongs_to_many, since you're then saying, "They have the foreign keys and I have the foreign keys` - except that you can think of it sort of adding a new part both to "I" and "They" that happens to be the same part for each, and has those keys.
Does that help?
The single most important question you must ask yourself when deciding between HABTM and has_many :through is this:
Do I want to store any information specific to the association?
Example 1: magazine subscriptions
A many-to-many relationship between readers and magazines might conceivably be structured as a HABTM or a has_many :through. However, the latter makes far more sense in this case because we can easily think of information specific to the association that we might want to store.
A reader is related to a magazine through a subscription, and every subscription can be described by fields such as price, starting date, issue frequency and whether it's active or not.
Example 2: tags
The relationship between an existing Tag model and, say, an Article model is clearly of the many-to-many kind. The fact that one particular tag has been associated to any particular article must have no influence on whether the same tag will be able to be similarly associated to other articles in the future.
But, differently from the previous example, here the association itself is all the information we need. We just need to know which tags are associated to any given article. It doesn't matter when the association was formed. It doesn't matter how long it lasted.
It may matter to us how many articles a tag is associated with. But that information is stored in the Tag model since it's not specific to an association. There is even a Rails feature that takes care of that called counter_cache.
has_one wouldn't work, you'd need belongs_to
it is not superfluous if you have logic in your association model
has_and_belongs_to_many is good enough for you
See example below
class Student
has_and_belongs_to_many :courses
has_many :teachers, through: :courses
end
class Teacher
has_many :courses
has_many :students, through: :courses
end
class Course
has_and_belongs_to_many :students
belongs_to :teacher
def boring?
teacher.name == 'Boris Boring'
end
end
In the example above, I make use of both versions. See how Course would have its own logic? See how a class for CourseStudent might not? That's what it all comes down to. Well, to me it is. I use has_many through for as long as I can't give a proper name to my association model and/or the model doesn't need extra logic or behavior.

Rails: Many to many polymorphic relationships

See comments for updates.
I've been struggling to get a clear and straight-forward answer on this one, I'm hoping this time I'll get it! :D
I definitely have a lot to learn still with Rails, however I do understand the problem I'm facing and would really appreciate additional help.
I have a model called "Task".
I have an abstract model called "Target".
I would like to relate multiple instances of subclasses of Target to Task.
I am not using single table inheritance.
I would like to query the polymorphic relationship to return a mixed result set of subclasses of Target.
I would like to query individual instances of subclasses of Target to obtain tasks that they are in a relationship with.
So, I figure a polymorphic many to many relationship between Tasks and subclasses of Targets is in order.
In more detail, I will be able to do things like this in the console (and of course elsewhere):
task = Task.find(1)
task.targets
[...array of all the subclasses of Target here...]
But! Assuming models "Store", "Software", "Office", "Vehicle", which are all subclasses of "Target" exist, it would be nice to also traverse the relationship in the other direction:
store = Store.find(1)
store.tasks
[...array of all the Tasks this Store is related to...]
software = Software.find(18)
software.tasks
[...array of all the Tasks this Software is related to...]
The database tables implied by polymorphic relationships appears to be capable of doing this traversal, but I see some recurring themes in trying to find an answer which to me defeat the spirit of polymorphic relationships:
Using my example still, people appear to want to define Store, Software, Office, Vehicle in Task, which we can tell right away isn't a polymorphic relationship as it only returns one type of model.
Similar to the last point, people still want to define Store, Software, Office and Vehicle in Task in one way shape or form. The important bit here is that the relationship is blind to the subclassing. My polymorphs will initially only be interacted with as Targets, not as their individual subclass types. Defining each subclass in Task again starts to eat away at the purpose of the polymorphic relationship.
I see that a model for the join table might be in order, that seems somewhat correct to me except that it adds some complexity I assumed Rails would be willing to do away with. I plea inexperience on this one.
It seems to be a small hole in either rails functionality or the collective community knowledge. So hopefully stackoverflow can chronicle my search for the answer!
Thanks to everyone who help!
You can combine polymorphism and has_many :through to get a flexible mapping:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :targets, :through => :assignment
end
class Store < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
...And so forth.
Although the answer proposed by by SFEley is great, there a some flaws:
The retrieval of tasks from target (Store/Vehicle) works, but the backwards wont. That is basically because you can't traverse a :through association to a polymorphic data type because the SQL can't tell what table it's in.
Every model with a :through association need a direct association with the intermediate table
The :through Assignment association should be in plural
The :as statement wont work together with :through, you need to specify it first with the direct association needed with the intermediate table
With that in mind, my simplest solution would be:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :assignments
# acts as the the 'has_many targets' needed
def targets
assignments.map {|x| x.target}
end
end
class Store < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment
end
class Vehicle < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment, :as => :target
end
References:
http://blog.hasmanythrough.com/2006/4/3/polymorphic-through
The has_many_polymorphs solution you mention isn't that bad.
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => [:store, :software, :office, :vehicle]
end
Seems to do everything you want.
It provides the following methods:
to Task:
t = Task.first
t.targets # Mixed collection of all targets associated with task t
t.stores # Collection of stores associated with task t
t.softwares # same but for software
t.offices # same but for office
t.vehicles # same but for vehicles
to Software, Store, Office, Vehicle:
s = Software.first # works for any of the subtargets.
s.tasks # lists tasks associated with s
If I'm following the comments correctly, the only remaining problem is that you don't want to have to modify app/models/task.rb every time you create a new type of Subtarget. The Rails way seems to require you to modify two files to create a bidirectional association. has_many_polymorphs only requires you to change the Tasks file. Seems like a win to me. Or at least it would if you didn't have to edit the new Model file anyway.
There are a few ways around this, but they seem like way too much work to avoid changing one file every once in a while. But if you're that dead set against modifying Task yourself to add to the polymorphic relationship, here's my suggestion:
Keep a list of subtargets, I'm going to suggest in lib/subtargets formatted one entry per line that is essentially the table_name.underscore. (Capital letters have an underscore prefixed and then everything is made lowercase)
store
software
office
vehicle
Create config/initializers/subtargets.rb and fill it with this:
SubtargetList = File.open("#{RAILS_ROOT}/lib/subtargets").read.split.reject(&:match(/#/)).map(&:to_sym)
Next you're going to want to either create a custom generator or a new rake task. To generate your new subtarget and add the model name to the subtarget list file, defined above. You'll probably end up doing something bare bones that makes the change and passes the arguments to the standard generator.
Sorry, I don't really feel like walking you through that right now, but here are some resources
Finally replace the list in the has_many_polymorphs declaration with SubtargetList
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => SubtargetList
end
From this point on you could add a new subtarget with
$ script/generate subtarget_model home
And this will automatically update your polymorphic list once you reload your console or restart the production server.
As I said it's a lot of work to automatically update the subtargets list. However, if you do go this route you can tweak the custom generator ensure all the required parts of the subtarget model are there when you generate it.
Using STI:
class Task < ActiveRecord::Base
end
class StoreTask < Task
belongs_to :store, :foreign_key => "target_id"
end
class VehicleTask < Task
belongs_to :vehicle, :foreign_key => "target_id"
end
class Store < ActiveRecord::Base
has_many :tasks, :class_name => "StoreTask", :foreign_key => "target_id"
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :class_name => "VehicleTask", :foreign_key => "target_id"
end
In your databse you'll need:
Task type:string and Task target_id:integer
The advantage is that now you have a through model for each task type which can be specific.
See also STI and polymorphic model together
Cheers!
This may not be an especially helpful answer, but stated simply, I don't think there is an easy or automagic way to do this. At least, not as easy as with simpler to-one or to-many associations.
I think that creating an ActiveRecord model for the join table is the right way to approach the problem. A normal has_and_belongs_to_many relationship assumes a join between two specified tables, whereas in your case it sounds like you want to join between tasks and any one of stores, softwares, offices, or vehicles (by the way, is there a reason not to use STI here? It seems like it would help reduce complexity by limiting the number of tables you have). So in your case, the join table would also need to know the name of the Target subclass involved. Something like
create_table :targets_tasks do |t|
t.integer :target_id
t.string :target_type
t.integer :task_id
end
Then, in your Task class, your Target subclasses, and the TargetsTask class, you could set up has_many associations using the :through keyword as documented on the ActiveRecord::Associations::ClassMethods rdoc pages.
But still, that only gets you part of the way, because :through won't know to use the target_type field as the Target subclass name. For that, you might be able to write some custom select/finder SQL fragments, also documented in ActiveRecord::Associations::ClassMethods.
Hopefully this gets you moving in the right direction. If you find a complete solution, I'd love to see it!
I agree with the others I would go for a solution that uses a mixture of STI and delegation would be much easier to implement.
At the heart of your problem is where to store a record of all the subclasses of Target. ActiveRecord chooses the database via the STI model.
You could store them in a class variable in the Target and use the inherited callback to add new ones to it. Then you can dynamically generate the code you'll need from the contents of that array and leverage method_missing.
Have you pursued that brute force approach:
class Task
has_many :stores
has_many :softwares
has_many :offices
has_many :vehicles
def targets
stores + softwares + offices + vehicles
end
...
It may not be that elegant, but to be honest it's not that verbose, and there is nothing inherently inefficient about the code.

Resources