HHH-17139 Support Instant as revision timestamps
This commit is contained in:
parent
baa4141261
commit
b9a88a9670
|
@ -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)) <= 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}.
|
||||
*
|
||||
|
|
|
@ -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" );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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;
|
||||
|
@ -38,7 +40,12 @@ public class RevisionTimestampValueResolver {
|
|||
revisionTimestampSetter.set( object, new Date() );
|
||||
}
|
||||
else if ( timestampData.isTimestampLocalDateTime() ) {
|
||||
revisionTimestampSetter.set(object, LocalDateTime.now() );
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue