92 lines
3 KiB
Ruby
92 lines
3 KiB
Ruby
|
##
|
||
|
# This module is for replacing `dependent: :destroy` and `before_destroy` hooks.
|
||
|
#
|
||
|
# In general, `destroy_all` is inefficient because it calls each callback with `DELETE` queries i.e. O(n), whereas,
|
||
|
# `delete_all` is efficient as it deletes all rows with a single `DELETE` query.
|
||
|
#
|
||
|
# It's better to use `delete_all` as our best practice, however,
|
||
|
# if external data (e.g. ObjectStorage, FileStorage or Redis) are assosiated with database records,
|
||
|
# it is difficult to accomplish it.
|
||
|
#
|
||
|
# This module defines a format to use `delete_all` and delete associated external data.
|
||
|
# Here is an exmaple
|
||
|
#
|
||
|
# Situation
|
||
|
# - `Project` has many `Ci::BuildTraceChunk` through `Ci::Build`
|
||
|
# - `Ci::BuildTraceChunk` stores associated data in Redis, so it relies on `dependent: :destroy` and `before_destroy` for the deletion
|
||
|
#
|
||
|
# How to use
|
||
|
# - Define `use_fast_destroy :build_trace_chunks` in `Project` model.
|
||
|
# - Define `begin_fast_destroy` and `finalize_fast_destroy(params)` in `Ci::BuildTraceChunk` model.
|
||
|
# - Use `fast_destroy_all` instead of `destroy` and `destroy_all`
|
||
|
# - Remove `dependent: :destroy` and `before_destroy` as it's no longer need
|
||
|
#
|
||
|
# Expectation
|
||
|
# - When a project is `destroy`ed, the associated trace_chunks will be deleted by `delete_all`,
|
||
|
# and the associated data will be removed, too.
|
||
|
# - When `fast_destroy_all` is called, it also performns as same.
|
||
|
module FastDestroyAll
|
||
|
extend ActiveSupport::Concern
|
||
|
|
||
|
ForbiddenActionError = Class.new(StandardError)
|
||
|
|
||
|
included do
|
||
|
before_destroy do
|
||
|
raise ForbiddenActionError, '`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`'
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class_methods do
|
||
|
##
|
||
|
# This method delete rows and associated external data efficiently
|
||
|
#
|
||
|
# This method can replace `destroy` and `destroy_all` without having `after_destroy` hook
|
||
|
def fast_destroy_all
|
||
|
params = begin_fast_destroy
|
||
|
|
||
|
delete_all
|
||
|
|
||
|
finalize_fast_destroy(params)
|
||
|
end
|
||
|
|
||
|
##
|
||
|
# This method returns identifiers to delete associated external data (e.g. file paths, redis keys)
|
||
|
#
|
||
|
# This method must be defined in fast destroyable model
|
||
|
def begin_fast_destroy
|
||
|
raise NotImplementedError
|
||
|
end
|
||
|
|
||
|
##
|
||
|
# This method deletes associated external data with the identifiers returned by `begin_fast_destroy`
|
||
|
#
|
||
|
# This method must be defined in fast destroyable model
|
||
|
def finalize_fast_destroy(params)
|
||
|
raise NotImplementedError
|
||
|
end
|
||
|
end
|
||
|
|
||
|
module Helpers
|
||
|
extend ActiveSupport::Concern
|
||
|
|
||
|
class_methods do
|
||
|
##
|
||
|
# This method is to be defined on models which have fast destroyable models as children,
|
||
|
# and let us avoid to use `dependent: :destroy` hook
|
||
|
def use_fast_destroy(relation)
|
||
|
before_destroy(prepend: true) do
|
||
|
perform_fast_destroy(public_send(relation)) # rubocop:disable GitlabSecurity/PublicSend
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def perform_fast_destroy(subject)
|
||
|
params = subject.begin_fast_destroy
|
||
|
|
||
|
run_after_commit do
|
||
|
subject.finalize_fast_destroy(params)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|