-
Notifications
You must be signed in to change notification settings - Fork 196
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
Issues with callbacks (infinite loops, record status...) #108
base: master
Are you sure you want to change the base?
Conversation
It would be really great to get this merged. |
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.
Hi @aymeric-ledorze, this looks good overall. I have just a few small comments.
Can you please rebase this on current master?
lib/acts_as_paranoid/core.rb
Outdated
@@ -2,6 +2,12 @@ module ActsAsParanoid | |||
module Core | |||
def self.included(base) | |||
base.extend ClassMethods | |||
if ActiveRecord::VERSION::MAJOR > 5 || (ActiveRecord::VERSION::MAJOR == 5 && (ActiveRecord::VERSION::MINOR >= 2 || (ActiveRecord::VERSION::MINOR == 2 && ActiveRecord::VERSION::TINY >= 1))) |
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'm not sure what (ActiveRecord::VERSION::MINOR == 2 && ActiveRecord::VERSION::TINY >= 1)
is supposed to do, because if the minor version is 2, then the previous check (ActiveRecord::VERSION::MINOR >= 2
) is also already true.
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 looks like a mistake. I wanted to run this code only with rails versions >= 5.2.1 but I forgot to add a strict comparison in the minor part.
lib/acts_as_paranoid/core.rb
Outdated
@@ -199,7 +210,7 @@ def destroy_dependent_associations! | |||
end | |||
|
|||
def deleted? | |||
!if self.class.string_type_with_deleted_value? | |||
@destroyed || !if self.class.string_type_with_deleted_value? |
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.
The logic here is already quite complex and in need of a clean-up. How about moving the @destroyed
check to a separate line, like:
return true if @destroyed
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.
No problem, but it seems that you already integrated it that way. There is no problem if you want to change it again.
lib/acts_as_paranoid/core.rb
Outdated
@@ -223,5 +240,28 @@ def get_reflection_class(reflection) | |||
def paranoid_value=(value) | |||
self.send("#{self.class.paranoid_column}=", value) | |||
end | |||
|
|||
def stale_paranoid_value |
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 name is confusing. How about mark_as_soft_deleted
or something like that?
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.
Yes, why not? I agree that the method name is confusing but the idea was that the record is actually already marked as soft deleted in the database and the goal of this method is to sync the loaded record with the database state.
test/test_core.rb
Outdated
assert_not ParanoidTime.with_deleted.first.deleted_fully? | ||
|
||
ParanoidString.first.destroy | ||
assert ParanoidString.with_deleted.first.deleted? |
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.
Shouldn't this be an assertion about #deleted_fully?
?
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.
Maybe. I'm sorry, I can't remember why I wrote that test this way... I think we could both test deleted? and not deleted_fully? for the two paranoid objects.
lib/acts_as_paranoid/core.rb
Outdated
base.send(:alias_method, :remember_transaction_record_state_without_paranoid, :remember_transaction_record_state) | ||
base.send(:alias_method, :remember_transaction_record_state, :remember_transaction_record_state_with_paranoid) | ||
end | ||
base.send(:alias_method, :force_clear_transaction_record_state_without_paranoid, :force_clear_transaction_record_state) |
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.
You can avoid the base.send ...
by using class << base
, as done in lib/acts_as_paranoid/associations.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.
It is not that simple because in your example, you redefined belongs_to which is a class method and here we are dealing with instance methods. We could use a class_eval however.
Since this a rather large set of changes, I'll be merging them in one by one. |
7c34449
to
9434f32
Compare
Sorry I did not respond earlier but I am rather busy at the moment. I would like to thank you for accepting this commit ! |
eef44d3
to
d81cdd8
Compare
I just noticed that the test about callbacks that I added failed with newer Rails versions. Actually, it failed only with Rails > 6.0.0 since Rails fixed issues about commit callbacks starting from version 6.0.1 (PR 37251). This PR fixes a lot of issues and therefore makes most of my changes completely useless with newer Rails versions. However, they are still needed with older versions. This PR is also merged in Rails 5.2, starting with version 5.2.4. As for the ActiveStorage test, Rails changed ActiveStorage loading so I had to add a "fake" Rails module to prevent a crash. But I also noticed that it will change again in future Rails version so I prefer to wait for the stabilisation of the Rails 6.1 branch before fixing it for the active_record_edge gemfile. |
d81cdd8
to
e0aa8ce
Compare
cc61e44
to
ac1df78
Compare
6d8d43c
to
a32759d
Compare
d2cca1d
to
b7783d0
Compare
b7783d0
to
735fef5
Compare
There are various issues when dealing with callbacks when destroying a record:
after_destroy
callback modifies the deleted record again, this may trigger new unwanted callbacks. Moreover, this prevents a record to be recovered after being destroyed without reloading it.after_destroy_commit
callbacks are not called when hard destroying a record.after_destroy_commit
callbacks are called twice. This is what triggers issue SystemStackError during destroy with ActiveStorage 5.2 #103.This PR solves all these problems with the following changes:
@destroyed
is set to true only when the record is removed from the database, ie indestroy_fully!
. This is how Rails manages destruction and knows that a record is no longer persisted. Thus,deleted?
indicates if a record is deleted (soft or hard) anddeleted_fully?
indicates if a record is hard deleted.@_trigger_destroy_callback
is now also defined when hard destroying a record.@_trigger_destroy_callback
variable is set back tonil
after soft destroying a record at the end of a transaction or when a new transaction starts. This is how Rails solved a similar issue when creating a record. This means that if a record is destroyed and updated in the same transaction, theafter_destroy_callbacks
are correctly called once only with the very last versions of Rails (5.2.1.rc1 as of now). Note that for Rails versions older than 5.1, the callbacks are still incorrectly called twice even with two separate transactions.This PR also adds a test with the destruction of a record with ActiveStorage attachments. The loading of ActiveStorage is kinda complicated and there might be room for improvement. The previous change fixes the
SystemStackError
described by issue #103 but does not prevents the destruction of attachments when soft destroying a record.There are still open issues. For example, there is still no way to know if a record is expected to be soft or hard deleted in a
before_destroy
callback. There is also an undefined behavior when destroying and recovering a record in the same transaction. Shouldafter_destroy_commit
be called? It currently depends on the Rails version...