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

Add example to configure the ValidityAuditStrategy
This commit is contained in:
Vlad Mihalcea 2017-07-26 17:26:41 +03:00
parent 7c5213171b
commit a260006d76
4 changed files with 303 additions and 24 deletions

View File

@ -3,6 +3,7 @@
:sourcedir: ../../../../../test/java/org/hibernate/userguide/envers
:extrasdir: extras
[[envers-basics]]
=== Basics
To audit changes that are performed on an entity, you only need two things:
@ -36,7 +37,7 @@ Hibernate is going to generate the following tables using the `hibernate.hbm2ddl
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-mapping-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-mapping-example]
----
[source, SQL, indent=0]
@ -56,7 +57,7 @@ let's see how Envers auditing works when inserting, updating, and deleting the e
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-insert-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-insert-example]
----
[source, SQL, indent=0]
@ -70,7 +71,7 @@ include::{extrasdir}/envers-audited-insert-example.sql[]
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-update-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-update-example]
----
[source, SQL, indent=0]
@ -84,7 +85,7 @@ include::{extrasdir}/envers-audited-update-example.sql[]
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-delete-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-delete-example]
----
[source, SQL, indent=0]
@ -112,7 +113,7 @@ The audit (history) of an entity can be accessed using the `AuditReader` interfa
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-revisions-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-revisions-example]
----
[source, SQL, indent=0]
@ -128,7 +129,7 @@ Using the previously fetched revisions, we can now inspect the state of the `Cus
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-rev1-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-rev1-example]
----
[source, SQL, indent=0]
@ -151,7 +152,7 @@ The same goes for the second revision associated to the `UPDATE` statement.
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-rev2-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-rev2-example]
----
====
@ -162,7 +163,7 @@ For the deleted entity revision, Envers throws a `NoResultException` since the e
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-rev3-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-rev3-example]
----
====
@ -176,7 +177,7 @@ all attributes, except for the entity identifier, are going to be `null`.
====
[source, JAVA, indent=0]
----
include::{sourcedir}/AuditTest.java[tags=envers-audited-rev4-example]
include::{sourcedir}/DefaultAuditTest.java[tags=envers-audited-rev4-example]
----
====
@ -270,6 +271,7 @@ be regarded as experimental:
. `org.hibernate.envers.original_id_prop_name`
====
[[envers-additional-mappings]]
=== Additional mapping annotations
The name of the audit table can be set on a per-entity basis, using the `@AuditTable` annotation.
@ -299,6 +301,7 @@ you can set the `@AuditOverride( forClass = SomeEntity.class, isAudited = true/f
The `@Audited` annotation also features an `auditParents` attribute but it's now deprecated in favor of `@AuditOverride`,
====
[[envers-audit-strategy]]
=== Choosing an audit strategy
After the basic configuration, it is important to choose the audit strategy that will be used to persist and retrieve audit information.
@ -306,21 +309,63 @@ There is a trade-off between the performance of persisting and the performance o
Currently, there are two audit strategies.
. The default audit strategy persists the audit data together with a start revision.
For each row inserted, updated or deleted in an audited table, one or more rows are inserted in the audit tables, together with the start revision of its validity.
Rows in the audit tables are never updated after insertion.
Queries of audit information use subqueries to select the applicable rows in the audit tables.
For each row inserted, updated or deleted in an audited table, one or more rows are inserted in the audit tables, together with the start revision of its validity.
Rows in the audit tables are never updated after insertion.
Queries of audit information use subqueries to select the applicable rows in the audit tables.
+
IMPORTANT: These subqueries are notoriously slow and difficult to index.
. The alternative is a validity audit strategy.
This strategy stores the start-revision and the end-revision of audit information.
For each row inserted, updated or deleted in an audited table, one or more rows are inserted in the audit tables, together with the start revision of its validity.
But at the same time the end-revision field of the previous audit rows (if available) are set to this revision.
Queries on the audit information can then use 'between start and end revision' instead of subqueries as used by the default audit strategy.
This strategy stores the start-revision and the end-revision of audit information.
For each row inserted, updated or deleted in an audited table, one or more rows are inserted in the audit tables, together with the start revision of its validity.
But at the same time the end-revision field of the previous audit rows (if available) are set to this revision.
Queries on the audit information can then use 'between start and end revision' instead of subqueries as used by the default audit strategy.
+
The consequence of this strategy is that persisting audit information will be a bit slower because of the extra updates involved,
but retrieving audit information will be a lot faster.
This can be improved even further by adding extra indexes.
The consequence of this strategy is that persisting audit information will be a bit slower because of the extra updates involved,
but retrieving audit information will be a lot faster.
+
IMPORTANT: This can be improved even further by adding extra indexes.
[[envers-audit-ValidityAuditStrategy]]
==== Configuring the `ValidityAuditStrategy`
To better visualize how the `ValidityAuditStrategy`, consider the following exercise where
we replay the previous audit logging example for the `Customer` entity.
First, you need to configure the `ValidityAuditStrategy`:
[[envers-audited-validity-configuration-example]]
.Configuring the `ValidityAuditStrategy`
====
[source, JAVA, indent=0]
----
include::{sourcedir}/ValidityStrategyAuditTest.java[tags=envers-audited-validity-configuration-example]
----
====
If, you're using the `persistence.xml` configuration file,
then the mapping will looks as follows:
[source, XML, indent=0]
----
<property
name="org.hibernate.envers.audit_strategy"
value="org.hibernate.envers.strategy.ValidityAuditStrategy"
/>
----
Once you configured the `ValidityAuditStrategy`, the following schema is going to be automatically generated:
[[envers-audited-validity-mapping-example]]
.Envers schema for the `ValidityAuditStrategy`
====
[source, SQL, indent=0]
----
include::{extrasdir}/envers-audited-validity-mapping-example.sql[]
----
====
As you can see, the `REVEND` column is added as well as its Foreign key to the `REVINFO` table.
[[envers-revisionlog]]
=== Revision Log
@ -900,6 +945,7 @@ AuditQuery query = getAuditReader().createQuery()
.add( AuditEntity.property( "p", "age" ).eqProperty( "a", "streetNumber" ) );
----
[[envers-conditional-auditing]]
=== Conditional auditing
Envers persists audit data in reaction to various Hibernate events (e.g. `post update`, `post insert`, and so on), using a series of event listeners from the `org.hibernate.envers.event.spi` package.
@ -926,6 +972,7 @@ The use of `hibernate.listeners.envers.autoRegister` has been deprecated. A new
`hibernate.envers.autoRegisterListeners` should be used instead.
====
[[envers-schema]]
=== Understanding the Envers Schema
For each audited entity (that is, for each entity containing at least one audited field), an audit table is created.

View File

@ -0,0 +1,34 @@
create table Customer (
id bigint not null,
created_on timestamp,
firstName varchar(255),
lastName varchar(255),
primary key (id)
)
create table Customer_AUD (
id bigint not null,
REV integer not null,
REVTYPE tinyint,
REVEND integer,
created_on timestamp,
firstName varchar(255),
lastName varchar(255),
primary key (id, REV)
)
create table REVINFO (
REV integer generated by default as identity,
REVTSTMP bigint,
primary key (REV)
)
alter table Customer_AUD
add constraint FK5ecvi1a0ykunrriib7j28vpdj
foreign key (REV)
references REVINFO
alter table Customer_AUD
add constraint FKqd4fy7ww1yy95wi4wtaonre3f
foreign key (REVEND)
references REVINFO

View File

@ -12,18 +12,14 @@ import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.Audited;
import org.hibernate.envers.exception.AuditException;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.test.legacy.Custom;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
@ -34,7 +30,7 @@ import static org.junit.Assert.fail;
/**
* @author Vlad Mihalcea
*/
public class AuditTest extends BaseEntityManagerFunctionalTestCase {
public class DefaultAuditTest extends BaseEntityManagerFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {

View File

@ -0,0 +1,202 @@
/*
* 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.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.Audited;
import org.hibernate.envers.configuration.EnversSettings;
import org.hibernate.envers.strategy.ValidityAuditStrategy;
import org.hibernate.jpa.AvailableSettings;
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 ValidityStrategyAuditTest extends BaseEntityManagerFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
Customer.class
};
}
@Override
protected void addConfigOptions(Map options) {
//tag::envers-audited-validity-configuration-example[]
options.put(
EnversSettings.AUDIT_STRATEGY,
ValidityAuditStrategy.class.getName()
);
//end::envers-audited-validity-configuration-example[]
}
@Test
public void test() {
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::envers-audited-insert-example[]
Customer customer = new Customer();
customer.setId( 1L );
customer.setFirstName( "John" );
customer.setLastName( "Doe" );
entityManager.persist( customer );
//end::envers-audited-insert-example[]
} );
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::envers-audited-update-example[]
Customer customer = entityManager.find( Customer.class, 1L );
customer.setLastName( "Doe Jr." );
//end::envers-audited-update-example[]
} );
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::envers-audited-delete-example[]
Customer customer = entityManager.getReference( Customer.class, 1L );
entityManager.remove( customer );
//end::envers-audited-delete-example[]
} );
//tag::envers-audited-revisions-example[]
List<Number> revisions = doInJPA( this::entityManagerFactory, entityManager -> {
return AuditReaderFactory.get( entityManager ).getRevisions(
Customer.class,
1L
);
} );
//end::envers-audited-revisions-example[]
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::envers-audited-rev1-example[]
Customer customer = (Customer) AuditReaderFactory.get( entityManager )
.createQuery()
.forEntitiesAtRevision( Customer.class, revisions.get( 0 ) )
.getSingleResult();
assertEquals("Doe", customer.getLastName());
//end::envers-audited-rev1-example[]
} );
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::envers-audited-rev2-example[]
Customer customer = (Customer) AuditReaderFactory.get( entityManager )
.createQuery()
.forEntitiesAtRevision( Customer.class, revisions.get( 1 ) )
.getSingleResult();
assertEquals("Doe Jr.", customer.getLastName());
//end::envers-audited-rev2-example[]
} );
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::envers-audited-rev3-example[]
try {
Customer customer = (Customer) AuditReaderFactory.get( entityManager )
.createQuery()
.forEntitiesAtRevision( Customer.class, revisions.get( 2 ) )
.getSingleResult();
fail("The Customer was deleted at this revision: " + revisions.get( 2 ));
}
catch (NoResultException expected) {
}
//end::envers-audited-rev3-example[]
} );
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::envers-audited-rev4-example[]
Customer customer = (Customer) AuditReaderFactory.get( entityManager )
.createQuery()
.forEntitiesAtRevision(
Customer.class,
Customer.class.getName(),
revisions.get( 2 ),
true )
.getSingleResult();
assertEquals( Long.valueOf( 1L ), customer.getId() );
assertNull( customer.getFirstName() );
assertNull( customer.getLastName() );
assertNull( customer.getCreatedOn() );
//end::envers-audited-rev4-example[]
} );
}
//tag::envers-audited-mapping-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;
//Getters and setters are omitted for brevity
//end::envers-audited-mapping-example[]
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-audited-mapping-example[]
}
//end::envers-audited-mapping-example[]
}