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

Move code snippets to actual test cases for queries using modified flags
This commit is contained in:
Vlad Mihalcea 2017-08-31 17:12:49 +03:00
parent e7c239d57f
commit c25a859ade
5 changed files with 400 additions and 30 deletions

View File

@ -619,7 +619,7 @@ Sometimes it is useful to store additional metadata for each revision, when you
The feature described in <<envers-tracking-modified-entities-revchanges>> makes it possible to tell which entities were modified in a given revision. The feature described in <<envers-tracking-modified-entities-revchanges>> makes it possible to tell which entities were modified in a given revision.
The feature described here takes it one step further. 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: 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`. 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]] [[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()` 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 <<envers-tracking-properties-changes>>. that makes use of the functionality described in <<envers-tracking-properties-changes>>.
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() include::{sourcedir}/QueryAuditWithModifiedFlagTest.java[tags=envers-tracking-properties-changes-queries-entity-example]
.forRevisionsOfEntity( MyEntity.class, false, true ) ----
.add( AuditEntity.id().eq( id ) ); ====
.add( AuditEntity.property( "actualDate" ).hasChanged() );
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. [source, SQL, indent=0]
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]
---- ----
AuditQuery query = getAuditReader().createQuery() include::{extrasdir}/envers-tracking-properties-changes-queries-hasChanged-example.sql[]
.forEntitiesAtRevision( MyEntity.class, revisionNumber ) ----
.add( AuditEntity.property( "prop1" ).hasChanged() ) ====
.add( AuditEntity.property( "prop2" ).hasNotChanged() );
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. [source, SQL, indent=0]
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]
---- ----
AuditQuery query = getAuditReader().createQuery() include::{extrasdir}/envers-tracking-properties-changes-queries-hasChanged-and-hasNotChanged-example.sql[]
.forEntitiesModifiedAtRevision( MyEntity.class, revisionNumber )
.add( AuditEntity.property( "prop1" ).hasChanged() )
.add( AuditEntity.property( "prop2" ).hasNotChanged() );
---- ----
====
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]] [[envers-tracking-modified-entities-queries]]
=== Querying for entities modified in a given revision === Querying for entities modified in a given revision

View File

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

View File

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

View File

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

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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<Number> 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<Customer> 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<Customer> 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;
}
}
}