account_counters.rb 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. # frozen_string_literal: true
  2. module AccountCounters
  3. extend ActiveSupport::Concern
  4. ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze
  5. included do
  6. has_one :account_stat, inverse_of: :account
  7. after_save :save_account_stat
  8. end
  9. delegate :statuses_count,
  10. :statuses_count=,
  11. :following_count,
  12. :following_count=,
  13. :followers_count,
  14. :followers_count=,
  15. :last_status_at,
  16. to: :account_stat
  17. # @param [Symbol] key
  18. def increment_count!(key)
  19. update_count!(key, 1)
  20. end
  21. # @param [Symbol] key
  22. def decrement_count!(key)
  23. update_count!(key, -1)
  24. end
  25. # @param [Symbol] key
  26. # @param [Integer] value
  27. def update_count!(key, value)
  28. raise ArgumentError, "Invalid key #{key}" unless ALLOWED_COUNTER_KEYS.include?(key)
  29. raise ArgumentError, 'Do not call update_count! on dirty objects' if association(:account_stat).loaded? && account_stat&.changed? && account_stat.changed_attribute_names_to_save == %w(id)
  30. value = value.to_i
  31. default_value = value.positive? ? value : 0
  32. # We do an upsert using manually written SQL, as Rails' upsert method does
  33. # not seem to support writing expressions in the UPDATE clause, but only
  34. # re-insert the provided values instead.
  35. # Even ARel seem to be missing proper handling of upserts.
  36. sql = if value.positive? && key == :statuses_count
  37. <<-SQL.squish
  38. INSERT INTO account_stats(account_id, #{key}, created_at, updated_at, last_status_at)
  39. VALUES (:account_id, :default_value, now(), now(), now())
  40. ON CONFLICT (account_id) DO UPDATE
  41. SET #{key} = account_stats.#{key} + :value,
  42. last_status_at = now(),
  43. updated_at = now()
  44. RETURNING id;
  45. SQL
  46. else
  47. <<-SQL.squish
  48. INSERT INTO account_stats(account_id, #{key}, created_at, updated_at)
  49. VALUES (:account_id, :default_value, now(), now())
  50. ON CONFLICT (account_id) DO UPDATE
  51. SET #{key} = account_stats.#{key} + :value,
  52. updated_at = now()
  53. RETURNING id;
  54. SQL
  55. end
  56. sql = AccountStat.sanitize_sql([sql, account_id: id, default_value: default_value, value: value])
  57. account_stat_id = AccountStat.connection.exec_query(sql)[0]['id']
  58. # Reload account_stat if it was loaded, taking into account newly-created unsaved records
  59. if association(:account_stat).loaded?
  60. account_stat.id = account_stat_id if account_stat.new_record?
  61. account_stat.reload
  62. end
  63. end
  64. def account_stat
  65. super || build_account_stat
  66. end
  67. private
  68. def save_account_stat
  69. return unless association(:account_stat).loaded? && account_stat&.changed?
  70. account_stat.save
  71. end
  72. end