Nested Attributes: Ruby on Rails

Logan McGuire
3 min readAug 20, 2020
Series of nesting dolls.

Relationships can be very messy. Ideally they they are clean and make us happy, but rarely does that actually come to pass. Especially when you have a single table that acts as a lynchpin in your data structure, Rails has a solution for you: nested attributes. This relationship is remarkably easy to implement, once you know where to put the various pieces. So let’s get to unifying our tables with nested relationships!

First, we’ll need a project with two models. In this case we’ll go ahead and use Rails to get us up and running quickly. Once your file is started, go ahead and run the following in your project directory terminal:

rails g scaffold User first_name last_name

Followed by:

rails g model Address street city state zip:integer user:reference
rails db:migrate

This will build out a User with all of our routes, controller, and model, and a model for our Address. Next we’ll go into the model file for both of these, starting with User.

class User < ApplicationRecord
has_one :address, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true
end

This sets up our User class to be able to do full CRUD with address. Next inside of Address model:

class AbilityScore < ApplicationRecord
belongs_to :character
end

Now we just need to head over to users_controller.rb and finish up the last of the magic. With scaffold you should already have all your CRUD methods and strong params established, so now we’ll just add our relationship to the Address class. Anywhere where you are rendering back the json of User, add an includes for Address.

# GET /users
def index
@users = User.all
render json: @users, include: [:address]
end
# GET /users/1
def show
render json: @user, include: [:address]
end
# POST /users
def create
@user = User.new(user_params)
if @user.save
render json: @user, include: [:address], status: :created,
location: @user
else
render json: @user.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /users/1
def update
if @user.update(user_params)
render json: @user, include: [:address]
else
render json: @user.errors, status: :unprocessable_entity
end
end

And finally under the private heading for our user_params we’ll need to allow address attributes.

# Only allow a trusted parameter "white list" through.
def character_params
params.require(:character).permit(
:first_name, :last_name,
address_attributes: [:id, :street, :city, :state, :zip,
:user_id]
)
end

At this point you can use a service like Postman to do full CRUD actions with only User calls. However, the includes sends the address key, not the address_attributes key. So the strong params will not allow the data back until that key has been mutated. The following code would work for a POST request, creating both a User and an associated address. (Formatted for Postman)

{
“first_name”: “Montague”,
“last_name”: “Guyari”,
“address_attributes”: {
“street”: “234 Somewhere Lane”,
“city”: “Neverland”,
“state”: “Nevada”,
“zip”: 93347
}
}

And this would work for an PATCH request:

{
"id": 3,
"first_name": "Montague",
"last_name": "Guyari",
"address_attributes": {
"id": 5,
"street": "234 Somewhere Lane",
"city": "Neverland",
"state": "Nevada",
"zip": 93347,
"user_id": 3
}
}

And this would be the result of a GET request:

{
"id": 3,
"first_name": "Montague",
"last_name": "Guyari",
"address": {
"id": 5,
"street": "234 Somewhere Lane",
"city": "Neverland",
"state": "Nevada",
"zip": 93347,
"user_id": 3
}
}

If you were hoping to unify your data using nested attributes I hope this helped set you on the path to functional code! This is a great way of keeping your data organized and could even help make less requests for data in the long run. While it does have some quirks, namely needing to add _attributes to your nested return keys, overall the implementation is not hard!

--

--

Logan McGuire

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