Handling Requests That Update Multiple Models in Ruby on Rails

“It’s called ‘Business Logic’ “

It starts simply enough. You have a many to many relationship, or even a one to many or one to one relationship; and you have to touch these models or tables all as part of the same transaction. Sometimes there just isn’t a great way to get to a fully normalized database without putting a more complex transaction on a frontend and middleware. Maybe you want to store some user information in a protected table – personally identifiable information, and / or passwords – and less regulated information like an association that a comment a user publicly wrote is associated to that user – in another table. Personally I like to try and minimize API calls as well.

In this example, I’m going to explain how updating relationships with pets in the pre-alpha version of my upcoming game The Guild of the Magi is a transaction that updates multiple tables.

I ended up going with a custom controller instead of placing this logic within the models as recommended in this stackoverflow article because I do not actually need complex update logic – I would like to use the builtin .update methods as intended – I just need to have access to one selected record in one table while I update the other.

For this example, all you need to know is that the active user information is used for creating and tracking sessions, the pet is selected separately and referenced in the API call by the Pet’s unique ID, and in addition to whatever vital statistics of the pet the user has influenced, that the user took action at all should be reflected in raising or lowering the “relationship” attribute of the user_pet_relationship table.

There is a visual representation of the app’s postgres database below:

postgres diagram

Frontend Setup

First, let’s set up our request on the frontend to structure the data so that it’s neatly sorted into the tables we want to store it in already.

// client/src/components/pet-api/PetActionForm.tsxlet postData = {
        "pet": {
            name: myPets[selectedPet].pet.name,
            hunger: hunger,
            attention: attention
        },
        "user_pet_relationship": {
            relationship: myPets[selectedPet].relationship
        }
    }

pet and user_pet_relationship are the two tables we would like to update. The structure is therefore a JSON that has nested objects in it. Separating these out into separate objects will make it way easier to access this data on the backend and route it appropriately. So we’re ready to go ahead and PATCH this data!

// client/src/components/pet-api/PetActionForm.tsxfunctionpostPetStats() {
        fetch(`/pet-action-relationship/${myPets[selectedPet].pet.id}`, {
            method: "PATCH",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(postData),
        }).then((response) => {
            if (response.ok) {
                response.json().then((newPetInfo) => {
                    console.log(newPetInfo)
                });
            }
            else {
                response.json().then((errorData) =>console.log(errorData.errors));
            }
        });
    }

Now let’s follow this data through the backend and take a look at how to handle this.

Backend: Routing

First, rails is going to interpret the endpoint we’ve PATCHed to. /pet-action-relationship/${myPets[selectedPet].pet.id} will translate to any /pet-action-relationship/:id route, so we can look at what route rails will use to accept the connection.

#config/routes.rb#custom routes
  get "/mypets", to:"pets#mypets"
  patch "/pet-action-relationship/:id", to:"combined_endpoint#pet_action_relationship_update"

PATCHes on /pet-action-relationship/:id route go to a combined_endpoint controller, and load the method /pet-action-relationship_update function. This is different from conventional routes which expect endpoints named according to models to have createshow, or similar actions come to conventionally named controllers. An example of a conventional route would be like this, which will return a response with all the pets:

#config/routes.rb

get "/pets", to:"pets#index"

Backend: Controller

Let’s take a closer look at combined_endpoint_controller to see what’s happening here, and compare it to the pets_controller.

#app/controllers/combined_endpoint_controller.rb# ...# PATCH/PUT /pet-action-relationship/1def pet_action_relationship_updateunless pet_action_form_params_for_relationship[:relationship].blank?
      @pet.update!(pet_action_form_params_for_pet)
      relationship = set_user_pet_relationship
      relationship_update = relationship.update!(relationship: pet_action_form_params_for_relationship[:relationship])
    end
    render json: {pet:@pet, relationship:@user_pet_relationship} 
end

private 

def pet_action_form_params_for_pet
    params.require(:pet).permit(:name, :hunger, :attention, :weight, :height, :species, :diet, :sprite)
  enddef pet_action_form_params_for_relationship
    params.require(:user_pet_relationship).permit(:relationship)
  enddef set_pet@pet = Pet.find(params[:id])
  enddef set_user_pet_relationship@user_pet_relationship = UserPetRelationship.find_by!(pet_id:@pet.id && user_id: session[:user_id])
  end

In order to perform all this work, we needed to define params for both of the objects in the JSON that the frontend sent to the backend. I also set up a callback to get the dependent record that finds the pet’s relationship with the logged in user.

With that out of the way, inside the pet_action_relationship_update method first we validate really quickly that we have all the data we need to make a complete transaction. If we don’t, it’s not happening. There are also validations on the relevant models to ensure that non-nullable fields have values.

Since not every pet has a relationship with every user, especially when pets first meet their owner’s friends, that field has to be nullable, which is why that validation exists here and not in the model.

But once we’ve checked our postdata and everything is fine, we can use the normal builtin .update method to update both of the records that represent the vital statistics changing (better fed or calmer mental health) and the improved relationship that comes with good stewardship.

The work on the database done, the controller also does not need complex serializing to return the following response to the frontend, so that the vital statistics can be updated in the player’s dashboard.

{"pet":{"name":"Rumi","hunger":0,"attention":10,"id":4,"weight":2.0,"height":8.0,"species":null,"diet":null,"sprite":4,"created_at":"2022-12-02T00:34:23.893Z","updated_at":"2022-12-06T16:13:34.396Z"},"relationship":{"relationship":9,"id":1,"user_id":1,"pet_id":4,"player_is_owner":true,"bio":null,"guestbook":null,"created_at":"2022-12-02T00:34:23.924Z","updated_at":"2022-12-06T19:25:51.888Z"}}

Thank you for reading. I’m excited to add more features to this game and officially start updating and publicizing this project. If you are interested in reading more or supporting me and the types of projects I am working on, please leave a comment below. You can also contact me on LinkedIn, and stay tuned for the upcoming crowdfunding campaign to get this project off the ground.

The total wordcount of this post is:

885

Related Posts

One thought on “Handling Requests That Update Multiple Models in Ruby on Rails

Comments are closed.