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::Archivablenamespace. - 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
endArchiving 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.archivedGlobal 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
endCallbacks
Archivable documents have the following new callbacks.
Note that these callbacks are not fired on #destroy.
before_archiveafter_archivearound_archivebefore_restoreafter_restorearound_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
endRelation Dependencies
This gem adds two new relation dependency handling strategies:
-
:archive- Invokes#archiveand 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
endIn 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 callbacksGotchas
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_atIndexes
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: 1Note 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_atrather thandeleted_at. The namedeleted_atwas confusing with respect to hard deletion. - Mongoid::Paranoia overrides the
#deleteand#destroymethods; 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.archivedquery scopes as needed. Mongoid::Paranoia relies on.unscoped
Migration Checklist
- Add
mongoid_archivalto your gemspec aftermongoid_paranoia. You may use the two gems together in your project, but should include only one ofMongoid::ArchivableorMongoid::Paranoiainto each model class. In this manner, you can migrate each model one-by-one. - To avoid accidentally calling
#deleteand#destroy, add theMongoid::Archivable::Protectedmixin to cause those methods to raise an error. - Configure your
archived_field_name = :deleted_atfor backwards compatibility. - Add
.currentto your queries as necessary. You can remove usages of.unscoped. - In your relations, replace
dependent: :destroywithdependent: :archiveas 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.