HHH-11886 - Elaborate Envers documentation and switch to actual source code examples

Refactor the Revision log example with actual test cases
This commit is contained in:
Vlad Mihalcea 2017-07-27 16:08:49 +03:00
parent 582bd94530
commit f0ccd7126a
4 changed files with 278 additions and 56 deletions

View File

@ -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<T> 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<T> 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.

View File

@ -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]

View File

@ -0,0 +1,6 @@
create table CUSTOM_REV_INFO (
id integer not null,
timestamp bigint not null,
username varchar(255),
primary key (id)
)

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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<String> 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[]
}