Skip to content

Commit

Permalink
Merge pull request #1103 from paper-trail-gem/remove_association_trac…
Browse files Browse the repository at this point in the history
…king

Extract the experimental association tracking feature into separate gem
  • Loading branch information
jaredbeck committed Jun 4, 2018
2 parents 9013afb + caefd92 commit 80aeebf
Show file tree
Hide file tree
Showing 25 changed files with 50 additions and 1,143 deletions.
16 changes: 0 additions & 16 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,11 @@ ActiveRecord::Schema.define do
t.string :whodunnit
t.text :object, limit: 1_073_741_823
t.text :object_changes, limit: 1_073_741_823
t.integer :transaction_id
t.datetime :created_at
end
add_index :versions, %i[item_type item_id]
add_index :versions, [:transaction_id]

create_table :version_associations do |t|
t.integer :version_id
t.string :foreign_key_name, null: false
t.integer :foreign_key_id
end
add_index :version_associations, [:version_id]
add_index :version_associations, %i[foreign_key_name foreign_key_id],
name: "index_version_associations_on_foreign_key"
end
ActiveRecord::Base.logger = Logger.new(STDOUT)
require "paper_trail/config"

# STEP THREE: Configure PaperTrail as you would in your initializer
PaperTrail::Config.instance.track_associations = false

require "paper_trail"

# STEP FOUR: Define your AR models here.
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).

### Added

- The experimental associations tracking feature has been moved to a
separate gem, [paper_trail-association_tracking]
(https://github.com/westonganger/paper_trail-association_tracking).
PT will, for now, have a runtime dependency on this new gem. So, assuming the
gem extraction goes well, no breaking changes are anticipated.
- [#1093](https://github.com/paper-trail-gem/paper_trail/pull/1093) -
`PaperTrail.config.object_changes_adapter` - Allows specifying an adapter that will
determine how the changes for each version are stored in the object_changes column.
Expand Down
222 changes: 20 additions & 202 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ has been destroyed.
- [7.c. Cucumber](#7c-cucumber)
- [7.d. Spork](#7d-spork)
- [7.e. Zeus or Spring](#7e-zeus-or-spring)
- [8. Integration with Other Libraries](#8-integration-with-other-libraries)
- [9. Related Libraries and Ports](#9-related-libraries-and-ports)
- [8. PaperTrail Plugins](#8-papertrail-plugins)
- [9. Integration with Other Libraries](#9-integration-with-other-libraries)
- [10. Related Libraries and Ports](#10-related-libraries-and-ports)
- [Articles](#articles)
- [Problems](#problems)
- [Contributors](#contributors)
Expand Down Expand Up @@ -841,204 +842,18 @@ string, please try the [paper_trail-globalid][37] gem.

### 4.b. Associations

**Experimental feature with many known issues. Not recommended for production.**
See known issues below.
To track and reify associations, use [paper_trail-association_tracking][6] (PT-AT).

PaperTrail can restore three types of associations: Has-One, Has-Many, and
Has-Many-Through. In order to do this, you will need to do two things:
From 2014 to 2018, association tracking was part of PT core as an experimental
feature, but many issues were discovered. To attract new volunteers to address
these issues, PT-AT was extracted (see
https://github.com/paper-trail-gem/paper_trail/issues/1070).

1. Create a `version_associations` table
2. Set `PaperTrail.config.track_associations = true` (e.g. in an initializer)

Both will be done for you automatically if you install PaperTrail with the
`--with_associations` option
(e.g. `rails generate paper_trail:install --with-associations`)

If you want to add this functionality after the initial installation, you will
need to create the `version_associations` table manually, and you will need to
ensure that `PaperTrail.config.track_associations = true` is set.

PaperTrail will store in the `version_associations` table additional information
to correlate versions of the association and versions of the model when the
associated record is changed. When reifying the model, PaperTrail can use this
table, together with the `transaction_id` to find the correct version of the
association and reify it. The `transaction_id` is a unique id for version records
created in the same transaction. It is used to associate the version of the model
and the version of the association that are created in the same transaction.

To restore Has-One associations as they were at the time, pass option `has_one:
true` to `reify`. To restore Has-Many and Has-Many-Through associations, use
option `has_many: true`. To restore Belongs-To association, use
option `belongs_to: true`. For example:

```ruby
class Location < ActiveRecord::Base
belongs_to :treasure
has_paper_trail
end

class Treasure < ActiveRecord::Base
has_one :location
has_paper_trail
end

treasure.amount # 100
treasure.location.latitude # 12.345

treasure.update_attributes amount: 153
treasure.location.update_attributes latitude: 54.321

t = treasure.versions.last.reify(has_one: true)
t.amount # 100
t.location.latitude # 12.345
```

If the parent and child are updated in one go, PaperTrail can use the
aforementioned `transaction_id` to reify the models as they were before the
transaction (instead of before the update to the model).

```ruby
treasure.amount # 100
treasure.location.latitude # 12.345

Treasure.transaction do
treasure.location.update_attributes latitude: 54.321
treasure.update_attributes amount: 153
end

t = treasure.versions.last.reify(has_one: true)
t.amount # 100
t.location.latitude # 12.345, instead of 54.321
```

By default, PaperTrail excludes an associated record from the reified parent
model if the associated record exists in the live model but did not exist as at
the time the version was created. This is usually what you want if you just want
to look at the reified version. But if you want to persist it, it would be
better to pass in option `mark_for_destruction: true` so that the associated
record is included and marked for destruction. Note that `mark_for_destruction`
only has [an effect on associations marked with `autosave: true`][32].

```ruby
class Widget < ActiveRecord::Base
has_paper_trail
has_one :wotsit, autosave: true
end

class Wotsit < ActiveRecord::Base
has_paper_trail
belongs_to :widget
end

widget = Widget.create(name: 'widget_0')
widget.update_attributes(name: 'widget_1')
widget.create_wotsit(name: 'wotsit')

widget_0 = widget.versions.last.reify(has_one: true)
widget_0.wotsit # nil

widget_0 = widget.versions.last.reify(has_one: true, mark_for_destruction: true)
widget_0.wotsit.marked_for_destruction? # true
widget_0.save!
widget.reload.wotsit # nil
```

#### 4.b.1. Known Issues

Associations are an **experimental feature** and have the following known
issues, in order of descending importance.

1. PaperTrail only reifies the first level of associations.
1. Sometimes the has_one association will find more than one possible candidate
and will raise a `PaperTrail::Reifiers::HasOne::FoundMoreThanOne` error. For
example, see `spec/models/person_spec.rb`
- If you are not using STI, you may want to just assume the first result (of
multiple) is the correct one and continue. Versions pre v8.1.2 and below did
this without error or warning. To do so add the following line to your
initializer: `PaperTrail.config.association_reify_error_behaviour = :warn`.
Valid options are: `[:error, :warn, :ignore]`
- When using STI, even if you enable `:warn` you will likely still end up
recieving an `ActiveRecord::AssociationTypeMismatch` error.
1. [#542](https://github.com/paper-trail-gem/paper_trail/issues/542) -
Not compatible with [transactional tests][34], aka. transactional fixtures.
1. Requires database timestamp columns with fractional second precision.
- Sqlite and postgres timestamps have fractional second precision by default.
[MySQL timestamps do not][35]. Furthermore, MySQL 5.5 and earlier do not
support fractional second precision at all.
- Also, support for fractional seconds in MySQL was not added to
rails until ActiveRecord 4.2 (https://github.com/rails/rails/pull/14359).
1. PaperTrail can't restore an association properly if the association record
can be updated to replace its parent model (by replacing the foreign key)
1. Currently PaperTrail only supports a single `version_associations` table.
Therefore, you can only use a single table to store the versions for
all related models. Sorry for those who use multiple version tables.
1. PaperTrail relies on the callbacks on the association model (and the :through
association model for Has-Many-Through associations) to record the versions
and the relationship between the versions. If the association is changed
without invoking the callbacks, Reification won't work. Below are some
examples:

Given these models:

```ruby
class Book < ActiveRecord::Base
has_many :authorships, dependent: :destroy
has_many :authors, through: :authorships, source: :person
has_paper_trail
end

class Authorship < ActiveRecord::Base
belongs_to :book
belongs_to :person
has_paper_trail # NOTE
end

class Person < ActiveRecord::Base
has_many :authorships, dependent: :destroy
has_many :books, through: :authorships
has_paper_trail
end
```

Then each of the following will store authorship versions:

```ruby
@book.authors << @dostoyevsky
@book.authors.create name: 'Tolstoy'
@book.authorships.last.destroy
@book.authorships.clear
@book.author_ids = [@solzhenistyn.id, @dostoyevsky.id]
```

But none of these will:

```ruby
@book.authors.delete @tolstoy
@book.author_ids = []
@book.authors = []
```

Having said that, you can apparently get all these working (I haven't tested it
myself) with this patch:

```ruby
# In config/initializers/active_record_patch.rb
module ActiveRecord
# = Active Record Has Many Through Association
module Associations
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
alias_method :original_delete_records, :delete_records

def delete_records(records, method)
method ||= :destroy
original_delete_records(records, method)
end
end
end
end
```

See [issue 113][16] for a discussion about this.
Even though this has always been an experimental feature, we don't want the
extraction of PT-AT to be a breaking change. So, `paper_trail` will have a
runtime dependency on this gem and will keep running the existing tests related
to association tracking. This arrangement will be maintained for a few years, if
practical.

### 4.c. Storing Metadata

Expand Down Expand Up @@ -1158,7 +973,6 @@ Usage:
Options:
[--with-changes], [--no-with-changes] # Store changeset (diff) with each version
[--with-associations], [--no-with-associations] # Store transactional IDs to support association restoration
Runtime options:
-f, [--force] # Overwrite files that already exist
Expand Down Expand Up @@ -1621,7 +1435,11 @@ require 'rspec/rails'
require 'paper_trail/frameworks/rspec'
```

## 8. Integration with Other Libraries
## 8. PaperTrail Plugins
- [paper_trail-association_tracking][6] - track and reify associations
- [paper_trail-globalid][49] - enhances whodunnit by adding an `actor`

## 9. Integration with Other Libraries

- [ActiveAdmin][42]
- [paper_trail_manager][46] - Browse, subscribe, view and revert changes to
Expand All @@ -1630,11 +1448,10 @@ require 'paper_trail/frameworks/rspec'
- Sinatra - [paper_trail-sinatra][41]
- [globalize][45] - [globalize-versioning][44]
- [solidus_papertrail][47] - PT integration for Solidus
- [paper_trail-globalid][49] - enhances whodunnint by adding an `actor`
method to instances of PaperTrail::Version that returns the ActiveRecord
object who was responsible for change

## 9. Related Libraries and Ports
## 10. Related Libraries and Ports

- [izelnakri/paper_trail][50] - An Ecto library, inspired by PT.
- [sequelize-paper-trail][48] - A JS library, inspired by PT. A sequelize
Expand Down Expand Up @@ -1683,6 +1500,7 @@ Released under the MIT licence.
[3]: http://railscasts.com/episodes/255-undo-with-paper-trail
[4]: https://api.travis-ci.org/paper-trail-gem/paper_trail.svg?branch=master
[5]: https://travis-ci.org/paper-trail-gem/paper_trail
[6]: https://github.com/westonganger/paper_trail-association_tracking
[9]: https://github.com/paper-trail-gem/paper_trail/tree/3.0-stable
[10]: https://github.com/paper-trail-gem/paper_trail/tree/2.7-stable
[11]: https://github.com/paper-trail-gem/paper_trail/tree/rails2
Expand Down
16 changes: 0 additions & 16 deletions doc/bug_report_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,11 @@
t.string :whodunnit
t.text :object, limit: 1_073_741_823
t.text :object_changes, limit: 1_073_741_823
t.integer :transaction_id
t.datetime :created_at
end
add_index :versions, %i[item_type item_id]
add_index :versions, [:transaction_id]

create_table :version_associations do |t|
t.integer :version_id
t.string :foreign_key_name, null: false
t.integer :foreign_key_id
end
add_index :version_associations, [:version_id]
add_index :version_associations, %i[foreign_key_name foreign_key_id],
name: "index_version_associations_on_foreign_key"
end
ActiveRecord::Base.logger = Logger.new(STDOUT)
require "paper_trail/config"

# STEP THREE: Configure PaperTrail as you would in your initializer
PaperTrail::Config.instance.track_associations = true

require "paper_trail"

# STEP FOUR: Define your AR models here.
Expand Down
18 changes: 0 additions & 18 deletions lib/generators/paper_trail/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,13 @@ class InstallGenerator < ::Rails::Generators::Base
default: false,
desc: "Store changeset (diff) with each version"
)
class_option(
:with_associations,
type: :boolean,
default: false,
desc: "Store transactional IDs to support association restoration"
)

desc "Generates (but does not run) a migration to add a versions table." \
" Also generates an initializer file for configuring PaperTrail"

def create_migration_file
add_paper_trail_migration("create_versions")
add_paper_trail_migration("add_object_changes_to_versions") if options.with_changes?
if options.with_associations?
add_paper_trail_migration("create_version_associations")
add_paper_trail_migration("add_transaction_id_column_to_versions")
end
end

def create_initializer
create_file(
"config/initializers/paper_trail.rb",
"PaperTrail.config.track_associations = #{!!options.with_associations?}\n",
"PaperTrail.config.association_reify_error_behaviour = :error"
)
end

def self.next_migration_number(dirname)
Expand Down

This file was deleted.

Loading

0 comments on commit 80aeebf

Please sign in to comment.