Triple Nested (and Deeper) Attributes: Ruby on Rails

Logan McGuire
5 min readSep 25, 2020

--

Photo by Michel Stockman on Unsplash

This is a follow up to a previous article of mine, explaining setting up Nested Attributes. There have been times where I haven’t wanted to just have one parent and one child table. What do we do if we want to have a grandparent, parent, and child? To answer this question, we’ll be diving into Ruby on Rails focusing on the include method, and what that means for our nested attributes.

Alright, so fast forward through some of the initial setup, in your Rails directory, we’ll set up our three tables. In this instance we don’t need to access Houses or Rooms without User, so scaffold and two models!

rails g scaffold User first_name last_name
rails g model House address user:references
rails g model Room name house:references
rails db:migrate

Alright so now our basic tables have been made. I went through and seeded some Users, Houses, and Rooms. Now we just need to finish setting up our models before we get into the controller actions. Please note the following happens inside three different files, so watch those comments!

# user.rb, model file
class User < ApplicationRecord
has_many :houses, dependent: :destroy
accepts_nested_attributes_for :houses, allow_destroy: true
end
# house.rb, model file
class House < ApplicationRecord
belongs_to :user
has_many :rooms, dependent: :destroy
accepts_nested_attributes_for :rooms, allow_destroy: true
end
# room.rb, model file
class Room < ApplicationRecord
belongs_to :house
end

I know, I know, many houses, I wanted to do more than a has_one relationship so let’s just gloss over owning multiple houses. Now we move into the fun stuff for getting our tables to render! Likely you’ve used the include syntax before, like so:

# GET /users
def index
@users = User.all
render json: @users, include: [:houses]
end
# OR
# GET /users
def index
@users = User.all
render json: @users, include: [:houses, :rooms]
end

The first option only gets the houses, and the second doesn’t associate the houses with their respective rooms (also the second doesn’t currently work, because we didn’t set up any direct relationships between Users and Rooms). So a Postman request for the above two options would look like:

{
"id": 1,
"first_name": "billy",
"last_name": "bob",
"created_at": "2020-09-24T21:43:01.682Z",
"updated_at": "2020-09-24T21:43:01.682Z",
"houses": [
{
"id": 3,
"address": "404 Place",
"user_id": 1,
"created_at": "2020-09-24T21:43:01.698Z",
"updated_at": "2020-09-24T21:43:01.698Z"
}
]
}
{
"id": 1,
"first_name": "billy",
"last_name": "bob",
"created_at": "2020-09-24T21:43:01.682Z",
"updated_at": "2020-09-24T21:43:01.682Z",
"houses": [
{
"id": 3,
"address": "404 Place",
"user_id": 1,
"created_at": "2020-09-24T21:43:01.698Z",
"updated_at": "2020-09-24T21:43:01.698Z"
}
]
"rooms": [
{
"id": 3,
"name": "Kitchen",
"house_id": 3,
"created_at": "2020-09-24T21:43:01.709Z",
"updated_at": "2020-09-24T21:43:01.709Z"
}
]
}

So that sets up Users => Houses, or Users => Houses, Rooms. What we really want is Users => Houses => Rooms. We do this by altering the usual syntax for include. Using the second example of actually having rooms, we transition to nesting by:

# GET /users
def index
@users = User.all
render json: @users, include: { houses: { include: :rooms } }
end

I put the include on my index, show, create, and update methods. If you want to have additional buried relationships, then you reference the table name as a key rather than as a symbol. In this case :houses becomes houses:. You then just continue to nest by rotating in the include key word. So hypothetically you could:

# GET /users
def index
@users = User.all
render json: @users, include: {
houses: {
include: {
rooms: {
include: {
appliances: {
:accessories
},
furniture: {
include: [:accents, :care_products]
}
}
}
}
}
}
end

In the above example, all of the words (exempting include) are tables. By using include we specify that we want a key of the table name to represent going deeper and deeper into our JSON object. By doing this we can have the relationships persist the JSON transformation in a neat and organized way. Also, if you need to have more code following the include, you can put parentheses after your render to keep everything happy.

# pulled from the POST /users method
if @user.save
render(
json: @user,
include: { houses: { include: :rooms } },
status: :created,
location: @user
)

Now we just have to get our strong params in order and we’ll have a triple nested attribute ready to go. We set up our nested attributes in the models already. So long as we white list the entire received object, the information will cascade through our relationships. You’ll notice in this article our attribute will be plural (houses_attributes rather than house_ attributes) unlike the previous article.

def user_params
params.require(:user).permit(
:first_name, :last_name,
houses_attributes: [
:id, :address, :user_id,
rooms_attributes: [
:id, :name, :house_id
]
]
)
end

Now to show what we should expect! From a single User GET request we should see something like this in Postman:

{
"id": 2,
"first_name": "katie",
"last_name": "kane",
"created_at": "2020-09-24T21:43:01.686Z",
"updated_at": "2020-09-24T21:43:01.686Z",
"houses": [
{
"id": 4,
"address": "1234 Strongpass",
"user_id": 2,
"created_at": "2020-09-24T21:43:01.704Z",
"updated_at": "2020-09-24T21:43:01.704Z",
"rooms": [
{
"id": 6,
"name": "Enclosed Porch",
"house_id": 4,
"created_at": "2020-09-24T21:43:01.725Z",
"updated_at": "2020-09-24T21:43:01.725Z"
},
{
"id": 7,
"name": "Breakfast Nook",
"house_id": 4,
"created_at": "2020-09-24T21:43:01.747Z",
"updated_at": "2020-09-24T21:43:01.747Z"
},
{
"id": 8,
"name": "Office",
"house_id": 4,
"created_at": "2020-09-24T21:43:01.752Z",
"updated_at": "2020-09-24T21:43:01.752Z"
}
]
}
]
}

And if we format a POST request to User through Postman:

{
"user": {
"first_name": "Montague",
"last_name": "Guyari",
"houses_attributes": [
{
"address": "234 Somewhere Lane",
"rooms_attributes": [
{
"name": "Reading Nook"
}
]
}
]
}
}

We’ll get back (still in Postman):

{
"id": 3,
"first_name": "Montague",
"last_name": "Guyari",
"created_at": "2020-09-25T21:13:44.927Z",
"updated_at": "2020-09-25T21:13:44.927Z",
"houses": [
{
"id": 6,
"address": "234 Somewhere Lane",
"user_id": 3,
"created_at": "2020-09-25T21:13:44.980Z",
"updated_at": "2020-09-25T21:13:44.980Z",
"rooms": [
{
"id": 10,
"name": "Reading Nook",
"house_id": 6,
"created_at": "2020-09-25T21:13:45.000Z",
"updated_at": "2020-09-25T21:13:45.000Z"
}
]
}
]
}

And there you have it! You can request deeply nested attributes, and you can send them back. I hope this proves useful in your Ruby on Rails applications!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Logan McGuire
Logan McGuire

Written by Logan McGuire

A creator to the core, he enjoys all games (especially collaborative ones), baking bread, and software development.

No responses yet

Write a response