I need to atomically increment a model counter and use its new value (processed by a Sidekiq job).
At the moment, I use
Group.increment_counter :tasks_count, @task.id
in my model which atomically increments the counter.
But I also need its new value to send a notification, if the counter has e.g. the value 50
. Any ideas? Locking the table/row or is there an easier way?
Edit / SOLUTION
Based on mu is too short's answer and Rails's update_counters
method , I implemented an instance method (tested with PostgreSQL).
def self.increment_counter_and_return_value(counter_name, id)
quoted_column = connection.quote_column_name(counter_name)
quoted_table = connection.quote_table_name(table_name)
quoted_primary_key = connection.quote_column_name(primary_key)
quoted_primary_key_value = connection.quote(id)
sql = "UPDATE #{quoted_table} SET #{quoted_column} = COALESCE(#{quoted_column}, 0) + 1 WHERE #{quoted_table}.#{quoted_primary_key} = #{quoted_primary_key_value} RETURNING #{quoted_column}"
connection.select_value(sql).to_i
end
Use it like:
Group.increment_counter_and_return_value(:tasks_count, @task.id)
It uses RETURNING
to fetch the new value within the same query.
Your Group.increment_counter
call sends SQL like this to the database:
update groups
set tasks_count = coalesce(tasks_counter, 0) + 1
where id = X
where X
is @task.id
. The SQL way to get the new tasks_counter
value is to include a RETURNING clause:
update groups
set tasks_count = coalesce(tasks_counter, 0) + 1
where id = X
returning tasks_count
I don't know of any convenient Railsy way to get that SQL to the database though. The usual Rails approach would be to either do a bunch of locking and reload @task
or skip the locking and hope for the best:
Group.increment_counter :tasks_count, @task.id
@task.reload
# and now look at @task.tasks_count to get the new value
You can use RETURNING like this though:
new_count = Group.connection.execute(%Q{
update groups
set tasks_count = coalesce(tasks_counter, 0) + 1
where id = #{Group.connection.quote(@task.id)}
returning tasks_count
}).first['tasks_count'].to_i
You'd probably want to hide that mess behind a method on Group
so that you could say things like:
n = Group.increment_tasks_count_for(@task)
# or
n = @task.increment_tasks_count
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With