Mongoid::Archivable (mongoid_archival)
Mongoid::Archivable
enables archiving (soft delete) of Mongoid documents.
Instead of being removed from the database, archived docs are flagged with an archived_at
timestamp.
This allows you to maintain references to archived documents, and restore if necessary.
Instability Warning
Versions prior to 1.0.0 are in alpha state. Behaviors, APIs, method names, etc. may change anytime without warning. Please lock your gem version, be careful when upgrading, and write tests in your own project.
Disambiguation
- This gem is different than mongoid-archivable,
which moves documents to a separate "archive" database/collection. Cannot be used concurrently with
this gem as both use the
Mongoid::Archivable
namespace. - This gem is forked from mongoid_paranoia. Can be used concurrently with this gem. See section below for key differences.
TODO
- Support embedded documents.
- Support model-level configuration.
- Allow rename archive field alias.
- Consider adding #archive(callbacks: false)
- Consider adding .archive_all query action
Usage
Installation
In your application's Gemfile:
gem 'mongoid_archival'
Adding to Model Class
class Person
include Mongoid::Document
include Mongoid::Archivable
# TODO: archivable macro
end
Archiving with Documents
# Set the archived_at field to the current time, firing callbacks
# and archiving any dependent documents. Analogous to Mongoid #destroy method.
person.archive
# Sets the archived_at field to the current time, ignoring callbacks
# and dependency rules. Analogous to Mongoid #delete method.
person.archive_without_callbacks
# TODO person.archive(callbacks: false)
# Un-archive an archive document back.
person.restore
# Un-archive an archive document back, including any dependent documents.
person.restore(recursive: true)
Querying
# Return all documents, both archived and non-archived.
Person.all
# Return only documents that are not flagged as archived.
Person.current
# Return only documents that are flagged as archived.
Person.archived
Global Configuration
You may globally configure field and method names in an initializer.
# config/initializers/mongoid_archivable.rb
Mongoid::Archivable.configure do |c|
c.archived_field = :archived_at
c.archived_scope = :archived
c.nonarchived_scope = :current
end
Callbacks
Archivable documents have the following new callbacks.
Note that these callbacks are not fired on #destroy
.
before_archive
after_archive
around_archive
before_restore
after_restore
around_restore
class User
include Mongoid::Document
include Mongoid::Archivable
before_archive :before_archive_action
after_archive :after_archive_action
around_archive :around_archive_action
before_restore :before_restore_action
after_restore :after_restore_action
around_restore :around_restore_action
# You may `throw(:abort)` within a callback to prevent
# the action from proceeding.
def before_archive_action
throw(:abort) if name == 'Pete'
end
end
Relation Dependencies
This gem adds two new relation dependency handling strategies:
-
:archive
- Invokes#archive
and callbacks on each dependency, recursively including dependencies of dependencies. Analogous to:destroy
. -
:archive_all
- Calls.set(archived_at: Time.now)
on the dependency scope in a single query. Much faster but does not support callbacks or dependency recursion. Analogous to:delete_all
.
If the dependent model is not archivable, these strategies will be ignored without any effect.
class User
include Mongoid::Document
include Mongoid::Archivable
has_many :pokemons, dependent: :archive
belongs_to :gym, dependent: :archive_all
end
In addition, dependency strategies :nullify
, :restrict_with_exception
,
and :restrict_with_error
will be applied when archiving documents.
:destroy
and :delete_all
are intentionally ignored.
Protecting Against Deletion
Add the Mongoid::Archivable::Protected
mixin to cause
#delete
and #destroy
methods to raise an error.
The bang methods #delete!
and #destroy!
can be used instead.
This is useful when migrating a legacy codebase.
class Pokemon
include Mongoid::Document
include Mongoid::Archivable
include Mongoid::Archivable::Protected
end
venusaur = Pokemon.create
venusaur.delete # raises RuntimeError
venusaur.destroy # raises RuntimeError
venusaur.delete! # deletes the document without callbacks
venusaur.destroy! # deletes the document with callbacks
Gotchas
The following require additional manual changes when using this gem.
Uniqueness Validation
You must set scope: :archived_at
in your uniqueness validations to prevent
validating against archived documents.
validates_uniqueness_of :title, scope: :archived_at
Indexes
You should add archived_at
to your query indexes. As a rule-of-thumb, we recommend
to add archived_at
as the final key; this will create a compound index that will be
selected with or without archived_at
in the query.
index category: 1, title: 1, archived_at: 1
Note that this may not give the best performance in all cases, for example
when performing a time-range query on the value of archived_at
. Please refer to the
MongoDB Indexes documentation
to learn more about index design.
Comparison with Mongoid::Paranoia
We used Mongoid::Paranoia at TableCheck for many years. While many of design assumptions of Mongoid::Paranoia lead to initial productivity, we found them ultimately limiting and unintuitive as we grew both our team and our codebase.
Key Differences
- The flag named is
archived_at
rather thandeleted_at
. The namedeleted_at
was confusing with respect to hard deletion. - Mongoid::Paranoia overrides the
#delete
and#destroy
methods; this gem does not. Monkey patches and hackery are removed; behavior is less surprising. - This gem does not set a default scope on root (non-embedded) docs.
Use the
.current
(non-archived) and.archived
query scopes as needed. Mongoid::Paranoia relies on.unscoped
Migration Checklist
- Add
mongoid_archival
to your gemspec aftermongoid_paranoia
. You may use the two gems together in your project, but should include only one ofMongoid::Archivable
orMongoid::Paranoia
into each model class. In this manner, you can migrate each model one-by-one. - To avoid accidentally calling
#delete
and#destroy
, add theMongoid::Archivable::Protected
mixin to cause those methods to raise an error. - Configure your
archived_field_name = :deleted_at
for backwards compatibility. - Add
.current
to your queries as necessary. You can remove usages of.unscoped
. - In your relations, replace
dependent: :destroy
withdependent: :archive
as necessary.
About Us
Mongoid::Archivable is made with ❤ by TableCheck, the leading restaurant reservation and guest management app maker. If you are a ninja-level 🥷 coder (Javascript/Ruby/Elixir/Python/Go), designer, product manager, data scientist, QA, etc. and are ready to join us in Tokyo, Japan or work remotely, please get in touch at careers@tablecheck.com.
Shout out to Durran Jordan and Josef Šimánek for their original work on Mongoid::Paranoia.