From a260006d7697bd271452020db22dcd7ff177b15f Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Wed, 26 Jul 2017 17:26:41 +0300 Subject: [PATCH] HHH-11886 - Elaborate Envers documentation and switch to actual source code examples Add example to configure the ValidityAuditStrategy --- .../userguide/chapters/envers/Envers.adoc | 85 ++++++-- ...nvers-audited-validity-mapping-example.sql | 34 +++ .../{AuditTest.java => DefaultAuditTest.java} | 6 +- .../envers/ValidityStrategyAuditTest.java | 202 ++++++++++++++++++ 4 files changed, 303 insertions(+), 24 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-audited-validity-mapping-example.sql rename documentation/src/test/java/org/hibernate/userguide/envers/{AuditTest.java => DefaultAuditTest.java} (95%) create mode 100644 documentation/src/test/java/org/hibernate/userguide/envers/ValidityStrategyAuditTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc b/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc index 146e1645ae..b20760635e 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc @@ -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] +---- + +---- + +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. diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-audited-validity-mapping-example.sql b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-audited-validity-mapping-example.sql new file mode 100644 index 0000000000..3e8088a10a --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-audited-validity-mapping-example.sql @@ -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 \ No newline at end of file diff --git a/documentation/src/test/java/org/hibernate/userguide/envers/AuditTest.java b/documentation/src/test/java/org/hibernate/userguide/envers/DefaultAuditTest.java similarity index 95% rename from documentation/src/test/java/org/hibernate/userguide/envers/AuditTest.java rename to documentation/src/test/java/org/hibernate/userguide/envers/DefaultAuditTest.java index d2f98b5e06..97f1d761f0 100644 --- a/documentation/src/test/java/org/hibernate/userguide/envers/AuditTest.java +++ b/documentation/src/test/java/org/hibernate/userguide/envers/DefaultAuditTest.java @@ -12,18 +12,14 @@ 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 @@ /** * @author Vlad Mihalcea */ -public class AuditTest extends BaseEntityManagerFunctionalTestCase { +public class DefaultAuditTest extends BaseEntityManagerFunctionalTestCase { @Override protected Class[] getAnnotatedClasses() { diff --git a/documentation/src/test/java/org/hibernate/userguide/envers/ValidityStrategyAuditTest.java b/documentation/src/test/java/org/hibernate/userguide/envers/ValidityStrategyAuditTest.java new file mode 100644 index 0000000000..ba6bf631ef --- /dev/null +++ b/documentation/src/test/java/org/hibernate/userguide/envers/ValidityStrategyAuditTest.java @@ -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 . + */ +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 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[] +}