Skip to content

Instantly share code, notes, and snippets.

@tomchapin
Created April 9, 2020 08:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomchapin/c446ce18f6bd428e968acf958de728ed to your computer and use it in GitHub Desktop.
Save tomchapin/c446ce18f6bd428e968acf958de728ed to your computer and use it in GitHub Desktop.
# Handy utility mixin for incrementing counters in SQL in a manner that avoids race conditions
#
# Example Implementation:
#
# class Foo < ActiveRecord::Base
# include AtomicallyIncrementable
# end
#
# Then, assuming you have an integer column named "example_counter":
#
# foo = Foo.create(example_counter: 0)
# foo.atomic_increment!(:example_counter)
# puts foo.example_counter
# => 1
#
module AtomicallyIncrementable
# Increments an integer column in SQL (or increments in-memory model if record is not persisted in database)
# Also fetches the most recent value for the counter from the database and updates the in-memory model accordingly.
def atomic_increment!(column_name)
column_name_str = column_name.to_s
column_name_sym = column_name.to_sym
if self.class.column_names.include?(column_name_str) && column_name_str != self.class.primary_key
if self.persisted?
# Increment directly within the database
id = self[self.class.primary_key.to_sym]
raise 'Unable to determine id for current record!' unless id.present?
update = "#{self.class.table_name}"
set = "#{column_name_str} = COALESCE(#{column_name_str}, 0) + 1"
where = "#{self.class.table_name}.#{self.class.primary_key} = #{id}"
if self.class.column_names.include?('updated_at')
set += ', updated_at = NOW()'
end
query = "UPDATE #{update} SET #{set} WHERE #{where}"
self.class.connection.execute(query)
self[column_name_sym] = self.class.where(where).pluck(column_name_sym).first || 0
else
# Only update the local copy of the object in memory (without saving)
self[column_name_sym] += 1
end
else
raise 'Invalid counter_name specified!'
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment