Skip to content

HHH-20214 Full @Audited entities read/write support in core#12191

Open
mbellade wants to merge 1 commit intohibernate:mainfrom
mbellade:core-audited
Open

HHH-20214 Full @Audited entities read/write support in core#12191
mbellade wants to merge 1 commit intohibernate:mainfrom
mbellade:core-audited

Conversation

@mbellade
Copy link
Copy Markdown
Member

@mbellade mbellade commented Apr 16, 2026

https://hibernate.atlassian.net/browse/HHH-20214

Read and write support for @Audited entities modeled after hibernate-envers:

  • write support for inheritance hierarchies, secondary tables and collections with semantic diff
  • AuditLog API, alternative to AuditReader for built-in query functionality
  • native HQL support with transactionId() and modificationType() functions
  • ALL_REVISIONS mode for historical queries (fetch all past revisions)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license
and can be relicensed under the terms of the LGPL v2.1 license in the future at the maintainers' discretion.
For more information on licensing, please check here.



Please make sure that the following tasks are completed:
Tasks specific to HHH-20214 (Improvement):

  • Add tests for feature/improvement
  • Update documentation as relevant: javadoc for changed API, documentation/src/main/asciidoc/userguide for all features, documentation/src/main/asciidoc/introduction for main features, links from existing documentation
  • Add entries as relevant to migration-guide.adoc (breaking changes) and whats-new.adoc (new features/improvements)

@mbellade mbellade force-pushed the core-audited branch 2 times, most recently from ef5e845 to 200ed3d Compare April 16, 2026 16:15
Comment thread hibernate-core/src/main/java/org/hibernate/action/internal/EntityAction.java Outdated
@mbellade mbellade force-pushed the core-audited branch 3 times, most recently from f737bd0 to f0e3c69 Compare April 18, 2026 07:59
Comment thread hibernate-core/src/main/java/org/hibernate/annotations/Audited.java Outdated
@mbellade mbellade force-pushed the core-audited branch 2 times, most recently from 8dd9368 to 08543a0 Compare April 18, 2026 11:58
Comment thread hibernate-core/src/main/java/org/hibernate/annotations/Audited.java Outdated
Comment thread hibernate-core/src/main/java/org/hibernate/action/internal/EntityAction.java Outdated
@GeneratedValue
@RevisionEntity.TransactionId
@Column(name = "REV")
private int id;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe long would be more appropriate?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I copied the mapping from the existing Envers one and left it as-is for maximum compatibility, but I guess a long mapping should read/write fine to existing schemas anyway, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure that most JDBC drivers are capable of implicitly converting BIGINT to INTEGER on write and the other way around on read.

Comment thread hibernate-core/src/main/java/org/hibernate/annotations/Audited.java Outdated
Comment thread hibernate-core/src/main/java/org/hibernate/audit/spi/RevisionEntitySupplier.java Outdated
Comment thread hibernate-core/src/main/java/org/hibernate/annotations/Audited.java
@mbellade mbellade force-pushed the core-audited branch 2 times, most recently from a737470 to 3e24736 Compare April 22, 2026 09:30
Comment thread hibernate-core/src/main/java/org/hibernate/annotations/Audited.java
Comment thread hibernate-core/src/main/java/org/hibernate/audit/spi/RevisionEntitySupplier.java Outdated
@sonarqubecloud
Copy link
Copy Markdown


@RevisionEntity.Timestamp
@Column(name = "REVTSTMP")
private long timestamp = System.currentTimeMillis();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use @CreationTimestamp here instead? That way, a user would also have the possibility to influence the clock, if needed during testing.

Copy link
Copy Markdown
Member Author

@mbellade mbellade Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny thing: I've tried, but it seems like long is not handled by our CurrentTimestampGeneration for the SourceType.VM (in-memory) generation case, and we fallback to the DB but there we get an error for incompatible types (localtimestamp vs BIGINTEGER).

Edit: should I change that while I'm at it, or is System.currentTimeMillis() fine for now? Since the revision entity instance is created just moments before persisting / flushing it, it should be effectively the same.

public final class AuditHelper {
public static final String TRANSACTION_ID = "transactionId";
public static final String MODIFICATION_TYPE = "modificationType";
public static final String TRANSACTION_END = "transactionEnd";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably also align the naming here, no?

Suggested change
public static final String TRANSACTION_END = "transactionEnd";
public static final String TRANSACTION_END_ID = "transactionEndId";

if ( data.allRevisions ) {
data.getRowProcessingState().getSession()
.getLoadQueryInfluencers()
.setTemporalIdentifier( ALL_REVISIONS );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible, that an outer load sets one transaction id, and then this inner load resets this to ALL_REVISIONS, causing problems for subsequent initializers?

I'm thinking about a EntityInitializer containing a EntitySelectFetchInitializer, then containing another EntityInitializer with ALL_REVISIONS. Maybe you should rather track the current temporal identifier where you set it within the Data object and set it back to the previous value here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, the data.allRevisions flag is only set for the root entity result initializer (the one that has the transactionIdAssembler), so it should only be reset after the entire row is read.

// Set the per-row temporal identifier so that association loads use the correct revision
data.getRowProcessingState().getSession()
.getLoadQueryInfluencers()
.setTemporalIdentifier( data.entityKey.getTransactionId() );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method also being called for all the other initialization paths, like when the entity comes from the first level cache and is initialized or second level / query cache? My hunch is, that we might also have to do this call in the other resolveInstance methods.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you talking about the resolveInstance(Object, Data)? if that's the one, that is only for associated entities from a root initializer that found an instance already in the PC, but the root initializer itself would have called the resolveInstance(data) version which sets the tx-id.

Also resolveInstanceFromCache as well is called after we go through here, so I think we're fine in all cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants