Retrieve Associated Parent Attributes from Child Record in Rails
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.
child.parent.parent_attribute
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".
A Example
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_id
is:
@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.
Links
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) %>
Validations
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
Parent update
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