Triple Nested (and Deeper) Attributes: Ruby on Rails
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!