Inline `after_commit` Callbacks in Rails - ruby-on-rails

I've got a sidekiq job that needs to be run after the commit, but only in some situations and not all, in order to avoid a common race condition.
For example, the below after_commit will always fire but the code inside will only execute if the flag is true (previously set in the verify method).
class User < ActiveRecord::Base
...
after_commit do |user|
if #enqueue_some_job
SomeJob.new(user).enqueue
#enqueue_some_job = nil
end
end
def verify
#enqueue_some_job = ...
...
save!
end
end
The code is a bit ugly. I'd much rather be able to somehow wrap the callback inline like this:
class User < ActiveRecord::Base
def verify
if ...
run_after_commit do |user|
SomeJob.new(user).enqueue
end
end
...
save!
end
end
Does anything built into Rails exist to support a syntax like this (that doesn't rely on setting a temporary instance variable)? Or do any libraries exist that extend Rails to add a syntax like this?

Found a solution using a via a concern. The snippet gets reused enough that it is probably a better option to abstract the instance variable and form a reusable pattern. It doesn't handle returns (not sure which are supported via after_commit since no transaction is present to roll back.
app/models/concerns/callbackable.rb
module Callbackable
extend ActiveSupport::Concern
included do
after_commit do |resource|
if #_execute_after_commit
#_execute_after_commit.each do |callback|
callback.call(resource)
end
#_execute_after_commit = nil
end
end
end
def execute_after_commit(&callback)
if callback
#_execute_after_commit ||= []
#_execute_after_commit << callback
end
end
end
app/models/user.rb
class User < ActiveRecord::Base
include Callbackable
def verify
if ...
execute_after_commit do |user|
SomeJob.new(user).enqueue
end
end
...
save!
end
end

You can use a method name instead of a block when declaring callbacks:
class User < ActiveRecord::Base
after_commit :do_something!
def do_something!
end
end
To set a condition on the callback you can use the if and unless options. Note that these are just hash options - not keywords.
You can use a method name or a lambda:
class User < ActiveRecord::Base
after_commit :do_something!, if: -> { self.some_value > 2 }
after_commit :do_something!, unless: :something?
def do_something!
end
def something?
true || false
end
end

Assuming that you need to verify a user after create.
after_commit :run_sidekiq_job, on: :create
after_commit :run_sidekiq_job, on: [:create, :update] // if you want on update as well.
This will ensure that your job will run only after a commit to db.
Then define your job that has to be performed.
def run_sidekiq_job
---------------
---------------
end
Hope it helps you :)

Related

Why does the presence of after_create_commit make my after_update_commit never fire?

I'm seeing a scenario where i have something like this:
class User
def thing; puts "hello"; end
after_update_commit :thing
after_create_commit :thing
end
the after_update_commit never fires when doing user.update first_name: rand
but if i comment out after_create_commit, it does work.
Whichever one is declared last is the one that wins.
seems to only be for _commit callbacks
only happens with multiple callbacks for same method
Is this a Rails bug or is there a reason for this?
rails 6.1.4.6
https://guides.rubyonrails.org/active_record_callbacks.html#transaction-callbacks
Using both after_create_commit and after_update_commit with the same method name will only allow the last callback defined to take effect, as they both internally alias to after_commit which overrides previously defined callbacks with the same method name.
solution if don't have conditions:
after_commit :thing, on: [:update, :create]
solution if do have condition (in this case, is_active? on update)
after_commit :thing, on: [:update, :create]
def thing
# id_previously_changed? is only true when record is created
unless id_previously_changed?
return unless is_active?
end
# ...
end
As it was said in John Bachir answer you may have different conditions for create and for update actions (and these conditions may be complicated). I ended up with:
class User
def thing; puts "hello"; end
after_commit :thing_on_create, on: :create, if: :create_condition?
after_commit :thing_on_update, on: :update, if: :update_condition?
alias_method :thing_on_create, :thing
alias_method :thing_on_update, :thing
def create_condition?
Time.now.monday?
end
def update_condition?
active?
end
end
It is a little bit more readable (and does not affect the method itself) IMO.

Run block defined on class within instance's scope

I would like to create something similar to ActiveRecord validation: before_validate do ... end. I am not sure how could I reference attributes of class instance from the block given. Any idea?
class Something
attr_accessor :x
def self.before_validate(&block)
#before_validate_block = block
end
before_validate do
self.x.downcase
end
def validate!
# how should this method look like?
# I would like that block would be able to access instance attributes
end
end
#3limin4t0r's answer covers mimicing the behavior in plain ruby very well. But if your are working in Rails you don't need to reinvent the wheel just because you're not using ActiveRecord.
You can use ActiveModel::Callbacks to define callbacks in any plain old ruby object:
class Something
extend ActiveModel::Callbacks
define_model_callbacks :validate, scope: :name
before_validate do
self.x.downcase
end
def validate!
run_callbacks :validate do
# do validations here
end
end
end
Featurewise it blows the socks off any of the answers you'll get here. It lets define callbacks before, after and around the event and handles multiple callbacks per event.
If validations are what you really are after though you can just include ActiveModel::Validations which gives you all the validations except of course validates_uniqueness_of which is defined by ActiveRecord.
ActiveModel::Model includes all the modules that make up the rails models API and is a good choice if your are declaring a virtual model.
This can be achieved by using instance_eval or instance_exec.
class Something
attr_accessor :x
# You need a way to retrieve the block when working with the
# instance of the class. So I've changed the method so it
# returns the +#before_validate_block+ when no block is given.
# You could also add a new method to do this.
def self.before_validate(&block)
if block
#before_validate_block = block
else
#before_validate_block
end
end
before_validate do
self.x.downcase
end
def validate!
block = self.class.before_validate # retrieve the block
instance_eval(&block) # execute it in instance context
end
end
How about this?
class Something
attr_accessor :x
class << self
attr_reader :before_validate_blocks
def before_validate(&block)
#before_validate_blocks ||= []
#before_validate_blocks << block
end
end
def validate!
blocks = self.class.before_validate_blocks
blocks.each {|b| instance_eval(&b)}
end
end
Something.before_validate do
puts x.downcase
end
Something.before_validate do
puts x.size
end
something = Something.new
something.x = 'FOO'
something.validate! # => "foo\n3\n"
This version allows us to define multiple validations.

after_commit is never being called

I am on rails 4.2.10. I need to trigger a job using sidekiq in after_save method. But the job is triggered, before the object is committed into the database, so I get the error, object not found with id=xyz.
So, I need to use
after_commit :method_name, :on => [:create, :update]
But the changes that I made in object doesn't show up in above method. I have an attribute email. When I was calling above method after_save, email_changed? return true. But if I call the same method using after_commit, email_changed? returns `false.
Is it because I am using object.save method and not create method?
Below is the method, which I am calling to trigger the job:
def update_or_create_user
if email_changed?
ServiceUpdateDataJob.perform_later action: 'update', data: {type: 'user', user_id: self.id}
end
true
end
I recognize this isn't exactly an answer to your question as stated. However...
IMO, you're overloading your model's responsibilities. I suggest you create a service that triggers the job when your model is saved. It might look something like:
class FooService
attr_accessor :unsaved_record
class << self
def call(unsaved_record)
new(unsaved_record).call
end
end
def initialize(unsaved_record)
#unsaved_record = unsaved_record
end
def call
kick_off_job if unsaved_record.save
!unsaved_record.new_record?
end
private
def kick_off_job
# job logic
end
end
You might use the service in a controller something like:
class FooController < ApplicationController
def create
#new_record = ModelName.new(record_params)
if FooService.call(#new_record)
# do successful save stuff
else
# do unsuccessful save stuff
end
end
...
end

How to call `super` in a block

I have a block of code. It was:
class User < ActiveRecord::Base
def configuration_with_cache
Rails.cache.fetch("user_#{id}_configuration") do
configuration_without_cache
end
end
alias_method_chain :configuration, :cache
end
I want to remove the notorious alias_method_chain, so I decided to refactor it. Here is my version:
class User < ActiveRecord::Base
def configuration
Rails.cache.fetch("#{id}_agency_configuration") do
super
end
end
end
But it doesn't work. The super enters a new scope. How can I make it work? I got TypeError: can't cast Class, and I misunderstood it.
To start off, calling super in blocks does behave the way you want. Must be your console is in a corrupted state (or something).
class User
def professional?
Rails.cache.fetch("user_professional") do
puts 'running super'
super
end
end
end
User.new.professional?
# >> running super
# => false
User.new.professional?
# => false
Next, this looks like something Module#prepend was made to help with.
module Cacher
def with_rails_cache(method)
mod = Module.new do
define_method method do
cache_key = "my_cache_for_#{method}"
Rails.cache.fetch(cache_key) do
puts "filling the cache"
super()
end
end
end
prepend mod
end
end
class User
extend Cacher
with_rails_cache :professional?
end
User.new.professional?
# >> filling the cache
# => false
User.new.professional?
# => false
you can user Super in block.
please see this, any issues let me know.
Calling it as just 'super' will pass the block.
super(*args, &block)' will as well.

Use super with before_validation

I have this code in my every model.
Class people
def before_validation
#attributes.each do |key,value|
self[key] = nil if value.blank?
end
end
end
Now i want to put my loop in separate module. Like
Module test
def before_validation
#attributes.each do |key,value|
self[key] = nil if value.blank?
end
end
end
And i want to call this before_validation this way
Class people
include test
def before_validation
super
.....Here is my other logic part.....
end
end
Are there any way to do it like that in rails??
You can setup multiple methods to be called by the before_validation callback. So instead of straight up defining the before_validation, you can pass the methods you want to get called before validation.
module Test
def some_test_before_validaiton_method
# do something
end
end
class People < ActiveRecord::Base
include Test
def people_before_validation_foo
#do something else
end
before_validation :some_test_before_validation_method
before_validation :people_before_validaiton_foo
end
You can read more about callbacks here: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html

Resources