Skip to content

Commit c64a934

Browse files
mp911deChaedie
authored andcommitted
Fix #2850: Allow customization of @RevisionTimestamp property name
Signed-off-by: ChaedongIm <[email protected]>
1 parent 48bc6aa commit c64a934

File tree

6 files changed

+126
-16
lines changed

6 files changed

+126
-16
lines changed

spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* {@link RevisionEntityInformation} for {@link DefaultRevisionEntity}.
2323
*
2424
* @author Oliver Gierke
25+
* @author Chaedong Im
2526
*/
2627
class DefaultRevisionEntityInformation implements RevisionEntityInformation {
2728

@@ -36,4 +37,8 @@ public boolean isDefaultRevisionEntity() {
3637
public Class<?> getRevisionEntityClass() {
3738
return DefaultRevisionEntity.class;
3839
}
40+
41+
public String getRevisionTimestampFieldName() {
42+
return "timestamp";
43+
}
3944
}

spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@
6666
* @author Greg Turnquist
6767
* @author Aref Behboodi
6868
* @author Ngoc Nhan
69+
* @author Chaedong Im
6970
*/
7071
@Transactional(readOnly = true)
7172
public class EnversRevisionRepositoryImpl<T, ID, N extends Number & Comparable<N>>
7273
implements RevisionRepository<T, ID, N> {
7374

7475
private final EntityInformation<T, ?> entityInformation;
76+
private final RevisionEntityInformation revisionEntityInformation;
7577
private final EntityManager entityManager;
7678

7779
/**
@@ -90,14 +92,16 @@ public EnversRevisionRepositoryImpl(JpaEntityInformation<T, ?> entityInformation
9092
Assert.notNull(revisionEntityInformation, "RevisionEntityInformation must not be null!");
9193

9294
this.entityInformation = entityInformation;
95+
this.revisionEntityInformation = revisionEntityInformation;
9396
this.entityManager = entityManager;
9497
}
9598

9699
@SuppressWarnings("unchecked")
97100
public Optional<Revision<N, T>> findLastChangeRevision(ID id) {
98101

102+
String timestampFieldName = getRevisionTimestampFieldName();
99103
List<Object[]> singleResult = createBaseQuery(id) //
100-
.addOrder(AuditEntity.revisionProperty("timestamp").desc()) //
104+
.addOrder(AuditEntity.revisionProperty(timestampFieldName).desc()) //
101105
.addOrder(AuditEntity.revisionNumber().desc()) //
102106
.setMaxResults(1) //
103107
.getResultList();
@@ -213,6 +217,16 @@ private Revision<N, T> createRevision(QueryResult<T> queryResult) {
213217
return Revision.of((RevisionMetadata<N>) queryResult.createRevisionMetadata(), queryResult.entity);
214218
}
215219

220+
private String getRevisionTimestampFieldName() {
221+
if (revisionEntityInformation instanceof ReflectionRevisionEntityInformation reflection) {
222+
return reflection.getRevisionTimestampFieldName();
223+
} else if (revisionEntityInformation instanceof DefaultRevisionEntityInformation defaultInfo) {
224+
return defaultInfo.getRevisionTimestampFieldName();
225+
} else {
226+
return "timestamp";
227+
}
228+
}
229+
216230
@SuppressWarnings("unchecked")
217231
static class QueryResult<T> {
218232

spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.envers.repository.support;
1717

1818
import org.hibernate.envers.RevisionNumber;
19+
import org.hibernate.envers.RevisionTimestamp;
1920

2021
import org.springframework.data.repository.history.support.RevisionEntityInformation;
2122
import org.springframework.data.util.AnnotationDetectionFieldCallback;
@@ -27,11 +28,13 @@
2728
* find out about the revision number type.
2829
*
2930
* @author Oliver Gierke
31+
* @author Chaedong Im
3032
*/
3133
public class ReflectionRevisionEntityInformation implements RevisionEntityInformation {
3234

3335
private final Class<?> revisionEntityClass;
3436
private final Class<?> revisionNumberType;
37+
private final String revisionTimestampFieldName;
3538

3639
/**
3740
* Creates a new {@link ReflectionRevisionEntityInformation} inspecting the given revision entity class.
@@ -42,10 +45,14 @@ public ReflectionRevisionEntityInformation(Class<?> revisionEntityClass) {
4245

4346
Assert.notNull(revisionEntityClass, "Revision entity type must not be null");
4447

45-
AnnotationDetectionFieldCallback fieldCallback = new AnnotationDetectionFieldCallback(RevisionNumber.class);
46-
ReflectionUtils.doWithFields(revisionEntityClass, fieldCallback);
48+
AnnotationDetectionFieldCallback revisionNumberFieldCallback = new AnnotationDetectionFieldCallback(RevisionNumber.class);
49+
ReflectionUtils.doWithFields(revisionEntityClass, revisionNumberFieldCallback);
4750

48-
this.revisionNumberType = fieldCallback.getRequiredType();
51+
AnnotationDetectionFieldCallback revisionTimestampFieldCallback = new AnnotationDetectionFieldCallback(RevisionTimestamp.class);
52+
ReflectionUtils.doWithFields(revisionEntityClass, revisionTimestampFieldCallback);
53+
54+
this.revisionNumberType = revisionNumberFieldCallback.getRequiredType();
55+
this.revisionTimestampFieldName = revisionTimestampFieldCallback.getRequiredField().getName();
4956
this.revisionEntityClass = revisionEntityClass;
5057

5158
}
@@ -61,4 +68,8 @@ public Class<?> getRevisionEntityClass() {
6168
public Class<?> getRevisionNumberType() {
6269
return this.revisionNumberType;
6370
}
71+
72+
public String getRevisionTimestampFieldName() {
73+
return this.revisionTimestampFieldName;
74+
}
6475
}

spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImplUnitTests.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
import org.hibernate.envers.DefaultRevisionEntity;
2222
import org.hibernate.envers.RevisionType;
2323
import org.junit.jupiter.api.Test;
24+
import org.springframework.data.envers.sample.CustomRevisionEntity;
25+
import org.springframework.data.envers.sample.CustomRevisionEntityWithDifferentTimestamp;
2426
import org.springframework.data.history.AnnotationRevisionMetadata;
2527
import org.springframework.data.history.RevisionMetadata;
2628

2729
/**
2830
* Unit tests for {@link EnversRevisionRepositoryImpl}.
2931
*
3032
* @author Jens Schauder
33+
* @author Chaedong Im
3134
*/
3235
class EnversRevisionRepositoryImplUnitTests {
3336

@@ -57,4 +60,27 @@ void revisionTypeOfDefaultRevisionMetadataIsProperlySet() {
5760
assertThat(revisionMetadata.getRevisionType()).isEqualTo(RevisionMetadata.RevisionType.DELETE);
5861
}
5962

63+
@Test // gh-2850
64+
void reflectionRevisionEntityInformationDetectsStandardTimestampField() {
65+
66+
ReflectionRevisionEntityInformation revisionInfo = new ReflectionRevisionEntityInformation(CustomRevisionEntity.class);
67+
68+
assertThat(revisionInfo.getRevisionTimestampFieldName()).isEqualTo("timestamp");
69+
}
70+
71+
@Test // gh-2850
72+
void reflectionRevisionEntityInformationDetectsCustomTimestampField() {
73+
74+
ReflectionRevisionEntityInformation revisionInfo = new ReflectionRevisionEntityInformation(CustomRevisionEntityWithDifferentTimestamp.class);
75+
76+
assertThat(revisionInfo.getRevisionTimestampFieldName()).isEqualTo("myCustomTimestamp");
77+
}
78+
79+
@Test // gh-2850
80+
void defaultRevisionEntityInformationReturnsStandardTimestampFieldName() {
81+
82+
DefaultRevisionEntityInformation revisionInfo = new DefaultRevisionEntityInformation();
83+
84+
assertThat(revisionInfo.getRevisionTimestampFieldName()).isEqualTo("timestamp");
85+
}
6086
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.envers.sample;
17+
18+
import jakarta.persistence.Entity;
19+
import jakarta.persistence.GeneratedValue;
20+
import jakarta.persistence.Id;
21+
22+
import org.hibernate.envers.RevisionEntity;
23+
import org.hibernate.envers.RevisionNumber;
24+
import org.hibernate.envers.RevisionTimestamp;
25+
26+
/**
27+
* Custom revision entity with a non-standard timestamp field name to test dynamic timestamp property detection.
28+
*
29+
* @author Chaedong Im
30+
*/
31+
@Entity
32+
@RevisionEntity
33+
public class CustomRevisionEntityWithDifferentTimestamp {
34+
35+
@Id @GeneratedValue @RevisionNumber
36+
private int revisionId;
37+
38+
@RevisionTimestamp
39+
private long myCustomTimestamp; // Non-standard field name
40+
41+
public int getRevisionId() {
42+
return revisionId;
43+
}
44+
45+
public void setRevisionId(int revisionId) {
46+
this.revisionId = revisionId;
47+
}
48+
49+
public long getMyCustomTimestamp() {
50+
return myCustomTimestamp;
51+
}
52+
53+
public void setMyCustomTimestamp(long myCustomTimestamp) {
54+
this.myCustomTimestamp = myCustomTimestamp;
55+
}
56+
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import jakarta.persistence.QueryHint;
2121
import jakarta.persistence.Tuple;
2222

23-
import java.lang.reflect.Type;
2423
import java.util.Arrays;
2524
import java.util.Collection;
2625
import java.util.List;
@@ -160,8 +159,6 @@ public CodeBlock build() {
160159
Assert.notNull(queries, "Queries must not be null");
161160

162161
boolean isProjecting = context.getReturnedType().isProjecting();
163-
Class<?> actualReturnType = isProjecting ? context.getActualReturnType().toClass()
164-
: context.getRepositoryInformation().getDomainType();
165162

166163
String dynamicReturnType = null;
167164
if (queryMethod.getParameters().hasDynamicProjection()) {
@@ -212,7 +209,7 @@ public CodeBlock build() {
212209
if ((StringUtils.hasText(sortParameterName) || StringUtils.hasText(dynamicReturnType))
213210
&& queries != null && queries.result() instanceof StringAotQuery
214211
&& StringUtils.hasText(queryStringVariableName)) {
215-
builder.add(applyRewrite(sortParameterName, dynamicReturnType, queryStringVariableName, actualReturnType));
212+
builder.add(applyRewrite(sortParameterName, dynamicReturnType, isProjecting, queryStringVariableName));
216213
}
217214

218215
builder.add(createQuery(false, queryVariableName, queryStringVariableName, queryRewriterName, queries.result(),
@@ -246,8 +243,8 @@ private CodeBlock buildQueryString(StringAotQuery sq, String queryStringVariable
246243
return builder.build();
247244
}
248245

249-
private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicReturnType, String queryString,
250-
Class<?> actualReturnType) {
246+
private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicReturnType, boolean isProjecting,
247+
String queryString) {
251248

252249
Builder builder = CodeBlock.builder();
253250

@@ -266,6 +263,9 @@ private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicRe
266263
builder.addStatement("$L = rewriteQuery($L, $L, $L)", queryString, context.localVariable("declaredQuery"), sort,
267264
dynamicReturnType);
268265
} else if (hasSort) {
266+
267+
Object actualReturnType = isProjecting ? context.getActualReturnTypeName() : context.getDomainType();
268+
269269
builder.addStatement("$L = rewriteQuery($L, $L, $T.class)", queryString, context.localVariable("declaredQuery"),
270270
sort, actualReturnType);
271271
} else if (hasDynamicReturnType) {
@@ -657,11 +657,9 @@ public CodeBlock build() {
657657

658658
Builder builder = CodeBlock.builder();
659659

660-
boolean isProjecting = context.getActualReturnType() != null
661-
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
662-
context.getActualReturnType());
663-
Type actualReturnType = isProjecting ? context.getActualReturnType().getType()
664-
: context.getRepositoryInformation().getDomainType();
660+
boolean isProjecting = !ObjectUtils.nullSafeEquals(context.getDomainType(), context.getActualReturnTypeName());
661+
TypeName actualReturnType = isProjecting ? context.getActualReturnTypeName()
662+
: TypeName.get(context.getDomainType());
665663
builder.add("\n");
666664

667665
if (modifying.isPresent()) {
@@ -775,7 +773,7 @@ public CodeBlock build() {
775773
if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) {
776774
builder.addStatement("return $T.ofNullable(($T) convertOne($L.getSingleResultOrNull(), $L, $T.class))",
777775
Optional.class, actualReturnType, queryVariableName, aotQuery.isNative(),
778-
context.getActualReturnType().toClass());
776+
queryResultType);
779777
} else {
780778
builder.addStatement("return ($T) convertOne($L.getSingleResultOrNull(), $L, $T.class)",
781779
context.getReturnTypeName(), queryVariableName, aotQuery.isNative(),

0 commit comments

Comments
 (0)