Rails "around_action" : a less used callback
Tags: rails, ruby, codereview, beginners
Published: 2023-09-22 12:31:21

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](https://api.rubyonrails.org/classes/AbstractController/Callbacks.html).

## 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  :

```rb
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](https://guides.rubyonrails.org/layouts_and_rendering.html#understanding-yield))
The previous sentence is essential to understand what we can actually do with **around_action**.

Let's continue with our nutmegs exploitation :

```rb
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](https://guides.rubyonrails.org/action_controller_overview.html#after-filters-and-around-filters) :

```rb
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** :

```rb
# 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 :

```rb
# 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 !**