RESTful Cascading in Rails
Why Cascade?
One of the primary benefits of using REST architecture in designing our APIs is that it helps us keep our server-side code simple. It is not difficult to imagine a very simple application where creating, editing, and deleting an instance of a class does not alter any other instances. Put another way, we are imagining an app with no relationships between models. However, the fact is, very few practical object-oriented apps above the most simplistic lack join tables and relationships.
In Rails, we can set instances' relationships to other classes' instances in our models. Those keywords that Rails developers have used so often -- 'belongs_to', 'has_many', and 'has_one' -- appear at the top of most model classes.
In this blog, I am going to be focusing on the one-to-many relationship. These relationships need to be carefully coded for all potentially transformative HTTP verbs: POST, PATCH/PUT, DELETE. Instances on the many side of the one-to-many are dependent upon the one side. So, if something happens to the one, we may want to also touch on the many. Likewise, if something happens to one of our many-side instances, we may want to make an update to our one. While we could effectively code any edit into the controller actions, Rails provides a few tools to help us out with cascading updates and deletions.
Create
Update: The section below describes how to RESTfully cascade on create in a controller method. This works fine for view/frontend-only interaction. For RESTfully cascading in all situations, coding these cascades in the model is the superior option.
Let's say we are building an app like Succotash. When a user creates a new field, we want to build out the associated subunits for that field, and then also a few initial stages (time periods) for the app. In this case, the field has many subunits (beds), which in turn have many stages. We need to cascade these creations and associate them all together, while only really receiving data for our top-level from the front-end.
The code below shows how we can create so many instances from just one request. Notice, however, that the beds and stages all are provided with what are essentially default values. (This code is a simplified version of what is used in the live app.)
Rather than having to write a few chained methods on our front-end that cycle through three or more request/response cycles, we have only hit our backend once and managed to create all the relationships in one controller method.
While we could also use a similar pattern to cascade our updates and deletions, Rails provides us with some tools to simplify the process.
Update
The controller is also the place you want to make any updates to different instances using the same pattern demonstrated above. But what if you just want to cascade your updated_at times? Rails and ActiveRecord provides a neat little keyword that can cascade our updated_at (or other) dates.
Thinking again about Succotash, it is indeed possible that a user could update Stages and Beds without ever actually touching our Field instance. But to our user, they think they are updating the field when they update its children. Considering our profile page sorts fields by their updated_at time, we definitely want to update the field.
There are two ways to do this: in the controller and in the model. Both use the same keyword: touch. To do so in the controller:
By calling the touch method on an instance, it updates the field provided to the present time. It does not need to be our updated_at field either. We could 'touch' any field where the schema allows a datetime and update it to the present time by passing that field's name as an argument to touch.
My preferred method of updating the updated_at field, however, is to provide the touch method to my models. This fix eliminates the need to add the extra code to the controller.
The 'touches' in the models will cascade upward to the ultimate 'one' in our one-to-many relationships. When we create or update a stage, it will automatically update the updated_at column for its bed, which, having been updated, will touch and update the updated_at column for its field.
Touch in models only works in one direction: from child to parent (many to one). That is sensible for an app like Succotash. Touch in a controller, on the other hand, does not even need a relationship to exist. Your controller can 'touch' any instance and update its updated_at field, even when there is no direct relationship.
As a bonus feature, if you have enabled caching in Rails, touch will also automatically expire the cache of dependent objects. This feature is particularly helpful if you have an ERB app that uses a Russian Doll ERB setup.
Destroy
The final and, probably most important transformative method to code correctly is our destroy/delete method. In the models we have been using as examples in this blog post, our stages requires the existence of a bed and our beds require the existence of a field. Without setting our models or database up correctly, we would need to code all these deletions into our controller. That is not impossible, by any means, but it could be time consuming -- either by designing nested loops or writing very complex code or a combination thereof.
There are a few different ways to set up cascading deletions in Rails. However, I am very much of the opinion that using database-related functionality with Rails is preferable to using only Rails. See my blog on PostgreSQL enums for another example.
I like to set up cascade deletions in migration files for that reason. Adding {on_delete: cascade} to our foreign keys will tell Rails to write SQL that effectively tells the table to cascade our deletions. If you are adding this feature to an already built table, the code will look something like:
Or, if you have planned ahead and are setting up cascade deletion in your initial table migration with Rail's t.reference syntax, you could set up cascade deletions in the following syntax:
These small pieces of code will help you avoid nasty errors in your Rails and database set-up. Cascade destruction of instances is extremely important in keeping complex apps from breaking, as well as from having dated useless rows in your databases.