From f0ccd7126a123d78e19083e24f80bd0474b73b65 Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Thu, 27 Jul 2017 16:08:49 +0300 Subject: [PATCH] HHH-11886 - Elaborate Envers documentation and switch to actual source code examples Refactor the Revision log example with actual test cases --- .../userguide/chapters/envers/Envers.adoc | 123 +++++++------ ...sionlog-RevisionEntity-persist-example.sql | 36 ++++ ...g-custom-revision-entity-table-example.sql | 6 + .../envers/CustomRevisionEntityTest.java | 169 ++++++++++++++++++ 4 files changed, 278 insertions(+), 56 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-revisionlog-RevisionEntity-persist-example.sql create mode 100644 documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-revisionlog-custom-revision-entity-table-example.sql create mode 100644 documentation/src/test/java/org/hibernate/userguide/envers/CustomRevisionEntityTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc b/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc index a625afcbad..91522e67cf 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc @@ -388,16 +388,19 @@ Compared to the default strategy, the `ValidityAuditStrategy` generates simpler === Revision Log When Envers starts a new revision, it creates a new revision entity which stores information about the revision. + By default, that includes just: -revision number:: An integral value (`int/Integer` or `long/Long`). Essentially the primary key of the revision - -revision timestamp:: either a `long/Long` or `java.util.Date` value representing the instant at which the revision was made. - When using a `java.util.Date`, instead of a `long/Long` for the revision timestamp, take care not to store it to a column data type which will loose precision. +revision number:: +An integral value (`int/Integer` or `long/Long`). Essentially the primary key of the revision +revision timestamp:: +Either a `long/Long` or `java.util.Date` value representing the instant at which the revision was made. +When using a `java.util.Date`, instead of a `long/Long` for the revision timestamp, take care not to store it to a column data type which will loose precision. Envers handles this information as an entity. By default it uses its own internal class to act as the entity, mapped to the `REVINFO` table. -You can, however, supply your own approach to collecting this information which might be useful to capture additional details such as who made a change or the ip address from which the request came. +You can, however, supply your own approach to collecting this information which might be useful to capture additional details such as who made a change +or the ip address from which the request came. There are two things you need to make this work: . First, you will need to tell Envers about the entity you wish to use. @@ -418,73 +421,81 @@ If your `RevisionListener` class is inaccessible from `@RevisionEntity` (e.g. it set `org.hibernate.envers.revision_listener` property to its fully qualified class name. Class name defined by the configuration parameter overrides revision entity's value attribute. +Considering we have a `CurrentUser` utility which stores the current logged user: + +[[envers-revisionlog-CurrentUser-example]] +.`CurrentUser` utility ==== -[source,java] +[source, JAVA, indent=0] ---- -@RevisionEntity( MyCustomRevisionListener.class ) -public class MyCustomRevisionEntity { - ... -} - -public class MyCustomRevisionListener - implements RevisionListener { - - public void newRevision( Object revisionEntity ) { - MyCustomRevisionEntity customRevisionEntity = - ( MyCustomRevisionEntity ) revisionEntity; - } -} +include::{sourcedir}/CustomRevisionEntityTest.java[tags=envers-revisionlog-CurrentUser-example] ---- ==== -.ExampleRevEntity.java +Now, we need to provide a custom `@RevisionEntity` to store the currently logged user + +[[envers-revisionlog-RevisionEntity-example]] +.Custom `@RevisionEntity` example ==== -[source,java] +[source, JAVA, indent=0] ---- -package org.hibernate.envers.example; - -import org.hibernate.envers.RevisionEntity; -import org.hibernate.envers.DefaultRevisionEntity; - -import javax.persistence.Entity; - -@Entity -@RevisionEntity( ExampleListener.class ) -public class ExampleRevEntity extends DefaultRevisionEntity { - private String username; - - public String getUsername() { return username; } - public void setUsername( String username ) { this.username = username; } -} +include::{sourcedir}/CustomRevisionEntityTest.java[tags=envers-revisionlog-RevisionEntity-example] ---- ==== -.ExampleListener.java +With the custom `RevisionEntity` implementation in place, +we only need to provide the `RevisionEntity` implementation which acts as a factory +of `RevisionEntity` instances. + +[[envers-revisionlog-RevisionListener-example]] +.Custom `@RevisionListener` example ==== -[source,java] +[source, JAVA, indent=0] ---- -package org.hibernate.envers.example; - -import org.hibernate.envers.RevisionListener; -import org.jboss.seam.security.Identity; -import org.jboss.seam.Component; - -public class ExampleListener implements RevisionListener { - - public void newRevision( Object revisionEntity ) { - ExampleRevEntity exampleRevEntity = ( ExampleRevEntity ) revisionEntity; - Identity identity = - (Identity) Component.getInstance( "org.jboss.seam.security.identity" ); - - exampleRevEntity.setUsername( identity.getUsername() ); - } -} +include::{sourcedir}/CustomRevisionEntityTest.java[tags=envers-revisionlog-RevisionListener-example] ---- ==== -[NOTE] +When generating the database schema, Envers creates the following `RevisionEntity` table: + +[[envers-revisionlog-custom-revision-entity-table-example]] +.Auto-generated `RevisionEntity` Envers table ==== -An alternative method to using the `org.hibernate.envers.RevisionListener` is to instead call the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/AuditReader.html#getCurrentRevision-java.lang.Class-boolean-[`getCurrentRevision( Class revisionEntityClass, boolean persist )`] method of the `org.hibernate.envers.AuditReader` interface to obtain the current revision, and fill it with desired information. +[source, SQL, indent=0] +---- +include::{extrasdir}/envers-revisionlog-custom-revision-entity-table-example.sql[] +---- +==== + +You can see the `username` column in place. + +Now, when inserting a `Customer` entity, Envers generates the following statements: + +[[envers-revisionlog-RevisionEntity-persist-example]] +.Auditing using the custom `@RevisionEntity` instance +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/CustomRevisionEntityTest.java[tags=envers-revisionlog-RevisionEntity-persist-example] +---- + +[source, SQL, indent=0] +---- +include::{extrasdir}/envers-revisionlog-RevisionEntity-persist-example.sql[] +---- +==== + +As demonstrated by the example above, the username is properly set and propagated to the `CUSTOM_REV_INFO` table. + +[WARNING] +==== +**This strategy is deprecated since version 5.2 as an alternative is going to be provided in Hibernate Envers 6.0.** + +An alternative method to using the `org.hibernate.envers.RevisionListener` is to instead call the +[line-through]#https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/AuditReader.html#getCurrentRevision-java.lang.Class-boolean-[`getCurrentRevision( Class revisionEntityClass, boolean persist )`]# +method of the `org.hibernate.envers.AuditReader` interface to obtain the current revision, +and fill it with desired information. + The method accepts a `persist` parameter indicating whether the revision entity should be persisted prior to returning from this method: `true`:: ensures that the returned entity has access to its identifier value (revision number), but the revision entity will be persisted regardless of whether there are any audited entities changed. diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-revisionlog-RevisionEntity-persist-example.sql b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-revisionlog-RevisionEntity-persist-example.sql new file mode 100644 index 0000000000..7a3e12ae59 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-revisionlog-RevisionEntity-persist-example.sql @@ -0,0 +1,36 @@ +insert +into + Customer + (created_on, firstName, lastName, id) +values + (?, ?, ?, ?) + +-- binding parameter [1] as [TIMESTAMP] - [Thu Jul 27 15:45:00 EEST 2017] +-- binding parameter [2] as [VARCHAR] - [John] +-- binding parameter [3] as [VARCHAR] - [Doe] +-- binding parameter [4] as [BIGINT] - [1] + +insert +into + CUSTOM_REV_INFO + (timestamp, username, id) +values + (?, ?, ?) + +-- binding parameter [1] as [BIGINT] - [1501159500888] +-- binding parameter [2] as [VARCHAR] - [Vlad Mihalcea] +-- binding parameter [3] as [INTEGER] - [1] + +insert +into + Customer_AUD + (REVTYPE, created_on, firstName, lastName, id, REV) +values + (?, ?, ?, ?, ?, ?) + +-- binding parameter [1] as [INTEGER] - [0] +-- binding parameter [2] as [TIMESTAMP] - [Thu Jul 27 15:45:00 EEST 2017] +-- binding parameter [3] as [VARCHAR] - [John] +-- binding parameter [4] as [VARCHAR] - [Doe] +-- binding parameter [5] as [BIGINT] - [1] +-- binding parameter [6] as [INTEGER] - [1] diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-revisionlog-custom-revision-entity-table-example.sql b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-revisionlog-custom-revision-entity-table-example.sql new file mode 100644 index 0000000000..5e28840090 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-revisionlog-custom-revision-entity-table-example.sql @@ -0,0 +1,6 @@ +create table CUSTOM_REV_INFO ( + id integer not null, + timestamp bigint not null, + username varchar(255), + primary key (id) +) \ No newline at end of file diff --git a/documentation/src/test/java/org/hibernate/userguide/envers/CustomRevisionEntityTest.java b/documentation/src/test/java/org/hibernate/userguide/envers/CustomRevisionEntityTest.java new file mode 100644 index 0000000000..43ac06a8d5 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/userguide/envers/CustomRevisionEntityTest.java @@ -0,0 +1,169 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.userguide.envers; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NoResultException; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.Audited; +import org.hibernate.envers.DefaultRevisionEntity; +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionListener; +import org.hibernate.envers.configuration.EnversSettings; +import org.hibernate.envers.strategy.ValidityAuditStrategy; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; + +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class CustomRevisionEntityTest extends BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Customer.class, + CustomRevisionEntity.class + }; + } + + @Test + public void test() { + //tag::envers-revisionlog-RevisionEntity-persist-example[] + CurrentUser.INSTANCE.logIn( "Vlad Mihalcea" ); + + doInJPA( this::entityManagerFactory, entityManager -> { + Customer customer = new Customer(); + customer.setId( 1L ); + customer.setFirstName( "John" ); + customer.setLastName( "Doe" ); + + entityManager.persist( customer ); + } ); + + CurrentUser.INSTANCE.logOut(); + //end::envers-revisionlog-RevisionEntity-persist-example[] + } + + @Audited + @Entity(name = "Customer") + public static class Customer { + + @Id + private Long id; + + private String firstName; + + private String lastName; + + @Temporal( TemporalType.TIMESTAMP ) + @Column(name = "created_on") + @CreationTimestamp + private Date createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + //tag::envers-revisionlog-CurrentUser-example[] + public static class CurrentUser { + + public static final CurrentUser INSTANCE = new CurrentUser(); + + private static final ThreadLocal storage = new ThreadLocal<>(); + + public void logIn(String user) { + storage.set( user ); + } + + public void logOut() { + storage.remove(); + } + + public String get() { + return storage.get(); + } + } + //end::envers-revisionlog-CurrentUser-example[] + + //tag::envers-revisionlog-RevisionEntity-example[] + @Entity(name = "CustomRevisionEntity") + @Table(name = "CUSTOM_REV_INFO") + @RevisionEntity( CustomRevisionEntityListener.class ) + public static class CustomRevisionEntity extends DefaultRevisionEntity { + + private String username; + + public String getUsername() { + return username; + } + + public void setUsername( String username ) { + this.username = username; + } + } + //end::envers-revisionlog-RevisionEntity-example[] + + //tag::envers-revisionlog-RevisionListener-example[] + public static class CustomRevisionEntityListener implements RevisionListener { + + public void newRevision( Object revisionEntity ) { + CustomRevisionEntity customRevisionEntity = + ( CustomRevisionEntity ) revisionEntity; + + customRevisionEntity.setUsername( + CurrentUser.INSTANCE.get() + ); + } + } + //end::envers-revisionlog-RevisionListener-example[] +}