From c25a859adeca1b4bee8719dc199b3e715d41b78c Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Thu, 31 Aug 2017 17:12:49 +0300 Subject: [PATCH] HHH-11886 - Elaborate Envers documentation and switch to actual source code examples Move code snippets to actual test cases for queries using modified flags --- .../userguide/chapters/envers/Envers.adoc | 89 ++++-- ...es-changes-queries-at-revision-example.sql | 25 ++ ...s-hasChanged-and-hasNotChanged-example.sql | 30 ++ ...ies-changes-queries-hasChanged-example.sql | 28 ++ .../QueryAuditWithModifiedFlagTest.java | 258 ++++++++++++++++++ 5 files changed, 400 insertions(+), 30 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-at-revision-example.sql create mode 100644 documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example.sql create mode 100644 documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-hasChanged-example.sql create mode 100644 documentation/src/test/java/org/hibernate/userguide/envers/QueryAuditWithModifiedFlagTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc b/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc index c5a1d93a8c..b60deb567b 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc @@ -619,7 +619,7 @@ Sometimes it is useful to store additional metadata for each revision, when you The feature described in <> makes it possible to tell which entities were modified in a given revision. The feature described here takes it one step further. -"Modification Flags" enable Envers to track which properties of audited entities were modified in a given revision. +_Modification Flags_ enable Envers to track which properties of audited entities were modified in a given revision. Tracking entity changes at property level can be enabled by: @@ -854,48 +854,77 @@ in which the entity was deleted should be included in the results. If yes, such entities will have the revision type `DEL` and all attributes, except the `id`, will be set to `null`. [[envers-tracking-properties-changes-queries]] -=== Querying for revisions of entity that modified given property +=== Querying for revisions of entity that modified a given property For the two types of queries described above it's possible to use special `Audit` criteria called `hasChanged()` and `hasNotChanged()` that makes use of the functionality described in <>. -They're best suited for vertical queries, however existing API doesn't restrict their usage for horizontal ones. -Let's have a look at following examples: +Let's have a look at various queries that can benefit from these two criteria. -[source,java] +First, you must make sure that your entity can track _modification flags_: + +[[envers-tracking-properties-changes-queries-entity-example]] +.Valid only when audit logging tracks entity attribute modification flags +==== +[source, JAVA, indent=0] ---- -AuditQuery query = getAuditReader().createQuery() - .forRevisionsOfEntity( MyEntity.class, false, true ) - .add( AuditEntity.id().eq( id ) ); - .add( AuditEntity.property( "actualDate" ).hasChanged() ); +include::{sourcedir}/QueryAuditWithModifiedFlagTest.java[tags=envers-tracking-properties-changes-queries-entity-example] +---- +==== + +The following query will return all revisions of the `Customer` entity with the given `id`, +for which the `lastName` property has changed. + +[[envers-tracking-properties-changes-queries-hasChanged-example]] +.Getting all `Customer` revisions for which the `lastName` attribute has changed +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/QueryAuditWithModifiedFlagTest.java[tags=envers-tracking-properties-changes-queries-hasChanged-example] ---- -This query will return all revisions of `MyEntity` with given `id`, where the `actualDate` property has been changed. -Using this query we won't get all other revisions in which `actualDate` wasn't touched. -Of course, nothing prevents user from combining `hasChanged` condition with some additional criteria - add method can be used here in a normal way. - -[source,java] +[source, SQL, indent=0] ---- -AuditQuery query = getAuditReader().createQuery() - .forEntitiesAtRevision( MyEntity.class, revisionNumber ) - .add( AuditEntity.property( "prop1" ).hasChanged() ) - .add( AuditEntity.property( "prop2" ).hasNotChanged() ); +include::{extrasdir}/envers-tracking-properties-changes-queries-hasChanged-example.sql[] +---- +==== + +Using this query we won't get all other revisions in which `lastName` wasn't touched. +From the SQL query you can see that the `lastName_MOD` column is being used in the WHERE clause, +hence the aforementioned requirement for tracking modification flags. + +Of course, nothing prevents user from combining `hasChanged` condition with some additional criteria. + +[[envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example]] +.Getting all `Customer` revisions for which the `lastName` attribute has changed and the `firstName` attribute has not changed +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/QueryAuditWithModifiedFlagTest.java[tags=envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example] ---- -This query will return horizontal slice for `MyEntity` at the time `revisionNumber` was generated. -It will be limited to revisions that modified `prop1` but not `prop2`. - -Note that the result set will usually also contain revisions with numbers lower than the `revisionNumber`, -so wem cannot read this query as "Give me all MyEntities changed in `revisionNumber` with `prop1` modified and `prop2` untouched". -To get such result we have to use the `forEntitiesModifiedAtRevision` query: - -[source,java] +[source, SQL, indent=0] ---- -AuditQuery query = getAuditReader().createQuery() - .forEntitiesModifiedAtRevision( MyEntity.class, revisionNumber ) - .add( AuditEntity.property( "prop1" ).hasChanged() ) - .add( AuditEntity.property( "prop2" ).hasNotChanged() ); +include::{extrasdir}/envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example.sql[] ---- +==== + +To get the `Customer` entities changed at a given `revisionNumber` with `lastName` modified and `firstName` untouched, +we have to use the `forEntitiesModifiedAtRevision` query: + +[[envers-tracking-properties-changes-queries-at-revision-example]] +.Getting the `Customer` entity for a given revision if the `lastName` attribute has changed and the `firstName` attribute has not changed +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/QueryAuditWithModifiedFlagTest.java[tags=envers-tracking-properties-changes-queries-at-revision-example] +---- + +[source, SQL, indent=0] +---- +include::{extrasdir}/envers-tracking-properties-changes-queries-at-revision-example.sql[] +---- +==== [[envers-tracking-modified-entities-queries]] === Querying for entities modified in a given revision diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-at-revision-example.sql b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-at-revision-example.sql new file mode 100644 index 0000000000..515c264526 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-at-revision-example.sql @@ -0,0 +1,25 @@ +select + queryaudit0_.id as id1_3_, + queryaudit0_.REV as REV2_3_, + queryaudit0_.REVTYPE as REVTYPE3_3_, + queryaudit0_.REVEND as REVEND4_3_, + queryaudit0_.created_on as created_5_3_, + queryaudit0_.createdOn_MOD as createdO6_3_, + queryaudit0_.firstName as firstNam7_3_, + queryaudit0_.firstName_MOD as firstNam8_3_, + queryaudit0_.lastName as lastName9_3_, + queryaudit0_.lastName_MOD as lastNam10_3_, + queryaudit0_.address_id as address11_3_, + queryaudit0_.address_MOD as address12_3_ +from + Customer_AUD queryaudit0_ +where + queryaudit0_.REV=? + and queryaudit0_.id=? + and queryaudit0_.lastName_MOD=? + and queryaudit0_.firstName_MOD=? + +-- binding parameter [1] as [INTEGER] - [2] +-- binding parameter [2] as [BIGINT] - [1] +-- binding parameter [3] as [BOOLEAN] - [true] +-- binding parameter [4] as [BOOLEAN] - [false] \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example.sql b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example.sql new file mode 100644 index 0000000000..dfd6f359fa --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example.sql @@ -0,0 +1,30 @@ +select + queryaudit0_.id as id1_3_0_, + queryaudit0_.REV as REV2_3_0_, + defaultrev1_.REV as REV1_4_1_, + queryaudit0_.REVTYPE as REVTYPE3_3_0_, + queryaudit0_.REVEND as REVEND4_3_0_, + queryaudit0_.created_on as created_5_3_0_, + queryaudit0_.createdOn_MOD as createdO6_3_0_, + queryaudit0_.firstName as firstNam7_3_0_, + queryaudit0_.firstName_MOD as firstNam8_3_0_, + queryaudit0_.lastName as lastName9_3_0_, + queryaudit0_.lastName_MOD as lastNam10_3_0_, + queryaudit0_.address_id as address11_3_0_, + queryaudit0_.address_MOD as address12_3_0_, + defaultrev1_.REVTSTMP as REVTSTMP2_4_1_ +from + Customer_AUD queryaudit0_ cross +join + REVINFO defaultrev1_ +where + queryaudit0_.id=? + and queryaudit0_.lastName_MOD=? + and queryaudit0_.firstName_MOD=? + and queryaudit0_.REV=defaultrev1_.REV +order by + queryaudit0_.REV asc + +-- binding parameter [1] as [BIGINT] - [1] +-- binding parameter [2] as [BOOLEAN] - [true] +-- binding parameter [3] as [BOOLEAN] - [false] \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-hasChanged-example.sql b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-hasChanged-example.sql new file mode 100644 index 0000000000..236eed03a1 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/extras/envers-tracking-properties-changes-queries-hasChanged-example.sql @@ -0,0 +1,28 @@ +select + queryaudit0_.id as id1_3_0_, + queryaudit0_.REV as REV2_3_0_, + defaultrev1_.REV as REV1_4_1_, + queryaudit0_.REVTYPE as REVTYPE3_3_0_, + queryaudit0_.REVEND as REVEND4_3_0_, + queryaudit0_.created_on as created_5_3_0_, + queryaudit0_.createdOn_MOD as createdO6_3_0_, + queryaudit0_.firstName as firstNam7_3_0_, + queryaudit0_.firstName_MOD as firstNam8_3_0_, + queryaudit0_.lastName as lastName9_3_0_, + queryaudit0_.lastName_MOD as lastNam10_3_0_, + queryaudit0_.address_id as address11_3_0_, + queryaudit0_.address_MOD as address12_3_0_, + defaultrev1_.REVTSTMP as REVTSTMP2_4_1_ +from + Customer_AUD queryaudit0_ cross +join + REVINFO defaultrev1_ +where + queryaudit0_.id = ? + and queryaudit0_.lastName_MOD = ? + and queryaudit0_.REV=defaultrev1_.REV +order by + queryaudit0_.REV asc + +-- binding parameter [1] as [BIGINT] - [1] +-- binding parameter [2] as [BOOLEAN] - [true] \ No newline at end of file diff --git a/documentation/src/test/java/org/hibernate/userguide/envers/QueryAuditWithModifiedFlagTest.java b/documentation/src/test/java/org/hibernate/userguide/envers/QueryAuditWithModifiedFlagTest.java new file mode 100644 index 0000000000..cc61bf0c64 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/userguide/envers/QueryAuditWithModifiedFlagTest.java @@ -0,0 +1,258 @@ +/* + * 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.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.List; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +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.query.AuditEntity; +import org.hibernate.envers.query.AuditQuery; +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.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class QueryAuditWithModifiedFlagTest extends BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Customer.class, + Address.class + }; + } + + @Override + protected void addConfigOptions(Map options) { + options.put( + EnversSettings.AUDIT_STRATEGY, + ValidityAuditStrategy.class.getName() + ); + } + + @Test + public void test() { + doInJPA( this::entityManagerFactory, entityManager -> { + Address address = new Address(); + address.setId( 1L ); + address.setCountry( "România" ); + address.setCity( "Cluj-Napoca" ); + address.setStreet( "Bulevardul Eroilor" ); + address.setStreetNumber( "1 A" ); + entityManager.persist( address ); + + Customer customer = new Customer(); + customer.setId( 1L ); + customer.setFirstName( "John" ); + customer.setLastName( "Doe" ); + customer.setAddress( address ); + + entityManager.persist( customer ); + } ); + + doInJPA( this::entityManagerFactory, entityManager -> { + Customer customer = entityManager.find( Customer.class, 1L ); + customer.setLastName( "Doe Jr." ); + } ); + + doInJPA( this::entityManagerFactory, entityManager -> { + Customer customer = entityManager.getReference( Customer.class, 1L ); + entityManager.remove( customer ); + } ); + + List revisions = doInJPA( this::entityManagerFactory, entityManager -> { + return AuditReaderFactory.get( entityManager ).getRevisions( + Customer.class, + 1L + ); + } ); + + doInJPA( this::entityManagerFactory, entityManager -> { + //tag::envers-tracking-properties-changes-queries-hasChanged-example[] + List customers = AuditReaderFactory + .get( entityManager ) + .createQuery() + .forRevisionsOfEntity( Customer.class, false, true ) + .add( AuditEntity.id().eq( 1L ) ) + .add( AuditEntity.property( "lastName" ).hasChanged() ) + .getResultList(); + //end::envers-tracking-properties-changes-queries-hasChanged-example[] + + assertEquals( 3, customers.size() ); + } ); + + doInJPA( this::entityManagerFactory, entityManager -> { + //tag::envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example[] + List customers = AuditReaderFactory + .get( entityManager ) + .createQuery() + .forRevisionsOfEntity( Customer.class, false, true ) + .add( AuditEntity.id().eq( 1L ) ) + .add( AuditEntity.property( "lastName" ).hasChanged() ) + .add( AuditEntity.property( "firstName" ).hasNotChanged() ) + .getResultList(); + //end::envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example[] + + assertEquals( 1, customers.size() ); + } ); + + doInJPA( this::entityManagerFactory, entityManager -> { + //tag::envers-tracking-properties-changes-queries-at-revision-example[] + Customer customer = (Customer) AuditReaderFactory + .get( entityManager ) + .createQuery() + .forEntitiesModifiedAtRevision( Customer.class, 2 ) + .add( AuditEntity.id().eq( 1L ) ) + .add( AuditEntity.property( "lastName" ).hasChanged() ) + .add( AuditEntity.property( "firstName" ).hasNotChanged() ) + .getSingleResult(); + //end::envers-tracking-properties-changes-queries-at-revision-example[] + + assertNotNull( customer ); + } ); + } + + //tag::envers-tracking-properties-changes-queries-entity-example[] + @Audited( withModifiedFlag = true ) + //end::envers-tracking-properties-changes-queries-entity-example[] + @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; + + @ManyToOne(fetch = FetchType.LAZY) + private Address address; + + 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; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + } + + @Audited + @Entity(name = "Address") + public static class Address { + + @Id + private Long id; + + private String country; + + private String city; + + private String street; + + private String streetNumber; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getStreetNumber() { + return streetNumber; + } + + public void setStreetNumber(String streetNumber) { + this.streetNumber = streetNumber; + } + } +}