2018-08-03 17:22:24 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2018-03-12 15:51:38 +00:00
|
|
|
# Include atomic internal id generation scheme for a model
|
|
|
|
#
|
2018-03-16 12:34:08 +00:00
|
|
|
# This allows us to atomically generate internal ids that are
|
2018-03-12 15:51:38 +00:00
|
|
|
# unique within a given scope.
|
|
|
|
#
|
|
|
|
# For example, let's generate internal ids for Issue per Project:
|
|
|
|
# ```
|
|
|
|
# class Issue < ActiveRecord::Base
|
|
|
|
# has_internal_id :iid, scope: :project, init: ->(s) { s.project.issues.maximum(:iid) }
|
|
|
|
# end
|
|
|
|
# ```
|
|
|
|
#
|
|
|
|
# This generates unique internal ids per project for newly created issues.
|
|
|
|
# The generated internal id is saved in the `iid` attribute of `Issue`.
|
|
|
|
#
|
|
|
|
# This concern uses InternalId records to facilitate atomicity.
|
|
|
|
# In the absence of a record for the given scope, one will be created automatically.
|
|
|
|
# In this situation, the `init` block is called to calculate the initial value.
|
|
|
|
# In the example above, we calculate the maximum `iid` of all issues
|
|
|
|
# within the given project.
|
|
|
|
#
|
|
|
|
# Note that a model may have more than one internal id associated with possibly
|
|
|
|
# different scopes.
|
2018-03-06 19:09:01 +00:00
|
|
|
module AtomicInternalId
|
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
2018-08-27 12:35:31 +00:00
|
|
|
class_methods do
|
2018-05-08 05:57:41 +00:00
|
|
|
def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName
|
2018-07-27 12:39:48 +00:00
|
|
|
# We require init here to retain the ability to recalculate in the absence of a
|
|
|
|
# InternaLId record (we may delete records in `internal_ids` for example).
|
|
|
|
raise "has_internal_id requires a init block, none given." unless init
|
|
|
|
|
2018-05-10 07:22:43 +00:00
|
|
|
before_validation :"ensure_#{scope}_#{column}!", on: :create
|
2018-05-11 06:34:36 +00:00
|
|
|
validates column, presence: presence
|
2018-05-03 08:48:23 +00:00
|
|
|
|
2018-05-10 07:22:43 +00:00
|
|
|
define_method("ensure_#{scope}_#{column}!") do
|
2018-04-20 14:00:15 +00:00
|
|
|
scope_value = association(scope).reader
|
2018-08-01 09:03:14 +00:00
|
|
|
value = read_attribute(column)
|
2018-05-03 08:48:23 +00:00
|
|
|
|
2018-08-01 09:03:14 +00:00
|
|
|
return value unless scope_value
|
2018-03-14 13:36:07 +00:00
|
|
|
|
2018-08-01 09:03:14 +00:00
|
|
|
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
|
|
|
|
usage = self.class.table_name.to_sym
|
|
|
|
|
|
|
|
if value.present?
|
|
|
|
InternalId.track_greatest(self, scope_attrs, usage, value, init)
|
|
|
|
else
|
|
|
|
value = InternalId.generate_next(self, scope_attrs, usage, init)
|
|
|
|
write_attribute(column, value)
|
2018-03-14 12:42:03 +00:00
|
|
|
end
|
2018-05-03 08:48:23 +00:00
|
|
|
|
2018-08-01 09:03:14 +00:00
|
|
|
value
|
2018-03-12 14:38:56 +00:00
|
|
|
end
|
|
|
|
end
|
2018-03-06 19:09:01 +00:00
|
|
|
end
|
|
|
|
end
|