“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:
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 PATCH
ed 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"
PATCH
es 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 create
, show
, 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
One thought on “Handling Requests That Update Multiple Models in Ruby on Rails”
Comments are closed.