diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/ConnectionCustomizer.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/ConnectionCustomizer.java new file mode 100644 index 00000000000..776bd55c418 --- /dev/null +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/ConnectionCustomizer.java @@ -0,0 +1,84 @@ +/* + * 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.connection; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.naming.Named; +import io.micronaut.core.order.Ordered; + +import java.sql.Connection; + +/** + * Handles before and after JDBC call for given connection {@link Connection}. + * + * Implementations of this interface can modify the behavior of connections created by Micronaut Data + * or do what might be needed before or after JDBC call for given connection. + * + * @author radovanradic + * @since 4.11 + */ +public interface ConnectionCustomizer extends Named, Ordered { + + /** + * Called before JDBC call is issues for given connection. + * + * This method allows implementations to perform additional setup or configuration on the connection. + * + * @param connection The JDBC connection + * @param methodInfo The method info + */ + void beforeCall(@NonNull Connection connection, @NonNull MethodInfo methodInfo); + + /** + * Called after JDBC call for given connection has been issued. + * + * This method allows implementations to release any resources or perform cleanup tasks related to the connection. + * + * @param connection The JDBC connection + */ + void afterCall(@NonNull Connection connection); + + /** + * Returns the name of this listener. Used for logging purposes. By default, returns class simple name. + * + * @return the name of this customizer + */ + @Override + @NonNull + default String getName() { + return getClass().getSimpleName(); + } + + /** + * Indicates whether this customizer is enabled. + * + * By default, all customizers are enabled. Subclasses may override this method to provide dynamic enabling/disabling logic. + * + * @return true if this customizer is enabled, false otherwise + */ + default boolean isEnabled() { + return true; + } + + /** + * Returns the name of the data source associated with this customizer. + * + * This method provides access to the name of the data source that this customizer is configured for. + * + * @return the name of the data source + */ + String getDataSourceName(); +} diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/MethodInfo.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/MethodInfo.java new file mode 100644 index 00000000000..5a8a1538d08 --- /dev/null +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/MethodInfo.java @@ -0,0 +1,32 @@ +/* + * 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.connection; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +/** + * The method info used for {@link ConnectionCustomizer} calls providing method metadata + * when might be needed. + * + * @param clazz The class name where method belongs + * @param methodName The method name + * @param annotationMetadata The annotation metadata for the method + */ +@Internal +public record MethodInfo(@NonNull Class clazz, @NonNull String methodName, @NonNull AnnotationMetadata annotationMetadata) { +} diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/OracleClientInfoConnectionCustomizer.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/OracleClientInfoConnectionCustomizer.java new file mode 100644 index 00000000000..c52396cea96 --- /dev/null +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/OracleClientInfoConnectionCustomizer.java @@ -0,0 +1,218 @@ +/* + * 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.connection; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource; +import io.micronaut.data.jdbc.config.DataJdbcConfiguration; +import io.micronaut.data.jdbc.connection.annotation.ClientInfo; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.runtime.ApplicationConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A customizer for Oracle database connections that sets client information before and after issuing JDBC call for given connection. + * + * This customizer checks if the connection is an Oracle database connection and then sets the client information + * (client ID, module, and action) before issuing JDBC call for the connection. It also clears these properties after the JDBC call for given connection. + * + * @author radovanradic + * @since 4.11 + */ +@EachBean(DataSource.class) +//@Requires(condition = OracleClientInfoCondition.class) +final class OracleClientInfoConnectionCustomizer implements ConnectionCustomizer { + + private static final String NAME_MEMBER = "name"; + private static final String VALUE_MEMBER = "value"; + private static final String INTERCEPTED_SUFFIX = "$Intercepted"; + private static final String ORACLE_CLIENT_INFO_ENABLED = "enable-oracle-client-info"; + + /** + * Constant for the Oracle connection client info client ID property name. + */ + private static final String ORACLE_CLIENT_ID = "OCSID.CLIENTID"; + /** + * Constant for the Oracle connection client info module property name. + */ + private static final String ORACLE_MODULE = "OCSID.MODULE"; + /** + * Constant for the Oracle connection client info action property name. + */ + private static final String ORACLE_ACTION = "OCSID.ACTION"; + /** + * Constant for the Oracle connection database product name. + */ + private static final String ORACLE_CONNECTION_DATABASE_PRODUCT_NAME = "Oracle"; + + private static final Logger LOG = LoggerFactory.getLogger(OracleClientInfoConnectionCustomizer.class); + + private static final Map, String> MODULE_CLASS_MAP = new ConcurrentHashMap<>(100); + + @Nullable + private final String applicationName; + private final String dataSourceName; + + private final boolean enabled; + + OracleClientInfoConnectionCustomizer(@NonNull DataSource dataSource, + @Parameter DataJdbcConfiguration jdbcConfiguration, + ApplicationContext applicationContext, + @Nullable ApplicationConfiguration applicationConfiguration) { + this.applicationName = applicationConfiguration != null ? applicationConfiguration.getName().orElse(null) : null; + this.enabled = isEnabled(jdbcConfiguration, applicationContext, dataSource); + this.dataSourceName = jdbcConfiguration.getName(); + } + + private boolean isEnabled(DataJdbcConfiguration dataJdbcConfiguration, ApplicationContext applicationContext, DataSource dataSource) { + if (dataJdbcConfiguration.getDialect() != Dialect.ORACLE) { + return false; + } + String property = "datasources." + dataJdbcConfiguration.getName() + "." + ORACLE_CLIENT_INFO_ENABLED; + if (!applicationContext.getProperty(property, Boolean.class).orElse(false)) { + return false; + } + boolean customizerEnabled; + try { + Connection connection = DelegatingDataSource.unwrapDataSource(dataSource).getConnection(); + customizerEnabled = isOracleConnection(connection); + } catch (SQLException e) { + LOG.error("Failed to get connection for oracle connection customizer", e); + customizerEnabled = false; + } + return customizerEnabled; + } + + @Override + public void beforeCall(@NonNull Connection connection, @NonNull MethodInfo methodInfo) { + // Set client info for the connection if Oracle connection before JDBC call + Map connectionClientInfo = getConnectionClientInfo(methodInfo); + if (connectionClientInfo != null && !connectionClientInfo.isEmpty()) { + LOG.trace("Setting connection tracing info to the Oracle connection"); + try { + for (Map.Entry additionalInfo : connectionClientInfo.entrySet()) { + String name = additionalInfo.getKey(); + String value = additionalInfo.getValue(); + connection.setClientInfo(name, value); + } + } catch (SQLClientInfoException e) { + LOG.debug("Failed to set connection tracing info", e); + } + } + } + + @Override + public void afterCall(@NonNull Connection connection) { + // Clear client info for connection if it was Oracle connection and client info was set previously + Properties properties = null; + try { + properties = connection.getClientInfo(); + } catch (SQLException e) { + LOG.debug("Failed to get connection client info", e); + } + if (properties != null && !properties.isEmpty()) { + try { + for (String key : properties.stringPropertyNames()) { + connection.setClientInfo(key, null); + } + } catch (SQLClientInfoException e) { + LOG.debug("Failed to clear connection tracing info", e); + } + } + } + + @Override + public String getName() { + return "Oracle Connection Client Info Customizer"; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public String getDataSourceName() { + return dataSourceName; + } + + /** + * Checks whether current connection is Oracle database connection. + * + * @param connection The connection + * @return true if current connection is Oracle database connection + */ + private boolean isOracleConnection(Connection connection) { + try { + String databaseProductName = connection.getMetaData().getDatabaseProductName(); + return StringUtils.isNotEmpty(databaseProductName) && databaseProductName.equalsIgnoreCase(ORACLE_CONNECTION_DATABASE_PRODUCT_NAME); + } catch (SQLException e) { + LOG.debug("Failed to get database product name from the connection", e); + return false; + } + } + + /** + * Gets connection client info from the {@link ClientInfo} annotation. + * + * @param methodInfo The method info + * @return The connection client info or null if not configured to be used + */ + private @Nullable Map getConnectionClientInfo(@NonNull MethodInfo methodInfo) { + AnnotationMetadata annotationMetadata = methodInfo.annotationMetadata(); + AnnotationValue annotation = annotationMetadata.getAnnotation(ClientInfo.class); + List> clientInfoValues = annotation != null ? annotation.getAnnotations(VALUE_MEMBER) : Collections.EMPTY_LIST; + Map clientInfoAttributes = new LinkedHashMap<>(clientInfoValues.size()); + if (CollectionUtils.isNotEmpty(clientInfoValues)) { + for (AnnotationValue clientInfoValue : clientInfoValues) { + String name = clientInfoValue.getRequiredValue(NAME_MEMBER, String.class); + String value = clientInfoValue.getRequiredValue(VALUE_MEMBER, String.class); + clientInfoAttributes.put(name, value); + } + } + // Fallback defaults if not provided in the annotation + if (StringUtils.isNotEmpty(applicationName)) { + clientInfoAttributes.putIfAbsent(ORACLE_CLIENT_ID, applicationName); + } + clientInfoAttributes.putIfAbsent(ORACLE_MODULE, + MODULE_CLASS_MAP.computeIfAbsent(methodInfo.clazz(), + clazz -> clazz.getName().replace(INTERCEPTED_SUFFIX, "")) + ); + clientInfoAttributes.putIfAbsent(ORACLE_ACTION, methodInfo.methodName()); + + return clientInfoAttributes; + } +} diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/annotation/ClientInfo.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/annotation/ClientInfo.java new file mode 100644 index 00000000000..0376ea4375f --- /dev/null +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/annotation/ClientInfo.java @@ -0,0 +1,67 @@ +/* + * 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.connection.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Repeatable annotation for {@link ClientInfo.Attribute}. + * + * @author radovanradic + * @since 4.11 + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ClientInfo { + + /** + * Returns the list of the client information attributes. + * + * @return the attribute collection + */ + Attribute[] value() default {}; + + /** + * An annotation used to set client info for the connection. + * + * @author radovanradic + * @since 4.11 + */ + @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(ClientInfo.class) + @interface Attribute { + + /** + * Returns the name of the client information attribute. + * + * @return the attribute name + */ + String name(); + + /** + * Returns the value of the client information attribute. + * + * @return the attribute value + */ + String value(); + } +} diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java index 33e0f42d904..f1aa38255fd 100644 --- a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java @@ -16,6 +16,7 @@ package io.micronaut.data.jdbc.operations; import io.micronaut.aop.InvocationContext; +import io.micronaut.aop.MethodInvocationContext; import io.micronaut.context.BeanContext; import io.micronaut.context.annotation.EachBean; import io.micronaut.context.annotation.Parameter; @@ -33,6 +34,8 @@ import io.micronaut.data.connection.annotation.Connectable; import io.micronaut.data.exceptions.DataAccessException; import io.micronaut.data.jdbc.config.DataJdbcConfiguration; +import io.micronaut.data.jdbc.connection.ConnectionCustomizer; +import io.micronaut.data.jdbc.connection.MethodInfo; import io.micronaut.data.jdbc.convert.JdbcConversionContext; import io.micronaut.data.jdbc.mapper.ColumnIndexCallableResultReader; import io.micronaut.data.jdbc.mapper.ColumnIndexResultSetReader; @@ -98,6 +101,8 @@ import jakarta.annotation.PreDestroy; import jakarta.inject.Named; import jakarta.persistence.Tuple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.sql.CallableStatement; @@ -146,6 +151,8 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository AutoCloseable, SyncCascadeOperations.SyncCascadeOperationsHelper { + private static final Logger LOG = LoggerFactory.getLogger(DefaultJdbcRepositoryOperations.class); + private final ConnectionOperations connectionOperations; private final TransactionOperations transactionOperations; private final DataSource dataSource; @@ -159,6 +166,7 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository private final ColumnIndexCallableResultReader columnIndexCallableResultReader; private final Map> sqlExceptionMappers = new EnumMap<>(Dialect.class); + private final List connectionCustomizers; /** * Default constructor. @@ -197,7 +205,8 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository JdbcSchemaHandler schemaHandler, @Nullable JsonMapper jsonMapper, SqlJsonColumnMapperProvider sqlJsonColumnMapperProvider, - List sqlExceptionMapperList) { + List sqlExceptionMapperList, + List connectionCustomizers) { super( dataSourceName, new ColumnNameResultSetReader(conversionService), @@ -232,6 +241,7 @@ public final class DefaultJdbcRepositoryOperations extends AbstractSqlRepository sqlExceptionMappers.put(dialect, dialectSqlExceptionMapperList); } } + this.connectionCustomizers = connectionCustomizers.stream().filter(c -> c.isEnabled() && c.getDataSourceName().equals(dataSourceName)).toList(); } @Override @@ -343,7 +353,8 @@ public ReactiveRepositoryOperations reactive() { @Nullable @Override public R findOne(@NonNull PreparedQuery pq) { - return executeRead(connection -> findOne(connection, getSqlPreparedQuery(pq))); + SqlPreparedQuery sqlPreparedQuery = getSqlPreparedQuery(pq); + return executeRead(getMethodInfo(sqlPreparedQuery), connection -> findOne(connection, sqlPreparedQuery)); } private R findOne(Connection connection, SqlPreparedQuery preparedQuery) { @@ -423,13 +434,29 @@ private List findAll(SqlStoredQuery sqlStoredQuery, ResultSet rs return result; } + private @NonNull MethodInfo getMethodInfo(SqlPreparedQuery sqlPreparedQuery) { + if (sqlPreparedQuery.getInvocationContext() instanceof MethodInvocationContext methodInvocationContext) { + return new MethodInfo(methodInvocationContext.getTarget().getClass(), methodInvocationContext.getMethodName(), + sqlPreparedQuery.getAnnotationMetadata()); + } + return null; + } + + private @Nullable MethodInfo getMethodInfo(EntityOperation operation) { + if (operation.getInvocationContext() instanceof MethodInvocationContext methodInvocationContext) { + return new MethodInfo(methodInvocationContext.getTarget().getClass(), methodInvocationContext.getMethodName(), + operation.getAnnotationMetadata()); + } + return null; + } + @Override public boolean exists(@NonNull PreparedQuery pq) { - return executeRead(connection -> { + SqlPreparedQuery sqlPreparedQuery = getSqlPreparedQuery(pq); + return executeRead(getMethodInfo(sqlPreparedQuery), connection -> { try { - SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); - try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, false, true)) { - preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery)); + try (PreparedStatement ps = prepareStatement(connection::prepareStatement, sqlPreparedQuery, false, true)) { + sqlPreparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, sqlPreparedQuery)); try (ResultSet rs = ps.executeQuery()) { return rs.next(); } @@ -534,14 +561,16 @@ private void closeResultSet(Connection connection, PreparedStatement ps, ResultS @NonNull @Override public Iterable findAll(@NonNull PreparedQuery preparedQuery) { - return executeRead(connection -> findAll(connection, getSqlPreparedQuery(preparedQuery), true)); + SqlPreparedQuery sqlPreparedQuery = getSqlPreparedQuery(preparedQuery); + return executeRead(getMethodInfo(sqlPreparedQuery), connection -> findAll(connection, sqlPreparedQuery, true)); } @NonNull @Override public Optional executeUpdate(@NonNull PreparedQuery pq) { - return executeWrite(connection -> { - SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); + SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); + MethodInfo methodInfo = getMethodInfo(preparedQuery); + return executeWrite(methodInfo, connection -> { try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, true, false)) { preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery)); int result = ps.executeUpdate(); @@ -560,8 +589,9 @@ public Optional executeUpdate(@NonNull PreparedQuery pq) { @Override public List execute(PreparedQuery pq) { - return executeWrite(connection -> { - SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); + SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); + MethodInfo methodInfo = getMethodInfo(preparedQuery); + return executeWrite(methodInfo, connection -> { try { if (preparedQuery.isProcedure()) { return callProcedure(connection, preparedQuery); @@ -606,7 +636,8 @@ private Integer sum(Stream stream) { @Override public Optional deleteAll(@NonNull DeleteBatchOperation operation) { - return Optional.ofNullable(executeWrite(connection -> { + MethodInfo methodInfo = getMethodInfo(operation); + return Optional.ofNullable(executeWrite(methodInfo, connection -> { SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity(); @@ -628,7 +659,8 @@ public Optional deleteAll(@NonNull DeleteBatchOperation operation @Override public int delete(@NonNull DeleteOperation operation) { - return executeWrite(connection -> { + MethodInfo methodInfo = getMethodInfo(operation); + return executeWrite(methodInfo, connection -> { SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery.getPersistentEntity(), operation.getEntity(), storedQuery); @@ -639,7 +671,8 @@ public int delete(@NonNull DeleteOperation operation) { @Override public R deleteReturning(DeleteReturningOperation operation) { - return executeWrite(connection -> { + MethodInfo methodInfo = getMethodInfo(operation); + return executeWrite(methodInfo, connection -> { SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery.getPersistentEntity(), operation.getEntity(), storedQuery); @@ -650,7 +683,8 @@ public R deleteReturning(DeleteReturningOperation operation) { @Override public List deleteAllReturning(DeleteReturningBatchOperation operation) { - return executeWrite(connection -> { + MethodInfo methodInfo = getMethodInfo(operation); + return executeWrite(methodInfo, connection -> { SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity(); @@ -671,7 +705,8 @@ public List deleteAllReturning(DeleteReturningBatchOperation ope @NonNull @Override public T update(@NonNull UpdateOperation operation) { - return executeWrite(connection -> { + MethodInfo methodInfo = getMethodInfo(operation); + return executeWrite(methodInfo, connection -> { SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery.getPersistentEntity(), operation.getEntity(), storedQuery); @@ -683,7 +718,8 @@ public T update(@NonNull UpdateOperation operation) { @NonNull @Override public Iterable updateAll(@NonNull UpdateBatchOperation operation) { - return executeWrite(connection -> { + MethodInfo methodInfo = getMethodInfo(operation); + return executeWrite(methodInfo, connection -> { final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); final RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity(); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); @@ -706,7 +742,8 @@ public Iterable updateAll(@NonNull UpdateBatchOperation operation) { @NonNull @Override public T persist(@NonNull InsertOperation operation) { - return executeWrite(connection -> { + MethodInfo methodInfo = getMethodInfo(operation); + return executeWrite(methodInfo, connection -> { final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery, storedQuery.getPersistentEntity(), operation.getEntity(), true); @@ -746,7 +783,8 @@ public Page findPage(@NonNull PagedQuery query) { @Override @NonNull public Iterable persistAll(@NonNull InsertBatchOperation operation) { - return executeWrite(connection -> { + MethodInfo methodInfo = getMethodInfo(operation); + return executeWrite(methodInfo, connection -> { final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); final RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity(); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); @@ -767,28 +805,60 @@ public Iterable persistAll(@NonNull InsertBatchOperation operation) { }); } - private I executeRead(Function fn) { + private I executeRead(MethodInfo methodInfo, Function fn) { if (!jdbcConfiguration.isAllowConnectionPerOperation() && connectionOperations.findConnectionStatus().isEmpty()) { throw connectionNotFoundAndNewNotAllowed(); } return connectionOperations.executeRead(status -> { Connection connection = status.getConnection(); + beforeCall(connection, methodInfo); applySchema(connection); - return fn.apply(connection); + I result = fn.apply(connection); + afterCall(connection, methodInfo); + return result; }); } - private I executeWrite(Function fn) { + private I executeWrite(@Nullable MethodInfo methodInfo, Function fn) { if (!jdbcConfiguration.isAllowConnectionPerOperation() && connectionOperations.findConnectionStatus().isEmpty()) { throw connectionNotFoundAndNewNotAllowed(); } return connectionOperations.executeWrite(status -> { Connection connection = status.getConnection(); + beforeCall(connection, methodInfo); applySchema(connection); - return fn.apply(connection); + I result = fn.apply(connection); + afterCall(connection, methodInfo); + return result; }); } + private void beforeCall(@NonNull Connection connection, @Nullable MethodInfo methodInfo) { + if (methodInfo == null) { + return; + } + for (ConnectionCustomizer connectionCustomizer : connectionCustomizers) { + try { + connectionCustomizer.beforeCall(connection, methodInfo); + } catch (Exception e) { + LOG.debug("An error occurred when calling listener {} beforeCall.", connectionCustomizer.getName(), e); + } + } + } + + private void afterCall(@NonNull Connection connection, @Nullable MethodInfo methodInfo) { + if (methodInfo == null) { + return; + } + for (ConnectionCustomizer connectionCustomizer : connectionCustomizers) { + try { + connectionCustomizer.afterCall(connection); + } catch (Exception e) { + LOG.debug("An error occurred when calling listener {} afterCall.", connectionCustomizer.getName(), e); + } + } + } + private DataAccessException connectionNotFoundAndNewNotAllowed() { return new DataAccessException("Connection is required for this operation. Annotate with @" + Connectable.class + ", @Transactional or enable `isAllowConnectionPerOperation`."); } @@ -840,7 +910,7 @@ private ConnectionContext getConnectionCtx() { @NonNull @Override public R execute(@NonNull ConnectionCallback callback) { - return executeWrite(connection -> { + return executeWrite(null, connection -> { try { return callback.call(connection); } catch (SQLException e) { diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleRepositorySetClientInfoSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleRepositorySetClientInfoSpec.groovy new file mode 100644 index 00000000000..b81f23bf4a3 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleRepositorySetClientInfoSpec.groovy @@ -0,0 +1,31 @@ +/* + * 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.oraclexe + +/** + * The test for setting oracle connection client info. + */ +class OracleRepositorySetClientInfoSpec extends OracleXERepositorySpec { + + @Override + Map getProperties() { + return super.getProperties() + [ + 'micronaut.application.name': 'OracleRepositorySetClientInfoSpec', + 'datasources.default.enable-oracle-client-info': 'true' + ] + } +} + diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEAuthorRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEAuthorRepository.java index 75c90ba308f..6e684389204 100644 --- a/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEAuthorRepository.java +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEAuthorRepository.java @@ -17,6 +17,7 @@ import io.micronaut.data.annotation.Join; import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.jdbc.connection.annotation.ClientInfo; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.tck.entities.Author; import io.micronaut.data.tck.repositories.AuthorRepository; @@ -25,5 +26,6 @@ public interface OracleXEAuthorRepository extends AuthorRepository { @Override @Join(value = "books", type = Join.Type.LEFT_FETCH) + @ClientInfo.Attribute(name = "OCSID.ACTION", value = "QueryAuthorByName") Author queryByName(String name); } diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEBookRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEBookRepository.java index 83be0b5b6f2..f41aca96a0e 100644 --- a/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEBookRepository.java +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/oraclexe/OracleXEBookRepository.java @@ -15,12 +15,14 @@ */ package io.micronaut.data.jdbc.oraclexe; +import io.micronaut.core.annotation.NonNull; import io.micronaut.data.annotation.Expandable; import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.Query; import io.micronaut.data.annotation.TypeDef; import io.micronaut.data.annotation.sql.Procedure; import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.jdbc.connection.annotation.ClientInfo; import io.micronaut.data.model.DataType; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.tck.entities.Book; @@ -31,6 +33,7 @@ import java.util.List; @JdbcRepository(dialect = Dialect.ORACLE) +@ClientInfo.Attribute(name = "OCSID.MODULE", value = "BOOKS") public abstract class OracleXEBookRepository extends BookRepository { public OracleXEBookRepository(OracleXEAuthorRepository authorRepository) { super(authorRepository); @@ -53,6 +56,11 @@ public OracleXEBookRepository(OracleXEAuthorRepository authorRepository) { @Procedure("add1") public abstract int add1Aliased(int input); + @Override + @ClientInfo(value = {@ClientInfo.Attribute(name = "OCSID.MODULE", value = "CustomModule"), + @ClientInfo.Attribute(name = "OCSID.ACTION", value = "INSERT")}) + public abstract @NonNull Book save(@NonNull Book book); + // public abstract Book updateReturning(Book book); // // public abstract String updateReturningTitle(Book book); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index f8a4cf65485..bb7eeda349b 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -15,6 +15,7 @@ */ package io.micronaut.data.runtime.operations.internal.sql; +import io.micronaut.aop.InvocationContext; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -445,6 +446,11 @@ public QueryResultInfo getQueryResultInfo() { return sqlStoredQuery.getQueryResultInfo(); } + @Override + public InvocationContext getInvocationContext() { + return invocationContext; + } + /** * Build a sort for ID for the given entity. * diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlPreparedQuery.java index 574cdb62669..f283118e47c 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/SqlPreparedQuery.java @@ -15,6 +15,7 @@ */ package io.micronaut.data.runtime.operations.internal.sql; +import io.micronaut.aop.InvocationContext; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.model.Pageable; @@ -54,4 +55,11 @@ public interface SqlPreparedQuery extends BindableParametersPreparedQuery< */ @Override QueryResultInfo getQueryResultInfo(); + + /** + * Returns the invocation context associated with this prepared query. + * + * @return the invocation context + */ + InvocationContext getInvocationContext(); } diff --git a/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc b/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc index 8b5a2299ecc..e86df98fd7b 100644 --- a/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc +++ b/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc @@ -61,5 +61,24 @@ As seen in the configuration above you should also configure the dialect. Althou IMPORTANT: The dialect setting in configuration does *not* replace the need to ensure the correct dialect is set at the repository. If the dialect is H2 in configuration, the repository should have `@JdbcRepository(dialect = Dialect.H2)` / `@R2dbcRepository(dialect = Dialect.H2)`. Because repositories are computed at compile time, the configuration value is not known at that time. +=== JDBC Connection client info tracing + +In order to trace SQL calls using `java.sql.Connection.setClientInfo(String, String)` method, you can +annotate a repository with the ann:data.jdbc.connection.annotation.ClientInfo[] annotation or ann:data.jdbc.connection.annotation.ClientInfo.Attribute[] for individual client info. + +Note that the ann:data.jdbc.connection.annotation.ClientInfo.Attribute[] annotation can be used on either the class or the method, thus allowing customization of the module or action individually. + +For Oracle database, following attributes can be set to the JDBC connection client info: `OCSID.MODULE`, `OCSID.ACTION` and `OCSID.CLIENTID` and provided in ann:data.jdbc.connection.annotation.ClientInfo.Attribute[]. +If some of these attributes are not provided then Micronaut Data Jdbc is going to populate values automatically for Oracle connections: + +*** `OCSID.MODULE` will get the value of the class name where annotation `@ClientInfo.Attribute` is added (usually Micronaut Data repository class) +*** `OCSID.ACTION` will get the value of the method name which is annotated with `@ClientInfo.Attribute` annotation +*** `OCSID.CLIENTID` will get the value of the Micronaut application name, if configured + +Please note this feature is currently supported only for Oracle database connections. In order to enable Oracle JDBC connection client info to be set, +you need to specify the configuration property `datasources.default.enable-oracle-client-info=true` on a per datasource basis. + +For other databases, new implementation of api:data.jdbc.connection.ConnectionCustomizer[] can be implemented. + TIP: See the guide for https://guides.micronaut.io/latest/micronaut-data-jdbc-repository.html[Access a Database with Micronaut Data JDBC] to learn more.