RESTful Cascading 2: The Rails Object Lifecycle
The Problem with Controllers
The last time this blog discussed cascading, I covered how create, update, and destroy instances of different classes together when they are connected. For 'create', we cascaded using the controller; for 'update', we touched our instances using the model; and for 'destroy', we told our database how our instances were connected so that it would destroy dependent items automatically through our migrations.
Despite using three different files to achieve our progress, in most apps, we could have placed the logic for all three updates in the model, all updates in the controller, or even in our database set-up. There may not be an official right answer to the problem, and maybe no wrong answer. But the closest to a bad decision is probably putting all the logic into our controller.
Why? Because the controller exists for one purpose: to interact with the view through the routes. The controller does not care about your seed file, nor does it give a fig about your Rails console commands. Recall our complex controller method that creates a field:
That's an awful lot of logic in a controller action. It also gets the job done as far as our user is concerned. But I'm not sure our developers would be pleased. The seed file would need to be gigantic to build a field, particularly one with lots of bed instances. The same with adding fields with the Rails console command line. We also would not really be able to write any tests for this functionality.
The solution is to move this logic into our models. That way, whether it's a user on the frontend or someone calling Field.create() in a seed file or the console, we should be able to produce a field with beds, each of which have two default stages.
Rails Object Lifecycle Callbacks
I covered the basics of the object lifecycle in the previous cascade blog. Objects are created, updated, and destroyed. (They are also saved and touched.) But a lot of the work Rails does is around those steps in the lifecycle. Validations, both those we specifically declare and ones the database and Rails framework require, are checked when we create or update.
Create, update, and destroy are just the main points in an object's lifecycle. Rails and Active Record allow us to run methods right before and after each of those lifecycle points. Active Record refers to these options as callbacks. A full list of the 19 available callbacks can be found in the Rails documentation.
The create method, besides being the most relevant to solving our controller problem, is a good example to walk through the lifecycle callbacks. For a simple create action (without any of the slugification and cascading creation of children that we have to deal with) will call the callback functions in the following order:
- before_validation
- Active Record runs through the model's validations. If validated, it continues with...
- after_validation
- before_save
- around_save
- before_create
- around_create: Requires a 'yield' call inside the block to actually go ahead with the process of creating the object.
- after_create
- after_save
- Active Record commits the object to our database or rolls it back due to some error.
- after_commit / after_rollback
As we will see below, Rails will be smart and move some of these around when it is appropriate to do so, but this is the basic order of callback functions. The 'save' callbacks will run on both create and update commands on the lifecycle, as will the validations. before_create, around_create, and after_create are replaced with their _update counterparts for the update command. The validate callbacks are also called when we check if an instance is valid with .valid?
Callback functions are placed at the top of the model alongside either a method or Proc we wish to run at that point of the object lifecycle.
With these callbacks added to our toolbox, we can start rebuilding our models with parts taken from our bloated controller file.
Putting Callbacks Into Practice
What do we want to happen when someone creates a field in Succotash? We need to create and associate a given number of beds based on the provided parameters. We also need to then create two stages for each of those beds. It is not in the controller file above, but we also want to create a slug based on the field's name and validate that the user does not have another field with that same slug. All of these considerations, because they are not limited to frontend issues but also have important database concerns, should be attached the model.
-
Create a number of beds based on the parameters - To be clear, most of the time when someone is playing with and testing our development code, they want to add as few parameters as possible, so we want to add a default value here. This will not have the affect of overwriting properly provided parameters (the "strong_params" in our controller). We can either add a default amount in a migration file to code it directly into our database, or, to avoid having to run another migration, we can add the attribute to our model.
That just leaves selecting the specific callback to use to create our beds during the field instance's create process. Since our bed needs to know to which field it belongs, we can only do so after our field has been created, so the after_create callback seems to fit the bill. The after_save callback does not work, because then Rails would also be creating beds on each update.
In code:
If a developer calls Field.create(user: someUserInstance) in the Rails console, a field will be created and exactly one bed will be created and associated with that field. The
-
Create exactly two stages for each bed - The process for creating the beds for the field can be directly applied to creating the stages for the beds, only this time, we do not even have to consider any parameters.
-
Create a slug for the field and validate it - Ruby's parameterize method can create the slug based on the field's name. The question is more about when and how we go about adding the slug as an attribute to our field.
Since validation occurs early on during the callback order, the field name needs to be slugified immediately prior to validation. That callback will also be called on updates to the field, which is necessary to keep the slug unique. The validation method should be scoped to the field's user_id. (The limited scope allows different users to have the same field/farm name.) The updated Field class in code:
With these methods in place, Succotash is no longer dependent on the frontend to manipulate data. Rails console commands, seed data, and RSpec tests will now be able to correctly produce the necessary children instances when a developer creates a new field.