Rails 7.2 compatibility: Add README.md note about custom audit table names#726
Open
pond wants to merge 3 commits intocollectiveidea:mainfrom
Open
Rails 7.2 compatibility: Add README.md note about custom audit table names#726pond wants to merge 3 commits intocollectiveidea:mainfrom
pond wants to merge 3 commits intocollectiveidea:mainfrom
Conversation
pond
commented
Aug 28, 2024
|
|
||
| #### Changing the table name | ||
|
|
||
| If you use a custom audit model _and_ define a different table name inside it, you must mark the `Audited::Audit` superclass as abstract. Otherwise you may get exceptions from attempts to access the missing `audits` table, since ActiveRecord still "sees" a valid, non-abstract model class in `Audited::Audit` and may attempt to access information about its inferred related table. |
Author
There was a problem hiding this comment.
Yes, I'm aware this suggests an STI-related solution to a non-STI problem, but in this specific case I think it does make sense; we don't want the base class to be instantiated ever and we definitely want ActiveRecord to use the subclass's table name only.
TL;DR, there's some confusing ActiveRecord documentation around inheritance that could do with tidying up. I'd do a PR for that, but I don't actually know what the intended behaviour is anymore given recent Rails changes.
Author
|
Any traction on this? It now amounts to just an improvement to |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds information in the
README.mdabout changes that must be made to client code using a custom table name for audit data, which becomes necessary in Rails 7.2 but is harmless in earlier versions.This PR was intended to have code and tests, arising from Rails 7.2's new behaviour wherein (so long as
Audited::Auditis not marked abstract) it tries to access anauditstable. See rails/rails#52715. The rest of this PR description is only of interest for readers wondering why this can't be done automatically.Merging this would close #725 as unfixable.
Option 1:
def self.inheritedinsideAudited::AuditThe Rails team suggested just having
Audited::Auditself-mark itself as abstract upon inheritance. Notwithstanding arguments about it being unusual for the act of inheritance to silently mutate its superclass, the approach doesn't work because the Audited gem might want the base class to be concrete since the default behaviour uses the base class table name. The example given is:If we were to blindly mark
Audited::Auditas abstract, all such client code (the majority use case) would fail because ActiveRecord will start looking for a table name ofcustom_audits. We want it to keep using the base class's table name, as it would when using STI.This means we need to check the table name of the inheriting subclass, to find out if it differs from the superclass default. However, doing this inside
def self.inheritedbreaks since the class definition is mid-way through; ActiveRecord explodes (quite reasonably). We need to wait until after the subclass is fully defined and then enquire. The solution to that - and a surprisingly clean-reading and high performance one at that, given what it's actually doing under the hood! - is a Tracepoint.It looks something like this.
The
rescue nilis required because if you ask for the table name at this point when one is not being defined in the subclass, ActiveRecord again crashes out, but it's a benign failure. Nonetheless, while this does work there are obvious issues:Option 2: Custom writer for the
audit_classconfiguration entryThis was a world of pain! On the surface it's very simple, but we go deep, deep down the rabbit hole due to our old nemesis, the autoloader. The core of the issue isn't a custom writer - it's the default reader. In the
Auditedmodule, we have this (in-method comments removed for brevity):This is a reasonable implementation. The custom class is usually configured in
config/initializers/audited.rb, perREADME.md, but the autoloader hasn't loaded models yet. As per the documented example, the class name is given as a string - trying to reference the class directly would provokeNameErrorat this stage in initialization. The current gem implementation then, on reading, constantlysafe_constantize's that. So long as the inheriting model has yet to be autoloaded, this will yieldniland the Audited gem will act as if there's no configured custom class. Any gem code at all that reads through theaudit_classaccessor "too early" will believe thatAudited::Auditis the in-use audit model.Worse, in tests, it was often the case that even at the point where a Rails application's initializer is running and actually calling the write accessor for that config value, the
Auditedmodule was defined butAudited::Auditwas not. This led to the following merry dance inAudited:...with that "if initialized" check being present to account for the potential case where someone might be setting the custom class later, for any reason - a belt & braces approach. The above calls a new class method in
Audited::Audit, which I'd written in full including comments - before I realised this was a losing battle! - as:OK, so if
AuditedandAudited::Auditare both loaded and parsed before someone sets the custom class, everything will work. But what if that's not the case, as happened at times in dev test? Well, we can also address this from the "other side" inAudited::Auditby adding a line underneath the above method:...and that worked fine in our real-world case but not in a minimal test case (the application in the Rails issue); here, we get back to the problem that
AuditTrailis not yet autoloaded and theAudited.audit_classreader therefore returnsnilat this instant in execution.We're tying ourselves in knots with increasingly convoluted code, all of which feels like a hack; adding more layers to try and work around every possible load order feels like madness; this isn't a good approach.
Option 3: Hack the subclass
Inside
Audited::Auditwe decide to forget any notion of abstract classes. Rails 7.2 is really, really keen on usingAudited::Audit's table name - so let's set it!The code above runs at, essentially, the instant that the parser is running through
class CustomAudit < Audited::Audit, but before the class body is being parsed. This means we overrideCustomAudit'stable_namewriter before the class has a chance to set a custom table name, if it's going to. Should it do so, theAudited::Auditbase class table name is set to match. It feels fragile but works, however, it does make testing difficult because - again - we are essentially mutating global properties insideAudited::Auditjust because of some seemingly innocuous action inside a subclass where a dev would normally never expect such a side effect.Behaviour is now close to standard; things are running in sort-of-STI mode all the time, just like they were before. Really, this is the most likely-to-work approach except:
The least-surprise violation again; setting a property in a subclass mutates the superclass too. That's not clever.
It's also nasty redefining the
table_name=method in the subclass because it might for whatever reason already have its own overriding implementation and we'd squash that.Conclusion
selfabstract indef self.inherited- but that is incorrect.README.md. Any client of Audited on Rails 7.2 or later using a custom table name will have to figure out what's wrong and fix it manually.