HHH-18487 align behavior of UnsavedValueFactory with semantics of persist()

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-08-28 13:09:07 +02:00
parent 318e4e6c5d
commit a87e08b2e2
4 changed files with 88 additions and 21 deletions

View File

@ -21,6 +21,8 @@ import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.VersionJavaType; import org.hibernate.type.descriptor.java.VersionJavaType;
import org.hibernate.type.descriptor.java.spi.PrimitiveJavaType; import org.hibernate.type.descriptor.java.spi.PrimitiveJavaType;
import static org.hibernate.engine.internal.Versioning.isNullInitialVersion;
/** /**
* Helper for dealing with unsaved value handling * Helper for dealing with unsaved value handling
* *
@ -81,25 +83,20 @@ public class UnsavedValueFactory {
public static <T> VersionValue getUnsavedVersionValue( public static <T> VersionValue getUnsavedVersionValue(
KeyValue bootVersionMapping, KeyValue bootVersionMapping,
VersionJavaType<T> jtd, VersionJavaType<T> jtd,
Long length,
Integer precision,
Integer scale,
Getter getter, Getter getter,
Supplier<?> templateInstanceAccess, Supplier<?> templateInstanceAccess) {
SessionFactoryImplementor sessionFactory) {
final String unsavedValue = bootVersionMapping.getNullValue(); final String unsavedValue = bootVersionMapping.getNullValue();
if ( unsavedValue == null ) { if ( unsavedValue == null ) {
if ( getter != null && templateInstanceAccess != null ) { if ( getter != null && templateInstanceAccess != null ) {
Object templateInstance = templateInstanceAccess.get(); final Object templateInstance = templateInstanceAccess.get();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final T defaultValue = (T) getter.get( templateInstance ); final T defaultValue = (T) getter.get( templateInstance );
// if the version of a newly instantiated object is null
// if the version of a newly instantiated object is not the same // or a negative number, use that value as the unsaved-value,
// as the version seed value, use that as the unsaved-value // otherwise assume it's the initial version set by program
final T seedValue = jtd.seed( length, precision, scale, mockSession( sessionFactory ) ); return isNullInitialVersion( defaultValue )
return jtd.areEqual( seedValue, defaultValue ) ? new VersionValue( defaultValue )
? VersionValue.UNDEFINED : VersionValue.UNDEFINED;
: new VersionValue( defaultValue );
} }
else { else {
return VersionValue.UNDEFINED; return VersionValue.UNDEFINED;

View File

@ -54,7 +54,7 @@ public class EntityVersionMappingImpl implements EntityVersionMapping, FetchOpti
private final Integer scale; private final Integer scale;
private final Integer temporalPrecision; private final Integer temporalPrecision;
private final BasicType versionBasicType; private final BasicType<?> versionBasicType;
private final VersionValue unsavedValueStrategy; private final VersionValue unsavedValueStrategy;
@ -90,15 +90,10 @@ public class EntityVersionMappingImpl implements EntityVersionMapping, FetchOpti
unsavedValueStrategy = UnsavedValueFactory.getUnsavedVersionValue( unsavedValueStrategy = UnsavedValueFactory.getUnsavedVersionValue(
(KeyValue) bootEntityDescriptor.getVersion().getValue(), (KeyValue) bootEntityDescriptor.getVersion().getValue(),
(VersionJavaType<?>) versionBasicType.getJavaTypeDescriptor(), (VersionJavaType<?>) versionBasicType.getJavaTypeDescriptor(),
length, declaringType.getRepresentationStrategy()
precision,
scale,
declaringType
.getRepresentationStrategy()
.resolvePropertyAccess( bootEntityDescriptor.getVersion() ) .resolvePropertyAccess( bootEntityDescriptor.getVersion() )
.getGetter(), .getGetter(),
templateInstanceAccess, templateInstanceAccess
creationProcess.getCreationContext().getSessionFactory()
); );
} }

View File

@ -16,6 +16,10 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor;
public interface VersionJavaType<T> extends JavaType<T> { public interface VersionJavaType<T> extends JavaType<T> {
/** /**
* Generate an initial version. * Generate an initial version.
* <p>
* Note that this operation is only used when the program sets a null or negative
* number as the value of the entity version field. It is not called when the
* program sets the version field to a sensible-looking version.
* *
* @param length The length of the type * @param length The length of the type
* @param precision The precision of the type * @param precision The precision of the type

View File

@ -0,0 +1,71 @@
package org.hibernate.orm.test.ops;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.Jpa;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Jpa(annotatedClasses = {MergeExplicitInitialVersionTest.E.class,
MergeExplicitInitialVersionTest.F.class,
MergeExplicitInitialVersionTest.G.class})
public class MergeExplicitInitialVersionTest {
@Test public void testGeneratedId(EntityManagerFactoryScope scope) {
E e = new E();
scope.inTransaction(s->s.persist(e));
assertEquals(e.version, 1);
e.text = "hello";
E e2 = scope.fromTransaction(s->s.merge(e));
assertEquals(e2.version, 2);
}
@Test public void testAssignedId(EntityManagerFactoryScope scope) {
F f = new F();
scope.inTransaction(s->s.persist(f));
assertEquals(f.version, 1);
f.text = "hello";
F f2 = scope.fromTransaction(s->s.merge(f));
assertEquals(f2.version, 2);
}
@Test public void testNegativeVersion(EntityManagerFactoryScope scope) {
G g = new G();
scope.inTransaction(s->s.persist(g));
assertEquals(g.version, 0);
g.text = "hello";
G g2 = scope.fromTransaction(s->s.merge(g));
assertEquals(g2.version, 1);
}
@Entity
static class E {
@Id
@GeneratedValue
long id;
@Version
int version = 1;
String text;
}
@Entity
static class F {
@Id
long id = 5;
@Version
int version = 1;
String text;
}
@Entity
static class G {
@Id
@GeneratedValue
long id;
@Version
int version = -1;
String text;
}
}