HHH-17139 Support Instant as revision timestamps

This commit is contained in:
Chris Cranford 2023-09-08 21:55:45 -04:00 committed by Christian Beikov
parent baa4141261
commit b9a88a9670
9 changed files with 321 additions and 8 deletions

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.envers;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
@ -230,6 +231,23 @@ public interface AuditReader {
Number getRevisionNumberForDate(LocalDateTime date) throws IllegalStateException,
RevisionDoesNotExistException, IllegalArgumentException;
/**
* Gets the revision number, that corresponds to the given date. More precisely, returns
* the number of the highest revision, which was created on or before the given date. So:
* <code>getRevisionDate(getRevisionNumberForDate(date)) &lt;= date</code> and
* <code>getRevisionDate(getRevisionNumberForDate(date)+1) > date</code>.
*
* @param date Date for which to get the revision.
*
* @return Revision number corresponding to the given date.
*
* @throws IllegalStateException If the associated entity manager is closed.
* @throws RevisionDoesNotExistException If the given date is before the first revision.
* @throws IllegalArgumentException If <code>date</code> is <code>null</code>.
*/
Number getRevisionNumberForDate(Instant date) throws IllegalStateException,
RevisionDoesNotExistException, IllegalArgumentException;
/**
* A helper method; should be used only if a custom revision entity is used. See also {@link RevisionEntity}.
*

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.envers.configuration.internal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Locale;
@ -498,12 +499,12 @@ public class RevisionInfoConfiguration {
}
final XClass propertyType = property.getType();
if ( isAnyType( propertyType, Long.class, Long.TYPE, Date.class, LocalDateTime.class, java.sql.Date.class ) ) {
if ( isAnyType( propertyType, Long.class, Long.TYPE, Date.class, LocalDateTime.class, Instant.class, java.sql.Date.class ) ) {
revisionInfoTimestampData = createPropertyData( property, accessType );
revisionTimestampFound = true;
}
else {
throwUnexpectedAnnotatedType( property, RevisionTimestamp.class, "long, Long, Date, LocalDateTime, or java.sql.Date" );
throwUnexpectedAnnotatedType( property, RevisionTimestamp.class, "long, Long, Date, LocalDateTime, Instant, or java.sql.Date" );
}
}

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.envers.exception;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;
@ -19,12 +20,14 @@ public class RevisionDoesNotExistException extends AuditException {
private final Number revision;
private final Date date;
private final LocalDateTime localDateTime;
private final Instant instant;
public RevisionDoesNotExistException(Number revision) {
super( "Revision " + revision + " does not exist." );
this.revision = revision;
this.date = null;
this.localDateTime = null;
this.instant = null;
}
public RevisionDoesNotExistException(Date date) {
@ -32,6 +35,7 @@ public class RevisionDoesNotExistException extends AuditException {
this.date = date;
this.revision = null;
this.localDateTime = null;
this.instant = null;
}
public RevisionDoesNotExistException(LocalDateTime localDateTime) {
@ -39,6 +43,15 @@ public class RevisionDoesNotExistException extends AuditException {
this.localDateTime = localDateTime;
this.revision = null;
this.date = null;
this.instant = null;
}
public RevisionDoesNotExistException(Instant instant) {
super( "There is no revision before or at " + instant + "." );
this.instant = instant;
this.revision = null;
this.date = null;
this.localDateTime = null;
}
public Number getRevision() {
@ -52,4 +65,9 @@ public class RevisionDoesNotExistException extends AuditException {
public LocalDateTime getLocalDateTime() {
return localDateTime;
}
public Instant getInstant() {
return instant;
}
}

View File

@ -8,8 +8,6 @@ package org.hibernate.envers.internal.entities;
import java.util.Objects;
import org.hibernate.type.Type;
/**
* @author Chris Cranford
* @author 6.0
@ -41,6 +39,10 @@ public class RevisionTimestampData extends PropertyData {
return "LocalDateTime".equals( typeName );
}
public boolean isInstant() {
return "instant".equals( typeName );
}
@Override
public int hashCode() {
int result = super.hashCode();

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.envers.internal.reader;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
@ -183,8 +184,16 @@ public class AuditReaderImpl implements AuditReaderImplementor {
throw new RevisionDoesNotExistException( revision );
}
// The timestamp object is either a date or a long
return timestampObject instanceof Date ? (Date) timestampObject : new Date( (Long) timestampObject );
// The timestamp object is either a date, instant, or a long
if ( timestampObject instanceof Date ) {
return (Date) timestampObject;
}
else if ( timestampObject instanceof Instant ) {
return Date.from( (Instant) timestampObject );
}
else {
return new Date( (Long) timestampObject );
}
}
catch (NonUniqueResultException e) {
throw new AuditException( e );
@ -231,6 +240,26 @@ public class AuditReaderImpl implements AuditReaderImplementor {
}
}
@Override
public Number getRevisionNumberForDate(Instant date) {
checkNotNull( date, "Date of revision" );
checkSession();
final Query<?> query = enversService.getRevisionInfoQueryCreator().getRevisionNumberForDateQuery( session, date );
try {
final Number res = (Number) query.uniqueResult();
if ( res == null ) {
throw new RevisionDoesNotExistException( date );
}
return res;
}
catch (NonUniqueResultException e) {
throw new AuditException( e );
}
}
@Override
@SuppressWarnings("unchecked")
public <T> T findRevision(Class<T> revisionEntityClass, Number revision)

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.envers.internal.revisioninfo;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Locale;
@ -76,6 +77,18 @@ public class RevisionInfoQueryCreator {
).setParameter( REVISION_NUMBER_FOR_DATE_QUERY_PARAMETER, timestampValueResolver.resolveByValue( localDateTime ) );
}
public Query<?> getRevisionNumberForDateQuery(Session session, Instant instant) {
return session.createQuery(
String.format(
Locale.ENGLISH,
REVISION_NUMBER_FOR_DATE_QUERY,
revisionInfoIdName,
revisionInfoEntityName,
timestampValueResolver.getName()
)
).setParameter( REVISION_NUMBER_FOR_DATE_QUERY_PARAMETER, timestampValueResolver.resolveByValue( instant ) );
}
public Query<?> getRevisionsQuery(Session session, Set<Number> revisions) {
return session.createQuery(
String.format( Locale.ENGLISH, REVISIONS_QUERY, revisionInfoEntityName, revisionInfoIdName )

View File

@ -6,8 +6,10 @@
*/
package org.hibernate.envers.internal.revisioninfo;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import org.hibernate.envers.internal.entities.RevisionTimestampData;
@ -40,6 +42,11 @@ public class RevisionTimestampValueResolver {
else if ( timestampData.isTimestampLocalDateTime() ) {
revisionTimestampSetter.set( object, LocalDateTime.now() );
}
else if ( timestampData.isInstant() ) {
// HHH-17139 truncated to milliseconds to allow Date-based AuditReader functions to
// continue to work with the same precision level.
revisionTimestampSetter.set( object, Instant.now().truncatedTo( ChronoUnit.MILLIS ) );
}
else {
revisionTimestampSetter.set( object, System.currentTimeMillis() );
}
@ -53,6 +60,9 @@ public class RevisionTimestampValueResolver {
else if ( timestampData.isTimestampLocalDateTime() ) {
return LocalDateTime.ofInstant( date.toInstant(), ZoneId.systemDefault() );
}
else if ( timestampData.isInstant() ) {
return date.toInstant();
}
else {
return date.getTime();
}
@ -68,10 +78,31 @@ public class RevisionTimestampValueResolver {
else if ( timestampData.isTimestampLocalDateTime() ) {
return localDateTime;
}
else if ( timestampData.isInstant() ) {
return localDateTime.atZone( ZoneId.systemDefault() ).toInstant();
}
else {
return localDateTime.atZone( ZoneId.systemDefault() ).toInstant().toEpochMilli();
}
}
return null;
}
public Object resolveByValue(Instant instant) {
if ( instant != null ) {
if ( timestampData.isTimestampDate() ) {
return Date.from( instant );
}
else if ( timestampData.isTimestampLocalDateTime() ) {
return LocalDateTime.ofInstant( instant, ZoneId.systemDefault() );
}
else if ( timestampData.isInstant() ) {
return instant;
}
else {
return instant.getEpochSecond();
}
}
return null;
}
}

View File

@ -0,0 +1,86 @@
/*
* 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.orm.test.envers.entities.reventity;
import java.time.Instant;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import org.hibernate.envers.RevisionEntity;
import org.hibernate.envers.RevisionNumber;
import org.hibernate.envers.RevisionTimestamp;
/**
* @author Chris Cranford
*/
@Entity
@GenericGenerator(name = "EnversTestingRevisionGenerator",
strategy = "org.hibernate.id.enhanced.TableGenerator",
parameters = {
@Parameter(name = "table_name", value = "REVISION_GENERATOR"),
@Parameter(name = "initial_value", value = "1"),
@Parameter(name = "increment_size", value = "1"),
@Parameter(name = "prefer_entity_table_as_segment_value", value = "true")
}
)
@RevisionEntity
public class CustomInstantRevEntity {
@Id
@GeneratedValue(generator = "EnversTestingRevisionGenerator")
@RevisionNumber
private int customId;
@RevisionTimestamp
private Instant instantTimestamp;
public int getCustomId() {
return customId;
}
public void setCustomId(int customId) {
this.customId = customId;
}
public Instant getInstantTimestamp() {
return instantTimestamp;
}
public void setInstantTimestamp(Instant instantTimestamp) {
this.instantTimestamp = instantTimestamp;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
CustomInstantRevEntity that = (CustomInstantRevEntity) o;
if ( customId != that.customId ) {
return false;
}
if ( instantTimestamp != null ? !instantTimestamp.equals( that.instantTimestamp ) : that.instantTimestamp != null ) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = customId;
result = 31 * result + (instantTimestamp != null ? instantTimestamp.hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.orm.test.envers.integration.reventity;
import java.time.Instant;
import java.util.Arrays;
import java.util.Date;
import jakarta.persistence.EntityManager;
import org.hibernate.dialect.CockroachDialect;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.exception.RevisionDoesNotExistException;
import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase;
import org.hibernate.orm.test.envers.Priority;
import org.hibernate.orm.test.envers.entities.StrTestEntity;
import org.hibernate.orm.test.envers.entities.reventity.CustomInstantRevEntity;
import org.hibernate.testing.SkipForDialect;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Chris Cranford
*/
public class CustomInstantRevEntityTest extends BaseEnversJPAFunctionalTestCase {
private Integer id;
private Instant instant1;
private Instant instant2;
private Instant instant3;
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] {StrTestEntity.class, CustomInstantRevEntity.class};
}
@Test
@Priority(10)
public void initData() throws InterruptedException {
instant1 = getCurrentInstant();
// Revision 1
EntityManager em = getEntityManager();
em.getTransaction().begin();
StrTestEntity entity = new StrTestEntity( "x" );
em.persist( entity );
id = entity.getId();
em.getTransaction().commit();
instant2 = getCurrentInstant();
// Revision 2
em.getTransaction().begin();
entity = em.find( StrTestEntity.class, id );
entity.setStr( "y" );
em.getTransaction().commit();
instant3 = getCurrentInstant();
}
@Test(expected = RevisionDoesNotExistException.class)
public void testInstant1() {
getAuditReader().getRevisionNumberForDate( new Date( instant1.toEpochMilli() ) );
}
@Test
@SkipForDialect(value = CockroachDialect.class, comment = "Fails because of int size")
public void testInstants() {
assertThat( getAuditReader().getRevisionNumberForDate( new Date( instant2.toEpochMilli() ) ).intValue() ).isEqualTo( 1 );
assertThat( getAuditReader().getRevisionNumberForDate( new Date( instant3.toEpochMilli() ) ).intValue() ).isEqualTo( 2 );
assertThat( getAuditReader().getRevisionNumberForDate( instant2 ).intValue() ).isEqualTo( 1 );
assertThat( getAuditReader().getRevisionNumberForDate( instant3 ).intValue() ).isEqualTo( 2 );
}
@Test
public void testInstantsForRevisions() {
final AuditReader reader = getAuditReader();
assertThat( reader.getRevisionNumberForDate( reader.getRevisionDate( 1 ) ).intValue() ).isEqualTo( 1 );
assertThat( reader.getRevisionNumberForDate( reader.getRevisionDate( 2 ) ).intValue() ).isEqualTo( 2 );
}
@Test
public void testRevisionsForInstants() {
final Instant revInstant1 = getAuditReader().findRevision( CustomInstantRevEntity.class, 1 ).getInstantTimestamp();
assertThat( revInstant1.toEpochMilli() ).isGreaterThan( instant1.toEpochMilli() );
assertThat( revInstant1.toEpochMilli() ).isLessThanOrEqualTo( instant2.toEpochMilli() );
final Instant revInstant2 = getAuditReader().findRevision( CustomInstantRevEntity.class, 2 ).getInstantTimestamp();
assertThat( revInstant2.toEpochMilli() ).isGreaterThan( instant2.toEpochMilli() );
assertThat( revInstant2.toEpochMilli() ).isLessThanOrEqualTo( instant3.toEpochMilli() );
}
@Test
public void testRevisionsCounts() {
assertThat( getAuditReader().getRevisions( StrTestEntity.class, id ) ).isEqualTo( Arrays.asList( 1, 2 ) );
}
@Test
public void testHistoryOfId1() {
assertThat( getAuditReader().find( StrTestEntity.class, id, 1 ) ).isEqualTo( new StrTestEntity( "x", id ) );
assertThat( getAuditReader().find( StrTestEntity.class, id, 2 ) ).isEqualTo( new StrTestEntity( "y", id ) );
}
private Instant getCurrentInstant() throws InterruptedException {
Instant now = Instant.now();
// Some databases default to second-based precision, sleep
Thread.sleep( 1100 );
return now;
}
}