Peeling Back the Layers of Nested Attributes
clheard54
Posted on March 2, 2020
Last week, after slogging through a mess of repetitive Sinatra code, I finally dove into Rails (oh the magic of rails g scaffold!). ActiveRecord worked behind the scenes to build out many-to-many relationships with ease. Rails was smart enough to create models, migrations, and views for me. The magic, my friends, is real.
But I've always been the person who isn't quite satisfied after a magic show. I can't just be awed and okay with it. I feel a compulsion to know the secret. To figure out what the trick is. To pull back the curtain and reveal what's hiding behind it. So when I came across something like this:
...I skidded to an abrupt halt. "Accepts_nested_attributes_for"? And suddenly these relationships I'd been painstakingly crafting just appeared out of the void?! The Ruby helper seemed entirely opaque. So I had to unpack it fully, to dive into nested attributes - and their corresponding nested hashes. What follows is my attempt to unveil the mysteries of this method.
SO WHEN DO WE RUN INTO NESTED ATTRIBUTES?
Suppose we’re working with a model that represents the relationships between breweries and their beers. A brewery has many beers; a beer belongs to a specific brewery.
A beer can hold a foreign key for its brewery, and that will be good enough to represent this has-many-belongs-to relationship. So far, so good. But a brewery will have several beers... and each beer has its own attributes. This is the nesting.
SO... HOW DOES NESTING AFFECT US?
This is where “accepts_nested_attributes” comes in. In short, this helper method provides a super handy-dandy shortcut: It allows us to tell a brewery the properties of its beers AND create those beers at the same time. Magic! In particular, “accepts_nested_attributes” comes in handy when we start fiddling with writing forms.
If we're adhering to the convention of RESTful Ruby routes, we probably need a "new" method that will allow us to create either a new beer or brewery. ActiveRecord is smart enough to create associations between objects (with very little work from us) -- but those objects need to exist before they can become related. What if we want to create a new brewery and immediately associate it with some beers -- but those beers don’t exist in our database yet either? How can we create both a brewery and its beers simultaneously?
Enter "accepts_nested_attributes"! Let’s consider the case that we want to create a new brewery (Beer-o-mania?!) that makes 3 beers. Ideally, we’d like the “Create a New Brewery” form to take in data not only about the brewery's name and location, but also about each of its three beers.
Those beers would be collected in an array -- but each beer would be its own hash, complete with its own name, style, and ABV keys. This means that our params would ideally look something like this:
Enter “Nested Attributes." As the above image shows, within the brewery hash, there is this array of beers -- and each beer has its own set of attributes. Now we just need to figure out how to build this structure from our form.
Remember, our goal is to enter info into a form in order to save a new brewery to our database. In order to assign attributes to an object, though, we need a writer method (think back to Object-Oriented programming and the necessity of attr_readers and attr_writers). Our ability to build a custom brewery depends on the existence of a writer method for the brewery's name, location -- and its beers. If we're trying to assign attributes within the beers, that will require a further writer method. Something like this:
The name “beers=” is already taken. So we resort to beers_attributes= as our method name. (And we can begin to see where "accepts_nested_attributes_for :beer" might come from!) Let’s head over to our Brewery Model to define this method.
RUBY'S BEHIND-THE-SCENES SLEIGHT OF HAND
Okay, here’s the thing. Remember our beer_attributes array from above? Well. Ruby takes that array and decides to secretly turn it into a HASH (with keys that are kind of like primary ids). So we have to keep the hash structure in mind as we try to transform each entry into a fully-fledged Ruby object. If you take a look below, it's clear that we are now only interested in the values of the Ruby hash.
Each of those values tells us all we could ever wish to know about a specific beer. All that’s left is to create an object from that information, and associate it with the Brewery. Guess what? Ruby provides another helper for us. The “build” method will do precisely this.
The ‘build’ method works, on the most basic level, almost entirely like the ‘new’ method. What distinguishes it is one extra feature, and that’s what makes it important to us here. ‘Build’ automatically associates our newly created object with the object it's called on. Whereas Beer.new will be instantiated with no brewery_id/foreign key assignment, Brewery.beers.build will instantiate a new beer that already has its brewery_id set.
So within the instance method “beers_attributes=”, we can call build on the brewery we’re currently working with, and ask build to create a beer in self.beers. A new beer can thus be created from 1) a set of attributes, that is 2) nested within the brewery hash, which is itself 3) located in our params hash.
To recap, right now we have a Brewery Model that looks like this:
And a New form within our Brewery Views that still needs a bit of work:
Building out the rest of this form really just means providing the fields needed to gather all the information about any beers that we’d like to create within a brewery. We can use the form helper "fields_for" to accomplish this.
TYING IT TOGETHER IN OUR MVC MODEL
Almost there! We’ve now set up our Model and our View successfully. The only thing to finalize is the Controller. There are two big things to remember here: One is that we’re eventually trying to create a Brewery based on the information stored in params. And what have we just done? Given params the ability to take in data (or nested attributes) on beers created by the brewery. We also wrote a writer method so that we could assign values to the beer attribute keys. Does our Controller know any of this yet, though? NO! It has no idea we actually want this assignment to be happening. And right now, our strong params are withholding from us the permission to make these changes. So :beer_attributes must be added to our strong params permissions -- and we should probably specify which beer attributes we want to give a user access to edit or update.
Last thing’s last. Let’s make sure that when we’re creating a Brewery, we give it a heads-up that it might be born with some beers alongside it. Writing Brewery.new is not enough inside the 'new' controller method anymore. We must instantiate it with some beer fields (let's say three) so that the data gathered in our form fields actually has somewhere to go and live...
And voila! Here's what our form looks like, and what it ultimately creates. Not bad at all.
Posted on March 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.