The other day I was re-exploring Rails documentation to give me a good refresh on what I already knew and what I forgot or what I slept on. There are so much little things that can get out of your vision when you are under production constraints, that having a little step back from time to time can offer you marvels to the mind and skill.
Lost in re-reading from the basics to the details, I fall back onto one of the core aspect of Rails : its callbacks. It's when I realized that there's a lot of them that I don't meet really often within the code bases I get the grasp on.
One especially feels the shy one : around_action, one of the AbstractController::Callbacks.
Some terminology before we start
Interpretation is an amazing spell the brain has ... but it comes with its defects. Let's ensure we speak the same language :
- An action is a controller method that gets called through a route of the application. create, show, edit, update, index, destroy are the common RESTful conventions.
- What I will call a behavior is anything that produces any kind of reading or interact with the application. It will most commonly be methods of any form.
Refreshing the basics
You certainly know about before, after actions - certainly the most commonly used callback types. Let's just have a small refresher to get us :
class NutmegsController < ApplicationController
before_action :set_nutmeg, except: %i[create index]
before_action :grate, except: %i[create index destroy]
after_action :dispatch_gatherer, only: :create
end
before_action produces behaviors right before the action, in the order you pass them to the controller.
So when it can both set_nutmeg and grate it will first set_nutmeg then grate it.
after_action produces behaviors right after the action if it is successful.
It's an adequate time to send the gatherer used to get the nutmeg to the central as an example.
Those are rather explicit about the moment they are executing the behaviors.
So what does around_action ?
Yeah ... that's a good question.
If "before" and "after" are crystal clear about the process timeline, "around" can get a bit too exposed to appreciation.
In short : around_action is expecting to receive behaviors that will do something during the action. It will yield the action process for the behavior. (a little yield refresher if need be)
The previous sentence is essential to understand what we can actually do with around_action.
Let's continue with our nutmegs exploitation :
class NutmegsController < ApplicationController
before_action :set_nutmeg, except: %i[create index]
before_action :grate, except: %i[create index destroy]
around_action :dry_air, except: %i[create destroy]
after_action :dispatch_gatherer, only: :create
private
def dry_air
raise UndryableRoomException unless get_air.dry
yield
end
end
Here we want to make sure that the air is dry while we show or most of the time we interact with the nutmegs. It will first try to dry the air on any non filtered actions - no need for it while we gather or destroy them - if it can't it will stop the action and alert the controller, elseway it will resume the action as expected.
Rails gives a good example
The Rails documentation is always a good start for anything and has a lot of very good examples about its usage. And it doesn't fall short on this one :
class ChangesController < ApplicationController
around_action :wrap_in_transaction, only: :show
private
def wrap_in_transaction
ActiveRecord::Base.transaction do
begin
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
Here what it does is something that could be a great usage of this callback : wrapping the action within a transaction to ensure any interaction with the database that will occur during its process. If all succeeds : it will resume the action as expected, if anything fails : we make sure that any change is rollback.
This can become real handy if we need to ensure multiple interactions within this one action.
Take this case with a grain of salt though : it can underlie a potential code smell (cf : Single responsibility principle in SOLID). But sometimes it's the adequate solution.
Going modular
To give you more insight about the mater and a more adequate way to scale your code :
A potential way to reuse the bit of code Rails provide us and clear up the code in the same code : use a Concern :
# app/controllers/concerns/transactionable.rb
module Transactionable
extend ActiveSupport::Concern
private
def wrap_in_transaction
ActiveRecord::Base.transaction do
begin
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
It will give us the possibility to include the behavior within any controller we need - and potentially extend Transactionable later :
# app/controllers/some_controller.rb
class SomeController < ApplicationController
include Transactionable
around_action :wrap_in_transaction, only: %i[create update]
end
Conclusion
around_action may be a less used callback, but as for lots of things within Rails : it can sometimes resolve some of your niche problems with precision.
And don't forget to dive back within the Rails documentation without intent from time to time when you feel so ... it's a great pleasure to re-enact your knowledge on some specific use cases.
That's it for today !
Hope this little refresher gave you some food for thought. At least it was a pleasure typing it.
Don't hesitate to give your insights and personal examples in the comments : they will be appreciated.
Enjoy your skills and have great day !
Top comments (0)