Browse Source
Refine assignment flow and use early returns where possible. Cache empty MapSqlParameterSource. Reduce dependency on RelationalMappingContext using a lower-level abstraction signature. Simplify names. Use default value check from Commons. Fix log warning message. Add missing since tags. Remove superfluous annotations and redundant code. Tweak documentation wording. Closes #2003 Original pull request: #2005pull/2034/head
9 changed files with 252 additions and 200 deletions
@ -1,89 +1,112 @@
@@ -1,89 +1,112 @@
|
||||
package org.springframework.data.jdbc.core.mapping; |
||||
|
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; |
||||
|
||||
import org.springframework.data.mapping.PersistentProperty; |
||||
import org.springframework.data.mapping.PersistentPropertyAccessor; |
||||
import org.springframework.data.mapping.context.MappingContext; |
||||
import org.springframework.data.relational.core.conversion.MutableAggregateChange; |
||||
import org.springframework.data.relational.core.dialect.Dialect; |
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext; |
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; |
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; |
||||
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback; |
||||
import org.springframework.data.relational.core.sql.SqlIdentifier; |
||||
import org.springframework.data.util.ReflectionUtils; |
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; |
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.NumberUtils; |
||||
|
||||
/** |
||||
* Callback for generating ID via the database sequence. By default, it is registered as a bean in |
||||
* {@link AbstractJdbcConfiguration} |
||||
* Callback for generating identifier values through a database sequence. |
||||
* |
||||
* @author Mikhail Polivakha |
||||
* @author Mark Paluch |
||||
* @since 3.5 |
||||
* @see org.springframework.data.relational.core.mapping.Sequence |
||||
*/ |
||||
public class IdGeneratingBeforeSaveCallback implements BeforeSaveCallback<Object> { |
||||
|
||||
private static final Log LOG = LogFactory.getLog(IdGeneratingBeforeSaveCallback.class); |
||||
private final static MapSqlParameterSource EMPTY_PARAMETERS = new MapSqlParameterSource(); |
||||
|
||||
private final RelationalMappingContext relationalMappingContext; |
||||
private final MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext; |
||||
private final Dialect dialect; |
||||
private final NamedParameterJdbcOperations operations; |
||||
|
||||
public IdGeneratingBeforeSaveCallback(RelationalMappingContext relationalMappingContext, Dialect dialect, |
||||
NamedParameterJdbcOperations namedParameterJdbcOperations) { |
||||
this.relationalMappingContext = relationalMappingContext; |
||||
public IdGeneratingBeforeSaveCallback( |
||||
MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext, |
||||
Dialect dialect, NamedParameterJdbcOperations operations) { |
||||
this.mappingContext = mappingContext; |
||||
this.dialect = dialect; |
||||
this.operations = namedParameterJdbcOperations; |
||||
this.operations = operations; |
||||
} |
||||
|
||||
@Override |
||||
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) { |
||||
|
||||
Assert.notNull(aggregate, "The aggregate cannot be null at this point"); |
||||
Assert.notNull(aggregate, "aggregate must not be null"); |
||||
|
||||
RelationalPersistentEntity<?> persistentEntity = relationalMappingContext.getPersistentEntity(aggregate.getClass()); |
||||
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(aggregate.getClass()); |
||||
|
||||
if (!persistentEntity.hasIdProperty()) { |
||||
if (!entity.hasIdProperty()) { |
||||
return aggregate; |
||||
} |
||||
|
||||
// we're doing INSERT and ID property value is not set explicitly by client
|
||||
if (persistentEntity.isNew(aggregate) && !hasIdentifierValue(aggregate, persistentEntity)) { |
||||
return potentiallyFetchIdFromSequence(aggregate, persistentEntity); |
||||
} else { |
||||
RelationalPersistentProperty idProperty = entity.getRequiredIdProperty(); |
||||
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(aggregate); |
||||
|
||||
if (!entity.isNew(aggregate) || hasIdentifierValue(idProperty, accessor)) { |
||||
return aggregate; |
||||
} |
||||
|
||||
potentiallyFetchIdFromSequence(idProperty, entity, accessor); |
||||
return accessor.getBean(); |
||||
} |
||||
|
||||
private boolean hasIdentifierValue(Object aggregate, RelationalPersistentEntity<?> persistentEntity) { |
||||
Object identifier = persistentEntity.getIdentifierAccessor(aggregate).getIdentifier(); |
||||
private boolean hasIdentifierValue(PersistentProperty<?> idProperty, |
||||
PersistentPropertyAccessor<Object> propertyAccessor) { |
||||
|
||||
if (persistentEntity.getIdProperty().getType().isPrimitive()) { |
||||
return identifier instanceof Number num && num.longValue() != 0L; |
||||
} else { |
||||
return identifier != null; |
||||
Object identifier = propertyAccessor.getProperty(idProperty); |
||||
|
||||
if (idProperty.getType().isPrimitive()) { |
||||
|
||||
Object primitiveDefault = ReflectionUtils.getPrimitiveDefault(idProperty.getType()); |
||||
return !primitiveDefault.equals(identifier); |
||||
} |
||||
|
||||
return identifier != null; |
||||
} |
||||
|
||||
private Object potentiallyFetchIdFromSequence(Object aggregate, RelationalPersistentEntity<?> persistentEntity) { |
||||
@SuppressWarnings("unchecked") |
||||
private void potentiallyFetchIdFromSequence(PersistentProperty<?> idProperty, |
||||
RelationalPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<Object> accessor) { |
||||
|
||||
Optional<SqlIdentifier> idSequence = persistentEntity.getIdSequence(); |
||||
|
||||
if (dialect.getIdGeneration().sequencesSupported()) { |
||||
idSequence.map(s -> dialect.getIdGeneration().createSequenceQuery(s)).ifPresent(sql -> { |
||||
Long idValue = operations.queryForObject(sql, Map.of(), (rs, rowNum) -> rs.getLong(1)); |
||||
PersistentPropertyAccessor<Object> propertyAccessor = persistentEntity.getPropertyAccessor(aggregate); |
||||
propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), idValue); |
||||
}); |
||||
} else { |
||||
if (idSequence.isPresent()) { |
||||
LOG.warn(""" |
||||
It seems you're trying to insert an aggregate of type '%s' annotated with @TargetSequence, but the problem is RDBMS you're |
||||
working with does not support sequences as such. Falling back to identity columns |
||||
""".formatted(aggregate.getClass().getName())); |
||||
} |
||||
if (idSequence.isPresent() && !dialect.getIdGeneration().sequencesSupported()) { |
||||
LOG.warn(""" |
||||
Aggregate type '%s' is marked for sequence usage but configured dialect '%s' |
||||
does not support sequences. Falling back to identity columns. |
||||
""".formatted(persistentEntity.getType(), ClassUtils.getQualifiedName(dialect.getClass()))); |
||||
return; |
||||
} |
||||
|
||||
return aggregate; |
||||
idSequence.map(s -> dialect.getIdGeneration().createSequenceQuery(s)).ifPresent(sql -> { |
||||
|
||||
Object idValue = operations.queryForObject(sql, EMPTY_PARAMETERS, (rs, rowNum) -> rs.getObject(1)); |
||||
|
||||
Class<?> targetType = ClassUtils.resolvePrimitiveIfNecessary(idProperty.getType()); |
||||
if (idValue instanceof Number && Number.class.isAssignableFrom(targetType)) { |
||||
accessor.setProperty(idProperty, |
||||
NumberUtils.convertNumberToTargetClass((Number) idValue, (Class<? extends Number>) targetType)); |
||||
} else { |
||||
accessor.setProperty(idProperty, idValue); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
@ -1,111 +1,171 @@
@@ -1,111 +1,171 @@
|
||||
/* |
||||
* Copyright 2024-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.jdbc.core.mapping; |
||||
|
||||
import static org.mockito.ArgumentMatchers.anyMap; |
||||
import static org.mockito.ArgumentMatchers.anyString; |
||||
import static org.assertj.core.api.Assertions.*; |
||||
import static org.mockito.ArgumentMatchers.*; |
||||
import static org.mockito.Mockito.*; |
||||
import static org.mockito.Mockito.any; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import org.assertj.core.api.Assertions; |
||||
import java.util.UUID; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mock; |
||||
import org.mockito.junit.jupiter.MockitoSettings; |
||||
import org.mockito.quality.Strictness; |
||||
|
||||
import org.springframework.data.annotation.Id; |
||||
import org.springframework.data.mapping.model.SimpleTypeHolder; |
||||
import org.springframework.data.relational.core.conversion.MutableAggregateChange; |
||||
import org.springframework.data.relational.core.dialect.MySqlDialect; |
||||
import org.springframework.data.relational.core.dialect.PostgresDialect; |
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext; |
||||
import org.springframework.data.relational.core.mapping.Table; |
||||
import org.springframework.data.relational.core.mapping.Sequence; |
||||
import org.springframework.data.relational.core.sql.IdentifierProcessing; |
||||
import org.springframework.data.relational.core.mapping.Table; |
||||
import org.springframework.jdbc.core.RowMapper; |
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; |
||||
import org.springframework.jdbc.core.namedparam.SqlParameterSource; |
||||
|
||||
/** |
||||
* Unit tests for {@link IdGeneratingBeforeSaveCallback} |
||||
* |
||||
* @author Mikhail Polivakha |
||||
* @author Mark Paluch |
||||
*/ |
||||
@MockitoSettings(strictness = Strictness.LENIENT) |
||||
class IdGeneratingBeforeSaveCallbackTest { |
||||
|
||||
@Test // GH-1923
|
||||
void mySqlDialectsequenceGenerationIsNotSupported() { |
||||
@Mock NamedParameterJdbcOperations operations; |
||||
RelationalMappingContext relationalMappingContext; |
||||
|
||||
@BeforeEach |
||||
void setUp() { |
||||
|
||||
relationalMappingContext = new RelationalMappingContext(); |
||||
relationalMappingContext.setSimpleTypeHolder(new SimpleTypeHolder(PostgresDialect.INSTANCE.simpleTypes(), true)); |
||||
} |
||||
|
||||
@Test // GH-1923
|
||||
void sequenceGenerationIsNotSupported() { |
||||
|
||||
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); |
||||
|
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, |
||||
MySqlDialect.INSTANCE, operations); |
||||
|
||||
EntityWithSequence processed = (EntityWithSequence) subject.onBeforeSave(new EntityWithSequence(), |
||||
MutableAggregateChange.forSave(new EntityWithSequence())); |
||||
|
||||
assertThat(processed.id).isNull(); |
||||
} |
||||
|
||||
@Test // GH-1923
|
||||
void entityIsNotMarkedWithTargetSequence() { |
||||
|
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, |
||||
MySqlDialect.INSTANCE, operations); |
||||
|
||||
NoSequenceEntity processed = (NoSequenceEntity) subject.onBeforeSave(new NoSequenceEntity(), |
||||
MutableAggregateChange.forSave(new NoSequenceEntity())); |
||||
|
||||
assertThat(processed.id).isNull(); |
||||
} |
||||
|
||||
@Test // GH-1923
|
||||
void entityIdIsPopulatedFromSequence() { |
||||
|
||||
long generatedId = 112L; |
||||
when(operations.queryForObject(anyString(), any(SqlParameterSource.class), any(RowMapper.class))) |
||||
.thenReturn(generatedId); |
||||
|
||||
RelationalMappingContext relationalMappingContext = new RelationalMappingContext(); |
||||
MySqlDialect mySqlDialect = new MySqlDialect(IdentifierProcessing.NONE); |
||||
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); |
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, |
||||
PostgresDialect.INSTANCE, operations); |
||||
|
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations); |
||||
EntityWithSequence processed = (EntityWithSequence) subject.onBeforeSave(new EntityWithSequence(), |
||||
MutableAggregateChange.forSave(new EntityWithSequence())); |
||||
|
||||
NoSequenceEntity entity = new NoSequenceEntity(); |
||||
assertThat(processed.getId()).isEqualTo(generatedId); |
||||
} |
||||
|
||||
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity)); |
||||
@Test // GH-2003
|
||||
void appliesIntegerConversion() { |
||||
|
||||
Assertions.assertThat(processed).isSameAs(entity); |
||||
Assertions.assertThat(processed).usingRecursiveComparison().isEqualTo(entity); |
||||
} |
||||
long generatedId = 112L; |
||||
when(operations.queryForObject(anyString(), any(SqlParameterSource.class), any(RowMapper.class))) |
||||
.thenReturn(generatedId); |
||||
|
||||
@Test // GH-1923
|
||||
void entityIsNotMarkedWithTargetSequence() { |
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, |
||||
PostgresDialect.INSTANCE, operations); |
||||
|
||||
RelationalMappingContext relationalMappingContext = new RelationalMappingContext(); |
||||
PostgresDialect mySqlDialect = PostgresDialect.INSTANCE; |
||||
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); |
||||
EntityWithIntSequence processed = (EntityWithIntSequence) subject.onBeforeSave(new EntityWithIntSequence(), |
||||
MutableAggregateChange.forSave(new EntityWithIntSequence())); |
||||
|
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations); |
||||
assertThat(processed.id).isEqualTo(112); |
||||
} |
||||
|
||||
NoSequenceEntity entity = new NoSequenceEntity(); |
||||
@Test // GH-2003
|
||||
void assignsUuidValues() { |
||||
|
||||
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity)); |
||||
UUID generatedId = UUID.randomUUID(); |
||||
when(operations.queryForObject(anyString(), any(SqlParameterSource.class), any(RowMapper.class))) |
||||
.thenReturn(generatedId); |
||||
|
||||
Assertions.assertThat(processed).isSameAs(entity); |
||||
Assertions.assertThat(processed).usingRecursiveComparison().isEqualTo(entity); |
||||
} |
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, |
||||
PostgresDialect.INSTANCE, operations); |
||||
|
||||
@Test // GH-1923
|
||||
void entityIdIsPopulatedFromSequence() { |
||||
EntityWithUuidSequence processed = (EntityWithUuidSequence) subject.onBeforeSave(new EntityWithUuidSequence(), |
||||
MutableAggregateChange.forSave(new EntityWithUuidSequence())); |
||||
|
||||
RelationalMappingContext relationalMappingContext = new RelationalMappingContext(); |
||||
relationalMappingContext.getRequiredPersistentEntity(EntityWithSequence.class); |
||||
assertThat(processed.id).isEqualTo(generatedId); |
||||
} |
||||
|
||||
PostgresDialect mySqlDialect = PostgresDialect.INSTANCE; |
||||
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); |
||||
@Table |
||||
static class NoSequenceEntity { |
||||
|
||||
long generatedId = 112L; |
||||
when(operations.queryForObject(anyString(), anyMap(), any(RowMapper.class))).thenReturn(generatedId); |
||||
@Id private Long id; |
||||
private Long name; |
||||
} |
||||
|
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations); |
||||
@Table |
||||
static class EntityWithSequence { |
||||
|
||||
EntityWithSequence entity = new EntityWithSequence(); |
||||
@Id |
||||
@Sequence(value = "id_seq", schema = "public") private Long id; |
||||
|
||||
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity)); |
||||
private Long name; |
||||
|
||||
Assertions.assertThat(processed).isSameAs(entity); |
||||
Assertions |
||||
.assertThat(processed) |
||||
.usingRecursiveComparison() |
||||
.ignoringFields("id") |
||||
.isEqualTo(entity); |
||||
Assertions.assertThat(entity.getId()).isEqualTo(generatedId); |
||||
} |
||||
public Long getId() { |
||||
return id; |
||||
} |
||||
} |
||||
|
||||
@Table |
||||
static class NoSequenceEntity { |
||||
@Table |
||||
static class EntityWithIntSequence { |
||||
|
||||
@Id |
||||
private Long id; |
||||
private Long name; |
||||
} |
||||
@Id |
||||
@Sequence(value = "id_seq") private int id; |
||||
|
||||
@Table |
||||
static class EntityWithSequence { |
||||
} |
||||
|
||||
@Id |
||||
@Sequence(value = "id_seq", schema = "public") |
||||
private Long id; |
||||
@Table |
||||
static class EntityWithUuidSequence { |
||||
|
||||
private Long name; |
||||
@Id |
||||
@Sequence(value = "id_seq") private UUID id; |
||||
|
||||
public Long getId() { |
||||
return id; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Loading…
Reference in new issue