-
Notifications
You must be signed in to change notification settings - Fork 895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix for issue #594, reifying sub-classed models that use STI #1108
Fix for issue #594, reifying sub-classed models that use STI #1108
Conversation
Given that this changes what is stored in the database, do we need to provide a convenient way for people convert their existing database records? If they did not convert their records, then reification (without |
it "uses the correct item_type in queries" do | ||
parent = described_class.new(name: "Jermaine Jackson") | ||
parent.path_to_stardom = "Emulating Motown greats such as the Temptations and "\ | ||
"The Supremes" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes -- I have a distinct interest in making a quasi-boring topic (auditing) into something enduring.
And actually I would hope that things like email notifications and cool reporting with pretty charts could piggy-back on top of what we store in versions
!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...do we need to provide a convenient way for people convert their existing database records?
We could, and in our project I actually have a migration that does this. It's possible to include with this a generator that creates a migration to cycle through existing PaperTrail::Version entries and update item_type appropriately.
-= But =- wonderfully enough this is not entirely essential because ActiveRecord just does the right thing; reified objects get instantiated as their proper subclassed type. With the Person / Doctor / Patient example, if for a Doctor you have a combo of entries from the past (<= v9.1) where the item_type was stored as Person, and now there are other entries (>= v9.2) some changes stored as Doctor, the reification always brings back a Doctor.
The AT part currently does not rehydrate all the historic attributes properly when this update is applied, so that's the reason for the couple notes in spec/models/pet_spec.rb
. During the coming week I have high hopes to solve that and then have the best of all worlds. The biggest "win" in my mind becomes more specific reporting of the item_type, and certainly having the example in spec/models/person.rb
work out is a wonderful benefit as well.
Great. If both I don't think this PR will make it into 9.2. Maybe 9.3? It's hard to predict the contents of future releases.
Sweet! Thanks for your work on this tricky issue. |
Added an example to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First review, looks like a good start!
Did you want to wait for PT-AT #5 to be released (eg. as PT-AT 1.0.1) and then we update our gemspec dependency to eg. ~> 1.0.1
?
lib/paper_trail/has_paper_trail.rb
Outdated
end | ||
end | ||
@paper_trail_type_name | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Can this method be moved into
RecordTrail
? - Why use an instance variable? I'd prefer a local variable if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This caches @paper_trail_type_name -- although perhaps people would not often call .versions more than once on a versioned object.
If it moves anywhere then I'd prefer to have it back in model_config where the has_many is established.
(TBH this started because the Rubocop ABC is set at 22, and I couldn't get under 22.69 after breaking out the has_many stuff in model_config, and with type_name
only being a simple string it seemed like a fairly lightweight addition to each object.)
lib/paper_trail/model_config.rb
Outdated
@@ -168,6 +168,31 @@ def cannot_record_after_destroy? | |||
::ActiveRecord::Base.belongs_to_required_by_default | |||
end | |||
|
|||
def type_aware_has_many(klass) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In keeping with the name of the method setup_associations
, I would call this setup_versions_association
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion!
lib/paper_trail/model_config.rb
Outdated
# custom inheritance column, `species`. If `attrs["species"]` is "Dog", | ||
# type_name is set to `Dog`. If `attrs["species"]` is blank, type_name | ||
# is set to `Animal`. You can see this particular example in action in | ||
# `spec/models/animal_spec.rb`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Assuming
paper_trail_type_name
can be moved intoRecordTrail
, and renamed to something likeversions_association_item_type
, this would beobject.paper_trail.versions_association_item_type
. - Extract a local variable
item_type = object.paper_trail.versions_association_item_type
and use in the two places below to avoid calculating it twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK all set!
lib/paper_trail/record_trail.rb
Outdated
@@ -237,7 +237,10 @@ def record_create | |||
@in_after_callback = true | |||
return unless enabled? | |||
versions_assoc = @record.send(@record.class.versions_association_name) | |||
versions_assoc.create! data_for_create | |||
version = versions_assoc.new data_for_create | |||
version.item_type = @record.class.name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can item_type
be moved into data_for_create
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately when initializing the new Version, ActiveRecord uses the polymorphic association to override whatever you specify for item_type
, so we need to new
it and then reset the item_type
on our own.
CHANGELOG.md
Outdated
@@ -24,7 +24,8 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/). | |||
|
|||
### Fixed | |||
|
|||
- None | |||
- [#594](https://github.com/paper-trail-gem/paper_trail/issues/594) - | |||
In order to properly reify a version of a model using STI, item_type can refer to the class instead of base_class. When making this change, the five reifiers in the gem [paper_trail-association_tracking] that refer to base_class can also be updated, which is covered in [PT-AT PR #5](https://github.com/westonganger/paper_trail-association_tracking/pull/5). With these changes, the previously failing example in person_spec.rb passes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Can you boil this down a bit, maybe into a single sentence?
- Should we link to Reify on associations fails if using Single Table Inheritance #594 or this PR (Fix for issue #594, reifying sub-classed models that use STI #1108) or both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the other way around might be best; I figure that more people use the core PaperTrail features than the association tracking. If Weston were to update his side now then it would break two examples in Certainly both halves of PaperTrail are important, at least to me, and it's tough to enact this kind of fix without having both sides move in tandem. |
What if we leave Now that I understand this proposal better, I'm uncomfortable about taking over the |
To make my concerns a bit more concrete, here are two possible future scenarios:
Sorry for not raising this concern earlier, it did not occur to me.
What do you think? |
Have had some free cycles again to dig back in on this, and I think we're finally out of the woods; some VERY good things to report! We don't have to force ActiveRecord's hand in the least. Remarkably when calling .create!() as we had been doing previously the result is different than if we call .new() and then .save(). Thankfully the latter causes https://apidock.com/rails/ActiveRecord/Inheritance/ClassMethods/find_sti_class Putting together a commit now for you to see this in action. |
I cannot confirm this. Here is what I tried: # paper_trail/spec/models/animal_spec.rb
it "saves the expected item_type" do
cat = Cat.create(name: "Buddy")
expect(cat.versions.first.item_type).to eq('Animal')
cat2 = Cat.new(name: "Buddy")
cat2.save
expect(cat2.versions.first.item_type).to eq('Cat') # => fails: Expected Cat, got Animal
end The above demonstrates that create (or create!) produces the same item_type as new/save. |
Ah -- the trick is not in how the Cat object gets built out, but how RecordTrail builds out the associated Version object:
|
Ah ha! Because of the |
That is a necessary additional piece. First the three places with .new() .save() in RecordTrail are able to record the real class name. Then when referencing One big win is that this now alleviates the trickery that |
I raised some concerns about us tampering with the
Can you address these concerns, please? Do you not share them? Thanks. |
I fail to see what kind of concern we can have. I mean, things operate just as before such that when we build a Version for a The three places in RecordTrail that use ActiveRecord code to make everything work each go a little something like this:
It is curious that calling versions.create() through the has_many only gets the base class, and instead doing versions.new() + version.save() arrives upon the real class, but that's not something that gives me concern. I figure the battle to try to get the kind of update to fix .create() rolled into Rails would be a significant uphill battle, especially since many people may have coded around this nuance, and rely on this behaviour to remain unmodified. I'm very happy with how it all works, and feel that further refinement may only be to improve performance slightly by caching |
I dislike optional arguments in general. I think they introduce more complexity than they are worth, in most cases.
We sleep until the next whole second because that is the precision of the timestamp that rails puts in generator filenames. If we didn't sleep, there's a good chance two tests would run within the same second and generate the same exact migration filename. Then, even though we delete the generated migrations after running them, some form of caching (perhaps filesystem, perhaps rails) will run the cached migration file. This implementation, with sleep, replaces a previous implementation using a "dummy migration" that forced rails to use the next timestamp. This was a very clever solution, and in the best case was N seconds faster than the sleep solution, where N is the number of times we run `generate_and_migrate` in the test suite. Currently N is 3, and I'm willing to live with 3s of sleep, and I prefer its simplicity. Perhaps most importantly, this implementation does not require any state to be persisted across tests (ie. the @migrator ivar in the previous implementation. Tests should be completely independent of eachother.
[ci skip]
I had a few hours tonight to review the changes to the I'm busy all day Friday and Saturday, but I'm hoping to finish my review on Sunday. |
Dismissing first review. Final review pending.
- Avoid repeating order clause - Method comments
Some people don't read changelogs.
In addition to the changes to I'm ready to merge this, but I'd welcome a review of my recent changes. Thanks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gosh we're happy to see all of this come together!
One note is that in RecordTrail#versions_association_item_type
it previously returned a blank string if the class type was the same as the base_class. This avoided the overhead of always having to .unscope() / .where(). I wonder about bringing that part back, but must admit that I haven't actually benchmarked how much extra CPU time and memory overhead this may or may not cause.
lib/paper_trail/model_config.rb
Outdated
item_type = object.paper_trail.versions_association_item_type | ||
if item_type.blank? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a note, I had been doing this test for blank because when versions_association_item_type
referred to the base_class then I had returned blank so that it wouldn't have to go through the unscope / where changes to the relation. If this overhead is acceptable then no worries.
lib/paper_trail/record_trail.rb
Outdated
if type_name == @record.class.base_class.name | ||
"" | ||
if item_type == @record.class.base_class.name | ||
nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(In connection with the test for .blank? from 184)
Not sure how much performance loss might be experienced, but the blank was an attempt to avoid the unscope / scope when item_type referred to the base_class. For many STI implementations perhaps it doesn't matter because the majority of things might be subclassed, but in some cases it could provide a benefit. (Family / CelebrityFamily kind of examples.)
::PaperTrail.config.classes_warned_about_sti_item_types ||= [] | ||
return if ::PaperTrail.config.classes_warned_about_sti_item_types.include?(record_class) | ||
|
||
::Kernel.warn(format(E_STI_ITEM_TYPES_NOT_UPDATED, record_class.name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this message! Thanks for putting this part together.
Once I better understood that caching issue, you might have seen that I had put in a sleep(2). For a good day I had toyed around with LOTS of different options, even a temporary monkey patching of Rails just for that test! Boy was that messy. After thinking more about it, another option we might consider is to use TimeCop. But I do like the approach you've arrived upon. |
The I'll go ahead and merge this, and I hope you can help test it by pointing your apps at Thanks for your hard work and patience on this. |
In my application, I have a While this PR fixes issues for those using more complex STI features, it breaks my basic use-case. A distinguishing factor is that I don't have a |
First let me say that I like the minimalist approach that is a hallmark of your suggestions -- we have a large-scale app that will benefit from your involvement around object / object_changes. In this case about using STI without So that I can be certain about the goal -- here's my attempt at a failing test. In the dummy app in the test suite,
Then perhaps this would this describe your expected behaviour:
(And if so then I would certainly like to add in examples for update and destroy as well.) To enact a fix then perhaps in
Whadya think? |
In #1137 I ended up fixing the bug (I think), but haven't added a test for it yet. I'd appreciate if you could review that PR 😄 |
This partially reverts commit 58369e1. I have kept the specs, skipped. Per the following, this approach does not seem to be working: - #1135 - #1137 - seanlinsley#1
…se STI (paper-trail-gem#1108) See the changes to the changelog and readme for details.
This partially reverts commit 58369e1. I have kept the specs, skipped. Per the following, this approach does not seem to be working: - paper-trail-gem#1135 - paper-trail-gem#1137 - seanlinsley#1
v10.0.0 * tag 'v10.0.0': (40 commits) Release 10.0.0 Testing the :skip option allow `object_changes_adapter` to use the default behavior Lint: Improve Metrics/AbcSize from 22 to 21 Docs: which class attributes are public/private Docs: installation Docs: Update changelog and readme re: paper-trail-gem#1143 Generator to update historic item_subtype entries (paper-trail-gem#1144) Testing joins, as recommended by Sean Add optional column: item_subtype Revert paper-trail-gem#1108 (lorint's STI fix) Lint: RSpec/EmptyLineAfterExampleGroup Update development dependencies Lint: RSpec/InstanceVariable in model_spec, ctn'd Code style re: errors Docs: Fix link to bug report Add association tracking removal exception Readme fix: caller.find {} rather than caller.first {} Do not require PT-AT Docs: Organizing the changelog for 10.0.0 ...
v10.0.0 * tag 'v10.0.0': (40 commits) Release 10.0.0 Testing the :skip option allow `object_changes_adapter` to use the default behavior Lint: Improve Metrics/AbcSize from 22 to 21 Docs: which class attributes are public/private Docs: installation Docs: Update changelog and readme re: paper-trail-gem#1143 Generator to update historic item_subtype entries (paper-trail-gem#1144) Testing joins, as recommended by Sean Add optional column: item_subtype Revert paper-trail-gem#1108 (lorint's STI fix) Lint: RSpec/EmptyLineAfterExampleGroup Update development dependencies Lint: RSpec/InstanceVariable in model_spec, ctn'd Code style re: errors Docs: Fix link to bug report Add association tracking removal exception Readme fix: caller.find {} rather than caller.first {} Do not require PT-AT Docs: Organizing the changelog for 10.0.0 ...
This update provides a fix for Issue #594 -- Reify on associations fails if using Single Table Inheritance.
In order to properly reify a version of a model using STI, item_type can refer directly to the class name instead of being base_class. When making this change, the five reifiers in the gem paper_trail-association_tracking that refer to
base_class
can also be updated, which is covered in PT-AT PR #5. With these changes, the previously failing example in person_spec.rb will pass.One key benefit from this change is to allow PT to represent STI model names. So if you have for instance the model "Person", there could be subclasses for "Doctor" and "Patient". The subclassed name gets put into
item_type
so that audit logs can be more clear.