Coder Social home page Coder Social logo

expectedbehavior / acts_as_archival Goto Github PK

View Code? Open in Web Editor NEW
128.0 7.0 23.0 392 KB

An ActiveRecord plugin for atomic archiving and unarchiving of object trees. Inspired by ActsAsParanoid and PermanentRecord

License: MIT License

Ruby 99.44% Shell 0.56%

acts_as_archival's Introduction

ActsAsArchival

Build Status Gem Version

Atomically archive object trees in your activerecord models.

We had the problem that acts_as_paranoid and similar plugins/gems always work on a record-by-record basis and made it very difficult to restore records atomically (or archive them, for that matter).

Because the archive and unarchive methods are in transactions, and every archival record involved gets the same archive number upon archiving, you can easily restore or remove an entire set of records without having to worry about partial deletion or restoration.

Additionally, other plugins generally screw with how destroy/delete work. We don't because we actually want to be able to destroy records.

Maintenance

You might read the commit logs and think "This must be abandonware! This hasn't been updated in 2y!" But! This is a mature project that solves a specific problem in ActiveRecord. It tends to only be updated when a new major version of ActiveRecord comes out and hence the infrequent updates.

Install

Gemfile:

gem "acts_as_archival"

Any models you want to be archival should have the columns archive_number (String) and archived_at (DateTime).

i.e. rails g migration AddAAAToPost archive_number archived_at:datetime

Any dependent-destroy AAA model associated to an AAA model will be archived with its parent.

If you're stuck on Rails 4.0x/3x/2x, check out the older tags/branches, which are no longer in active development.

Example

class Hole < ActiveRecord::Base
  acts_as_archival
  has_many :rats, dependent: :destroy
end

class Rat < ActiveRecord::Base
  acts_as_archival
end

Simple interactions & scopes

h = Hole.create                  #
h.archived?                      # => false
h.archive!                       # => true
h.archived?                      # => true
h.archive_number                 # => "b56876de48a5dcfe71b2c13eec15e4a2"
h.archived_at                    # => Thu, 01 Jan 2012 01:49:21 -0400
h.unarchive!                     # => true
h.archived?                      # => false
h.archive_number                 # => nil
h.archived_at                    # => nil

Associations

h = Hole.create                  #
r = h.rats.create                #
h.archive!                       # => true
h.archive_number                 # => "b56876de48a5dcfe71b2c13eec15e4a2"
r.archived_at                    # => Thu, 01 Jan 2012 01:52:12 -0400
r.archived?                      # => true
h.unarchive!                     # => true
h.archive_number                 # => nil
r.archived_at                    # => nil
r.archived?                      # => false

Relations

Hole.create!
Hole.create!
Hole.create!

holes = Hole.all

# All records in the relation will be archived with the same archive_number.
# Dependent/Destroy relationships will be archived, and callbacks will still be honored.
holes.archive_all!              # => [array of Hole records in the relation]

holes.first.archive_number      # => "b56876de48a5dcfe71b2c13eec15e4a2"
holes.last.archive_number       # => "b56876de48a5dcfe71b2c13eec15e4a2"

holes.unarchive_all!            # => [array of Hole records in the relation]

Scopes

h = Hole.create
Hole.archived.size               # => 0
Hole.unarchived.size             # => 1
h.archive!
Hole.archived.size               # => 1
Hole.unarchived.size             # => 0

Utility methods

h = Hole.create                  #
h.archival?                   # => true
Hole.archival?                # => true

Options

When defining an AAA model, it is is possible to make it unmodifiable when it is archived by passing readonly_when_archived: true to the acts_as_archival call in your model.

class CantTouchThis < ActiveRecord::Base
  acts_as_archival readonly_when_archived: true
end

record = CantTouchThis.create(foo: "bar")
record.archive!                              # => true
record.foo = "I want this to work"
record.save                                  # => false
record.errors.full_messages.first            # => "Cannot modify an archived record."

Callbacks

AAA models have four additional callbacks to do any necessary cleanup or other processing before and after archiving and unarchiving, and can additionally halt the archive callback chain.

class Hole < ActiveRecord::Base
  acts_as_archival

  # runs before #archive!
  before_archive :some_method_before_archiving

  # runs after #archive!
  after_archive :some_method_after_archiving

  # runs before #unarchive!
  before_unarchive :some_method_before_unarchiving

  # runs after #unarchive!
  after_unarchive :some_method_after_unarchiving

  # ... implement those methods
end

Halting the callback chain

  • Rails 4.2 - the callback method should return a false/nil value.
  • Rails 5.x - the callback should throw(:abort)/raise(:abort).

Caveats

  1. This will only work on associations that are dependent destroy. It should be trival to change that or make it optional.
  2. If you would like to work on this, you will need to setup sqlite on your development machine. Alternately, you can disable specific dev dependencies in the gemspec and test_helper and ask for help.

Testing

Running the tests should be as easy as:

script/setup                 # bundles, makes databases with permissions
rake                         # run tests on latest Rails
appraisal rake               # run tests on all versions of Rails

Check out more on appraisal if you need to add new versions of things or run into a version bug.

Help Wanted

We'd love to have your help making this better! If you have ideas for features this should implement or you think the code sucks, let us know. And PRs are greatly appreciated. ๐Ÿ‘

Thanks

ActsAsParanoid and PermanentRecords were both inspirations for this:

Contributors

  • Joel Meador
  • Michael Kuehl
  • Matthew Gordon
  • Vojtech Salbaba
  • David Jones
  • Dave Woodward
  • Miles Sterrett
  • James Hill
  • Maarten Claes
  • Anthony Panozzo
  • Aaron Milam
  • Anton Rieder
  • Josh Menden
  • Sergey Gnuskov

Thanks!

Copyright (c) 2009-2017 Expected Behavior, LLC, released under the MIT license

acts_as_archival's People

Contributors

aried3r avatar armilam avatar atlantis avatar dbastin avatar dependabot[bot] avatar esquivalient avatar gsmetal avatar janxious avatar josh-simplenexus avatar jqr avatar mcls avatar panozzaj avatar unixmonkey avatar vsalbaba avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

acts_as_archival's Issues

Deprecation Warnings for Rails 6.1

DEPRECATION WARNING: NOT conditions will no longer behave as NOR in Rails 6.1. To continue using NOR conditions, NOT each conditions manually (.where.not(:archived_at => ...).where.not(:archive_number => ...))

[RFC] Consider multiple archive numbers for association records

@esquivalient @stevegrossi

This came up as a "I expected this to work!" kinda thing, and am curious about your opinion on the matter.

A long explanation of the issue

# example adapted from http://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many
class Assembly < ActiveRecord::Base
  acts_as_archival
  has_many :manifests, dependent: :destroy
  has_many :parts, through: :manifests, dependent: :destroy
end

class Manifest < ActiveRecord::Base
  acts_as_archival
  belongs_to :assembly
  belongs_to :part
end

class Part < ActiveRecord::Base
  acts_as_archival
  has_many :manifests, dependent: :destroy
  has_many :assemblies, through: :manifests, dependent: :destroy
end

In this situation, Assembly and Part can be archived separately, and either will correctly archive the Manifest connecting them. But that doesn't represent the ideal for a situation in which a Part is archived after a Manifest connected to it is archived, since they will have different archive_numbers. Here is some demonstrative code:

assembly = Assembly.create
part = Part.create
manifest = Manifest.create(assembly: assembly, part: part)

assembly.archive
assembly.archived? # => true
manifest.archived? # => true
part.archive
part.archived? # => true
part.archive_number == manifest.archive_number # => false # starting to see a problem!

assembly.unarchive
manifest.archived? # => false
part.archived? # => true

manifest.unarchived.joins(:parts) # => [manifest] # oops!

So, this will now return a set of Manifests that includes an archived Part, which is technically correct but perhaps logically incorrect.

Solution

A thing that comes to mind: change the schema to allow multiple archive_number columns.

manifests:
  archive_number_part: string
  archive_number_assembly: string

Which would allow us to do something that is effectively:

class Manifest
  def archived?
    [archive_number, archive_number_part, archive_number_assembly].any?(&:present?)
  end
end

class Assembly
  def archive
    archive_number = generate_archive_number
    self.archive_number = archive_number
    manifests.each do |manifest|
      manifest.update_attributes(archive_number_assembly: archive_number)
    end
    save
  end
end

Note the actual code is going to be a lot more meta, but this is effectively a way to achieve this, and โ€œeasilyโ€ extends to 3+-way associations.


Another thing to consider is surfacing (through documentation) the internals of the archiving modules so that they can be derived from more easily to make custom archived? statuses easier to achieve. I don't know if that's a good idea, but certainly in non-trivial situations there is not much guidance in the repo itself for how to deal with multi-way conditional archiving - and may be it should remain that way. I htink explaining the philosophy behind this tool further might help, though. This is a solution to 90%+ of archiving problems, but the other 10% is murky. It's important to know that your application can override any of our internal bits to work within the application. Like ARec itself, this is often not needed, but ARec does a reasonable job of being like "it's okay. sometimes we aren't perfect and you need to use raw sql or do some transactions in a service or whatever" #Rambles.

Option to not archive dependent records

Hey!

Would you be open to adding an option to not archive dependent records? We might have a specific use case in our app, but we also want to reduce complexity, for example, if A has_many Bs and A is archived, alls B's get archived. If I separately unarchive alls Bs, A does not get unarchived automatically.
Of course we can do that ourselves, but we'd rather save us all the trouble and just archive A and display that visually.

Maybe as an option to acts_as_archival, acts_as_archival archive_dependent: false.

Of course, this would be an all-or-nothing option, as in you wouldn't be able to configure that not all Bs should be archived, but A also has_many Cs, which you would want to archive.

If you agree to this option, I'd do the all-or-nothing option which is optional of course.
I found act_on_archivals but I don't think there's a way to override it?

Archive dependent records without `dependent: :destroy`

We are using foreign keys with "ON DELETE CASCADE" to delete child records when parents are deleted, instead of dependent: :destroy, for performance reasons. The docs say "This will only work on associations that are dependent destroy. It should be trival to change that or make it optional." Could you provide more details if there is a suggested workaround for archiving children when a parent is archived? Also, would it be possible to auto-archive children with a dependent: :delete_all relationship in addition to dependent: :destroy?

Get Travis CI builds working

It seems this repository is being tested on Travis CI, but with the default config, which fails because no databases are being set up etc.

I don't know enough about Travis CI in conjunction with Appraisals to really work on it. But I think one of the problems is also the db_setup script, which asks for user input for the MySQL DB instead of using the values in test/database.yml.

Use `archive!` instead of `archive`

Since this library already uses archived?, maybe we could consider using .archive! since it modifies its receiver and deprecating .archive.

What do you think?

recover unwanted record for nested attributes

Hi,
Any model that have nested attributes it also recovers unwanted record from model.
Suppose we have three nested attributes associated with some item in model. When I
- archive that item then recover it
- destroy one of three nested attributes and make it archive
- after recovering that archived item it recovers all three nested attributes instead of recovering two(last updated state of an item)
Please help me with this issue...........

Big transactions

Hi
Is there an option to use update_all instead of single transactions?
This can become an real issue for some resources. Consider the following:
An organisation can have many profiles (lets say 10). Every profile has many trackings (1 a day for 3 years = 1095 )
When you archive the organisation it would create 1 + 10 + (10*1095) = 10.961 queries.
Thats kind of a lot.
Thanks

Deprecation warning on NOR usage regarding Rails 6.1

Just wanted to file an issue on this deprecation warning.

DEPRECATION WARNING: NOT conditions will no longer behave as NOR in Rails 6.1. To continue using NOR conditions, NOT each conditions manually (.where.not(:archived_at => ...).where.not(:archive_number => ...)). (called from irb_binding at (irb):8)

Thanks!

Archival exceptions should not be swallowed during testing

The current implementation of #handle_archival_action_exception translates all exceptions to an ActiveRecord::Rollback exception. If archiving fails because of an exception, the only information return is 'false'. There are some log lines in that method that look like they intend to provide more information, but those log messages were not added to the test log in a recent Rails 7 project.

I think this situation can be replicated by making a model with a bogus relationship, like

class Foo < ApplicationRecord
  acts_as_archival
  has_many :bar
end

where the project does not have a Bar model. So, calling archive on an instance of Foo like

test = Foo.first
test.archive! #=> false

Debugging in the run_callbacks will show the failure is about failing to find the Bar class.

Postgresql error handling

Hi,
So for it seems to be working with postgresql, the only thing is that the raise_if_not_archival does not get called even though the columns were not there yet. I just started with postgresql and didn't really see what would be causing it.
the error i get is:

ActiveRecord::StatementInvalid: PG::Error: ERROR:  column my_cool_column.archived_at does not exist

Query Performance - on archived vs unarchived

We have run into issues in our system which utilizes Postgres for data storage. When we attempt to get a list of objects which are unarchived or archived the queries are unable to hit an index. Digging in what we've discovered is that Postgres does NOT index NULL values.

When we make a query for the Object.unarchived.explain we see it's doing a seq scan on the table

                           QUERY PLAN
----------------------------------------------------------------
 Seq Scan on objects  (cost=0.00..75.03 rows=1403 width=549)
   Filter: ((archived_at IS NULL) AND (archive_number IS NULL))
(2 rows)

This is with an index added for archived_at and archive_number.

Has anyone else run into this? If so, were there any resolutions?

What is the thought of changing the gem to be able to use "another" default value instead of nil for those properties? For example:

archived_at = '1980-01-01 00:00:00'
archive_number = 0

This way Postgres would be able to index those values?

We've tried different partial indexes and the Object.archived is able to hit the index BUT the one for Object.unarchived still does the seq scan.

Thoughts?

UPDATE: It appears the issue is the scope of checking both archived_at and archive_number is the problem in that Postgres does NOT like checking for 2 null values. Theoretically, you could move from checking one column to both columns and performance would improve.

Deprecation warning in Rails 6.1 for archive!

Rails has deprecated using return, break and throw to exit a transaction.
Since the archive! method uses execute_archival_action where you "return" from inside a transaction, every usage of archive! will result in a Deprecation warning.

To fix, simply return the function result outside the transaction in the execute_archival_action method.

Archived from archive number named scope does not properly scope SQL query

Moles.archived_from_archive_number('blah').all :conditions => "'holes'.name = 'Moles Hole'", :joins => :hole

This query will result in SQL with archive_number and archived_at not properly prefixed with "moles", therefore generating "ambiguous column name" error. As both Mole and Hole act as archival, they both have archived_at and archive_number columns and database can't know which one to use in a query.

Document and tests for the before_archive and after_archive callbacks

Defined here:

callbacks = ['archive','unarchive']
define_callbacks *[callbacks, {:terminator => lambda { |_, result| result == false }}].flatten
callbacks.each do |callback|
eval <<-end_callbacks
def before_#{callback}(*args, &blk)
set_callback(:#{callback}, :before, *args, &blk)
end
def after_#{callback}(*args, &blk)
set_callback(:#{callback}, :after, *args, &blk)
end
end_callbacks
end

  • Documentation #19
  • Tests

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.