Skip to content

Commit

Permalink
Minor Improvement for Oracle JsonView optimistic locking (#2915)
Browse files Browse the repository at this point in the history
* Minor Improvement for Oracle JsonView optimistic locking

Extract SqlExceptionMapper for extensibility.
Added support for optimistic lock checking in batch update for JsonView.

* Separate handlers for Jdbc and R2dbc

* Address review comments.

* Removed unused var and imports

* More changes per CR comments

* Sonar suggested changes
  • Loading branch information
radovanradic authored Apr 25, 2024
1 parent bbd95c7 commit ee8cf96
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.data.connection.ConnectionOperations;
import io.micronaut.data.connection.annotation.Connectable;
import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
Expand Down Expand Up @@ -108,6 +109,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -143,6 +145,7 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository
ReactiveCapableRepository,
AutoCloseable,
SyncCascadeOperations.SyncCascadeOperationsHelper<DefaultJdbcRepositoryOperations.JdbcOperationContext> {

private final ConnectionOperations<Connection> connectionOperations;
private final TransactionOperations<Connection> transactionOperations;
private final DataSource dataSource;
Expand All @@ -157,13 +160,15 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository

private final ColumnNameCallableResultReader columnNameCallableResultReader;
private final ColumnIndexCallableResultReader columnIndexCallableResultReader;
private final Map<Dialect, List<SqlExceptionMapper>> sqlExceptionMappers = new EnumMap<>(Dialect.class);

/**
* Default constructor.
*
* @param dataSourceName The data source name
* @param jdbcConfiguration The jdbcConfiguration
* @param dataSource The datasource
* @param connectionOperations The connection operations
* @param transactionOperations The JDBC operations for the data source
* @param executorService The executor service
* @param beanContext The bean context
Expand All @@ -175,26 +180,26 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository
* @param schemaHandler The schema handler
* @param jsonMapper The JSON mapper
* @param sqlJsonColumnMapperProvider The SQL JSON column mapper provider
* @param connectionOperations
* @param sqlExceptionMapperList The SQL exception mapper list
*/
@Internal
@SuppressWarnings("ParameterNumber")
DefaultJdbcRepositoryOperations(@Parameter String dataSourceName,
@Parameter DataJdbcConfiguration jdbcConfiguration,
DataSource dataSource,
@Parameter ConnectionOperations<Connection> connectionOperations,
@Parameter TransactionOperations<Connection> transactionOperations,
@Named("io") @Nullable ExecutorService executorService,
BeanContext beanContext,
@NonNull DateTimeProvider dateTimeProvider,
RuntimeEntityRegistry entityRegistry,
DataConversionService conversionService,
AttributeConverterRegistry attributeConverterRegistry,
@Nullable
SchemaTenantResolver schemaTenantResolver,
JdbcSchemaHandler schemaHandler,
@Nullable JsonMapper jsonMapper,
SqlJsonColumnMapperProvider<ResultSet> sqlJsonColumnMapperProvider) {
@Parameter DataJdbcConfiguration jdbcConfiguration,
DataSource dataSource,
@Parameter ConnectionOperations<Connection> connectionOperations,
@Parameter TransactionOperations<Connection> transactionOperations,
@Named("io") @Nullable ExecutorService executorService,
BeanContext beanContext,
@NonNull DateTimeProvider dateTimeProvider,
RuntimeEntityRegistry entityRegistry,
DataConversionService conversionService,
AttributeConverterRegistry attributeConverterRegistry,
@Nullable SchemaTenantResolver schemaTenantResolver,
JdbcSchemaHandler schemaHandler,
@Nullable JsonMapper jsonMapper,
SqlJsonColumnMapperProvider<ResultSet> sqlJsonColumnMapperProvider,
List<SqlExceptionMapper> sqlExceptionMapperList) {
super(
dataSourceName,
new ColumnNameResultSetReader(conversionService),
Expand All @@ -220,6 +225,17 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository
this.jdbcConfiguration = jdbcConfiguration;
this.columnNameCallableResultReader = new ColumnNameCallableResultReader(conversionService);
this.columnIndexCallableResultReader = new ColumnIndexCallableResultReader(conversionService);
if (CollectionUtils.isNotEmpty(sqlExceptionMapperList)) {
for (SqlExceptionMapper sqlExceptionMapper : sqlExceptionMapperList) {
Dialect dialect = sqlExceptionMapper.getDialect();
List<SqlExceptionMapper> dialectSqlExceptionMapperList = sqlExceptionMappers.get(dialect);
if (dialectSqlExceptionMapperList == null) {
dialectSqlExceptionMapperList = new ArrayList<>();
}
dialectSqlExceptionMapperList.add(sqlExceptionMapper);
sqlExceptionMappers.put(dialect, dialectSqlExceptionMapperList);
}
}
}

@NonNull
Expand Down Expand Up @@ -531,11 +547,7 @@ public Optional<Number> executeUpdate(@NonNull PreparedQuery<?, Number> pq) {
}
return Optional.of(result);
} catch (SQLException e) {
Throwable throwable = handleSqlException(e, preparedQuery.getDialect());
if (throwable instanceof DataAccessException dataAccessException) {
throw dataAccessException;
}
throw new DataAccessException("Error executing SQL UPDATE: " + e.getMessage(), e);
throw sqlExceptionToDataAccessException(e, preparedQuery.getDialect(), sqlException -> new DataAccessException("Error executing SQL UPDATE: " + sqlException.getMessage(), sqlException));
}
});
}
Expand All @@ -551,11 +563,7 @@ public <R> List<R> execute(PreparedQuery<?, R> pq) {
return findAll(connection, preparedQuery, false);
}
} catch (SQLException e) {
Throwable throwable = handleSqlException(e, preparedQuery.getDialect());
if (throwable instanceof DataAccessException dataAccessException) {
throw dataAccessException;
}
throw new DataAccessException("Error executing SQL UPDATE: " + e.getMessage(), e);
throw sqlExceptionToDataAccessException(e, preparedQuery.getDialect(), sqlException -> new DataAccessException("Error executing SQL UPDATE: " + sqlException.getMessage(), sqlException));
}
});
}
Expand Down Expand Up @@ -936,6 +944,28 @@ public T next() {
return StreamSupport.stream(iterable.spliterator(), false);
}

/**
* Maps SQL exception, used in context of update but could be used elsewhere.
* It will return custom {@DataAccessException} based on the {@link SQLException} or null
* if it cannot be mapped to the custom {@link DataAccessException}.
*
* @param sqlException the SQL exception
* @param dialect the SQL dialect
* @return custom {@link DataAccessException} exception based on {@link SQLException} that was thrown or null
* if exception is not mappable to {@link DataAccessException} in given dialect {@link SqlExceptionMapper}
*/
@Nullable
private DataAccessException mapSqlException(SQLException sqlException, Dialect dialect) {
List<SqlExceptionMapper> dialectSqlExceptionMapperList = sqlExceptionMappers.getOrDefault(dialect, List.of());
for (SqlExceptionMapper dialectSqlExceptionMapper : dialectSqlExceptionMapperList) {
DataAccessException dataAccessException = dialectSqlExceptionMapper.mapSqlException(sqlException);
if (dataAccessException != null) {
return dataAccessException;
}
}
return null;
}

@NonNull
private ResultConsumer.Context<ResultSet> newMappingContext(ResultSet rs) {
return new ResultConsumer.Context<>() {
Expand Down Expand Up @@ -996,6 +1026,23 @@ private Object getGeneratedIdentity(@NonNull ResultSet generatedKeysResultSet, R
return columnIndexResultSetReader.readDynamic(generatedKeysResultSet, 1, identity.getDataType());
}

/**
* Handles {@link SQLException} first trying to map it to {@link DataAccessException} using {@link SqlExceptionMapper}.
* If mapped exception is not {@link DataAccessException} then returns {@link DataAccessException} using provided fallbackMapper.
*
* @param sqlException The SQL exception that was thrown
* @param dialect The dialect
* @param fallbackMapper The fallback mapper that returns {@link DataAccessException} if {@link SQLException} was not mapped to {@link DataAccessException}
* @return DataAccessException
*/
private DataAccessException sqlExceptionToDataAccessException(SQLException sqlException, Dialect dialect, Function<SQLException, DataAccessException> fallbackMapper) {
DataAccessException dataAccessException = mapSqlException(sqlException, dialect);
if (dataAccessException != null) {
return dataAccessException;
}
return fallbackMapper.apply(sqlException);
}

@Override
public boolean isSupportsBatchInsert(JdbcOperationContext jdbcOperationContext, RuntimePersistentEntity<?> persistentEntity) {
return isSupportsBatchInsert(persistentEntity, jdbcOperationContext.dialect);
Expand Down Expand Up @@ -1138,8 +1185,8 @@ protected void execute() throws SQLException {
checkOptimisticLocking(1, rowsUpdated);
}
} catch (SQLException e) {
Throwable throwable = handleSqlException(e, ctx.dialect);
if (throwable instanceof DataAccessException dataAccessException) {
DataAccessException dataAccessException = mapSqlException(e, ctx.dialect);
if (dataAccessException != null) {
throw dataAccessException;
}
throw e;
Expand Down Expand Up @@ -1245,7 +1292,7 @@ private void setParameters(PreparedStatement stmt, SqlStoredQuery<T, ?> storedQu
}

@Override
protected void execute() throws SQLException {
protected void execute() {
if (QUERY_LOG.isDebugEnabled()) {
QUERY_LOG.debug("Executing SQL query: {}", storedQuery.getQuery());
}
Expand Down Expand Up @@ -1282,6 +1329,8 @@ protected void execute() throws SQLException {
int expected = (int) entities.stream().filter(d -> !d.vetoed).count();
checkOptimisticLocking(expected, rowsUpdated);
}
} catch (SQLException e) {
throw sqlExceptionToDataAccessException(e, ctx.dialect, sqlException -> new DataAccessException("Error executing batch SQL UPDATE: " + sqlException.getMessage(), sqlException));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2017-2024 original 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 io.micronaut.data.jdbc.operations;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.exceptions.OptimisticLockException;
import io.micronaut.data.model.query.builder.sql.Dialect;
import jakarta.inject.Singleton;

import java.sql.SQLException;

/**
* The {@link SqlExceptionMapper} for {@link Dialect#ORACLE}.
* Handles {@link SQLException} for Oracle update commands. Can add more logic if needed, but this
* now handles only optimistic locking exception for given error code.
*
* @since 4.8.0
*/
@Singleton
@Internal
final class OracleSqlExceptionMapper implements SqlExceptionMapper {

private static final int JSON_VIEW_ETAG_NOT_MATCHING_ERROR = 42699;

@Override
public Dialect getDialect() {
return Dialect.ORACLE;
}

@Override
@Nullable
public DataAccessException mapSqlException(@NonNull SQLException sqlException) {
if (sqlException.getErrorCode() == JSON_VIEW_ETAG_NOT_MATCHING_ERROR) {
return new OptimisticLockException("ETAG did not match when updating record: " + sqlException.getMessage(), sqlException);
}
return null;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2017-2024 original 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 io.micronaut.data.jdbc.operations;

import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.order.Ordered;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.model.query.builder.sql.Dialect;

import java.sql.SQLException;

/**
* The {@link SQLException} mapper interface. Can be used to map given SQL exceptions to some custom exceptions
* (for example {@link DataAccessException} and its descendents like {@link io.micronaut.data.exceptions.OptimisticLockException}).
*
* @since 4.8.0
*/
@Experimental
public interface SqlExceptionMapper extends Ordered {

/**
* @return the {@link Dialect} that this mapper supports
*/
@NonNull
Dialect getDialect();

/**
* Maps {@link SQLException} to custom {@link DataAccessException}.
* In case when mapper is not able to map {@link SQLException} to custom {@link DataAccessException} then result will be null
* indicating that mapper cannot map the exception.
*
* @param sqlException The SQL exception
* @return mapped {@link DataAccessException} from {@link SQLException} or if mapper cannot map {@link SQLException} then returns null
*/
@Nullable
DataAccessException mapSqlException(@NonNull SQLException sqlException);
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ class OracleJdbcJsonViewSpec extends Specification {
studentViewRepository.update(newJoshStudentView)
then:
thrown(OptimisticLockException)

when:"Optimistic lock exception with invalid ETAG in batch update"
studentViewRepository.updateAll(List.of(newJoshStudentView))
then:
thrown(OptimisticLockException)
}

def "insert new"() {
Expand Down
Loading

0 comments on commit ee8cf96

Please sign in to comment.