I'll file this one under "things everyone else probably already knew, but I just found out and it is awesome!" In a one-to-many (or one-to-one) relationship you can pull an associated parent attribute directly from the child record without an additional explicit lookup.
Meta-note: parent/child metaphors are not 100% apt for SQL and break down in a lot of examples. Below are "children" that all have a foreign key referencing their "parent".
You are creating two records: Meeting, which only has a date and MeetingPresentation, which will carry some other information, but isn't important for this example.
class Meeting < ApplicationRecord has_many :presentations, dependent: :destroy end
class MeetingPresentation < ApplicationRecord belongs_to :meeting end
Now, let's say you want to loop over all the presentations in an index view, but you also want to display the meeting date attached to them. Getting the date is super simple:
< @presentations.each do |presentation| %> <%= presentation.meeting.date %> <% end %>
A more complex example might be if you want to perform some kind of validation on the parent attribute before displaying. For example, assume that users are not allowed to edit a presentation on the meeting date or after. Using the validation module discussed here, you can selectively not show them the edit button like so*:
< @presentations.each do |presentation| %> <%= presentation.title %> <% if authorized?(:edit) && presentation.meeting.date.future? %> <%= link_to edit_presentation_path(presentation) %> <% end %> <% end %>
*obviously you want to lock this down in the model too with a validation, not showing the button is just a convenience
Why Was This Confusing
If you already knew this, I imagined you stopped reading by now, but if you're still here, I think it's worth asking, why wasn't this obvious to me (and maybe you). For me, I had viewed ActiveRecord one-to-many objects as linear associations which only flowed downward because all the manipulation that I was doing to them involved linear parent -> child modifications.
If you have two objects set up like the example above and you try to save
MeetingPresentation.new(title: 'title') it will error because you haven't told it what meeting it's attached to. You can manually pass in the
meeting_id as part of the hash, but more realistically, you will define a meeting and then build the relationship so that you don't need to know what that
@meeting = Meeting.find(params[:id]) @meeting.meeting_presentations.create(title: 'a title')
Additionally, when you want to manipulate an already created record, you most often call either the record itself or, if it's a collection of records, that collection from the parent. Though it's possible to edit a parent relationship from the child, it's probably not advisable to do so unless you have thought out your specific use case. For example, the following will work:
presentation = MeetingPresentation.last presentation.meeting.update(date: 5.days.from_now)
If there were 3 other presentations associated with that meeting, "their" dates (through the parent) have also been changed. Again, there may be a specific reason do make this change like this, but I think it should be carefully considered.
This was my mental model before. After closer examination though, this should make sense. Whether you are calling the parent or child, you are really just calling an
_id. In this frame of mind, it doesn't matter which "direction" things are going. In fact, the idea of directionality doesn't make sense (and neither does the parent child analogy, really).
How to make use of this behavior
Displaying information in the view
Displaying parent info, as in the above example, is useful if you have normalized your DB so that there is little duplicated information and a lot of ID references. Instead of doing a DB lookup in the view or declaring additional variables in the controller, you can just simply call and display the parent attribute from the child.
If you have nested resources you will need to pass in both the parent and child IDs when targeting a child record. Collections (like an index of child records), may or may not need to display parent records. Extending the example above, imagine we want to show all the presentations organized alphabetically regardless of meeting date and give users buttons to edit and delete them. Assume that the loop variable is
|presentation| and that presentations are the lower nested resource, you can call links as:
<%= link_to edit_meeting_presentation_path(presentation.meeting, presentation) %>
In the validation example I said you should lock down the model as well as not show the link. One way you can do that would be an ActiveRecord hook that makes use of the parent attribute:
class MeetingPresentation < ApplicationRecord validate :verify_valid_meeting_date, on: [:create, :update] private def verify_valid_meeting_date unless self.meeting.date.future? errors.add( :meeting_presentation, 'cannot be created on or after a meeting has taken place') end end end
Okay, yes I said this wasn't a good idea, but I thought I would lay out a hypothetical in which this might be what you want to do (it's admittedly a bit contrived). Let's keep with the meetings and presentations example. Users give many presentations and some of those presentations may be classified. They are also the only ones who are allowed to modify the presentations which they own (but not the meeting directly). For some record keeping authority, you need to classify any meeting which contains classified presentations as such.
At this point you can just loop through all the children to see if any presentations are classified and return a true/false on whether the meeting is classified, however users may mark their presentations declassified at a later date and the record keeping authority requires that any meeting which ever contained any classified presentations remain categorized as such (I told you it was a bit contrived, though I have seen some business requirements that are worse).
Now we set Meeting to have a
has_classified boolean that defaults to false and update that any time a user submits a presentation with a boolean of
is_classified equal to true.
class MeetingPresentation < ApplicationRecord after_commit :mark_meeting_classified private def mark_meeting_classified if self.is_classified self.meeting.update(has_classified: true) unless self.meeting.has_classified end end end