Skip to content

Custom RevisionEntity class fails to provide RevisionNumber and RevisionTimestamp #250

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

Closed
androidbod opened this issue Aug 9, 2020 · 7 comments

Comments

@androidbod
Copy link

androidbod commented Aug 9, 2020

I have an issue with Spring Data Envers when using a Custom RevisionEntity class for storing revision information in Hibernate Envers.

Here is my Custom RevisionEntity - works well with an H2 Database for testing purposes - up to Spring Boot 2.2.7 it worked flawlessly.

package <TBD>;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import org.hibernate.envers.RevisionEntity;
import org.hibernate.envers.RevisionNumber;
import org.hibernate.envers.RevisionTimestamp;

import javax.persistence.*;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "audit_trail" /*, schema = "history"*/)
@RevisionEntity
@GenericGenerator(name = "audit_trail_sequence",
        strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
        parameters = {
            @Parameter(name = "sequence_name",value = "audit_trail_sequence"),
            @Parameter(name = "initial_value",value = "1"),
            @Parameter(name = "increment_size",value = "1")
        })
public class CustomRevisionEntity
{

    // =========================== Class Variables ===========================
    // =============================  Variables  =============================
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "audit_trail_sequence")
    @RevisionNumber
    private Long id;

    @RevisionTimestamp
    private Long timestamp;
}

I'm using a custom revision entity to allow for more than int limited number of revisions - hence I'm not using Hibernate's DefaultRevisionEntity - an extension of this class would not allow for changing the @RevisionNumber field type.

For Spring Boot 2.3.X (tested with 2.3.1 and 2.3.2):
Spring Data Envers' EnversRevisionRepositoryImpl uses the AnnotationRevisionMetadata class to extract and provide revision metadata for any revision loaded from the database. Through debugging I found out that, when aAnnotationRevisionMetadata instance is constructed for a revision, the @RevisionNumber and the @RevisionTimestamp field values are available, however the static methods constructing the respective Lazy suppliers will only provide appropriate Supplier functions for this information.

Somehow in the timespan from construction of these Lazy suppliers to actual usage of these suppliers, the information is "lost" along the way - currently I assume it has something to do with the fact, that my CustomRevisionEntity is in fact a HibernateProxy of the actual CustomRevisionEntity instance.

Generally this seems to have something to do with the deferred loading that is happening through the respectively constructed Lazy suppliers operating on these HibernateProxy objects which are liable to "loose" some information in the previously described timespan (possibly different Hibernate Sessions for Entity and Revision Infomation loading ? - just a thought).

I've tried wrapping the respective EnversRevisionRepositoryImpl calls into transactional business methods, but TransationContext or not - the result remains unchanged - the revision number and timestamp are not available for use after the EnversRevisionRepositoryImpl getRevision information methods return their respective result.

One possible solution that occured to me would be to call Hibernate.unproxy on the entity constructor parameter of the AnnotationRevisionMetadata from EnversRevisionRepositoryImpl.createRevisionMetadata.

		RevisionMetadata<?> createRevisionMetadata() {

			return metadata instanceof DefaultRevisionEntity //
					? new DefaultRevisionMetadata((DefaultRevisionEntity) metadata, revisionType) //
					: new AnnotationRevisionMetadata<>(Hibernate.unproxy(metadata), RevisionNumber.class, RevisionTimestamp.class, revisionType);
		}

I've tested that idea - it works - though I'm unsure if there are any performance related side effects from that solution.
I'm also considering whether or not the DefaultRevisionEntity provided by Envers could be subject to the same issue of being returned by Hibernate as a HibernateProxy instance.

If there are any questions, please contact me through Github.

Thank you

@androidbod
Copy link
Author

androidbod commented Aug 11, 2020

Never mind the issue - silly me didn't read up on the Hibernate @Proxy annotation - which can force Hibernate to always return unproxied entity instances for the annotated entity class. Add @Proxy(lazy = false) to the CustomRevisionEntity and its all good

@ddieterly
Copy link

Never mind the issue - silly me didn't read up on the Hibernate @Proxy annotation - which can force Hibernate to always return unproxied entity instances for the annotated entity class. Add @Proxy(lazy = false) to the CustomRevisionEntity and its all good

THANK YOU!

@albertus82
Copy link

Thank you @androidbod, adding lazy = false solves this issue, but it should be noted that this solution seems to introduce a n+1 query problem on RevisionRepository.findRevisions(id).

@AlexandreCassagne
Copy link

@albertus82 By some coincidence I'm implementing a custom revision entity today as well - do you know if there is a way to avoid the n+1 query problem with lazy = false?

@androidbod
Copy link
Author

androidbod commented Sep 3, 2021

@AlexandreCassagne - I don't think there is a way to avoid the n+1 query 'problem' when using @Proxy(lazy = false). Thinking about this - even if proxied Custom Revision entities would work correctly with Springs Lazy suppliers for Revision Number and Revision Timestamp, their usage would be subject to the same problem - at least for all Revisions that the Revision Number and/or the Revision Timestamp would be accessed via Springs Lazy suppliers - which would cause the respective RevisionEntity Hibernate Proxies to query the database at that point in time instead of querying this information at RevisionRepository.findRevisions method call time.

I would recommend to query revision information in a paginated fashion which should limit this problem by limiting the amount of revisions retrieved from the database for a single RevisionRepository.findRevisions call. Try to avoid using Springs Pagable.UNPAGED if at all applicable.

See:
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/history/RevisionRepository.html#findRevisions-ID-org.springframework.data.domain.Pageable-

This will only mitigate the problem - it will not solve it. The RevisionRepository methods that retrieve the latest Revision / retrieve a specific revision by Entity Id and RevisionNumber are of course impacted by this as well but as it concerns only a single Revision per method call - the n+1 problem should be - imho - negligable for those calls.

@albertus82
Copy link

@androidbod I'm not sure of this. I noticed that using the DefaultRevisionEntity (i.e. not providing a custom revision entity), no additional query is issued when accessing basic revision info (number and timestamp). If you want, you can try writing a simple test code that prints the toString of each revision found by findRevisions, and you'll notice the difference with and without a custom revision entity.

@androidbod
Copy link
Author

androidbod commented Sep 3, 2021

@albertus82 I trust you on that - I assume that Custom RevisionEntites are handled differently from the DefaultRevisionEntity - they are definitly handled differently by Spring (see original Issue description) but they are likely to be handled differnently by Hibernate as well. I would have assumed for Hibernate to just JOIN the Revision metadata independent of the RevisionEntity type, but for Custom Revision Entites this does not seem to be the case. Though I can't be certain if this is a Spring Issue, Hibernate Issue or Hibernate Envers Issue.

Update
I'm not even sure that this really is an issue at all - Custom Revision Entites are also designed to store additional metainformation with the Revision metadata of Envers (e.g Username of modifier or other Revision metadata that the developer wants to store). Perhaps this all is designed to work as it does. Then again - maybe it really is an issue that could be improved upon.

See: https://docs.jboss.org/envers/docs/
Chapter 4 briefly discusses Custom Revision Entites

Skimmed the Hibernate Envers Quickstart guide docs - nothing that would indicate that Hibernate handles Custom Revision Entites differently - but that does not mean that Hibernate doesn't handle them differently.

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

No branches or pull requests

4 participants