From 4a09882c79381ebdc8b36f9faa590fad4e899de9 Mon Sep 17 00:00:00 2001 From: Radovan Radic Date: Thu, 5 Dec 2024 14:55:13 +0100 Subject: [PATCH] Support including Module and Action in each JDBC session with Oracle JDBC (#3183) * Investigation for setting connection client info * Use Connectable interface to pass connection client tracing info * Remove unneeded changes * Use connectable for Oracle Book repo * Documentation, comments and code cleanup. * Rename ConnectionClientTracingInfo to ConnectionTracingInfo * Changes per CR comments, still not there. * Change as suggested in PR comment * Properly clear connection client info * Introduce new OracleConnectionClientInfo annotation for connection client info tracing * Fixed javadoc * Resolve module/class name using InvocationContext.getTarget() * Introduce ConnectionCustomizer for more flexibility * More changes as suggested in pull request comments. * Add @Experimental to ConnectionClientInfoDetails * Applied more suggestions from pull request comments. * Updated comments * Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc Co-authored-by: Graeme Rocher * Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc Co-authored-by: Graeme Rocher * Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc Co-authored-by: Graeme Rocher * Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc Co-authored-by: Graeme Rocher * Renamed classes as suggested and also property to enable Oracle client info connection listener * Refactoring according to suggestions in PR comments. * Cleanup * Cache module name by the class name * Remove enabled attribute and create repeatable annotation * Fixes for Sonar * Update @since attributes * Rename classes to shorter names * Rename connection client info annotations * Connection interceptor * Adjust code per Denis's suggestion for callbacks * Revert changes in ConnectableInterceptor * Revert new line * Reverted some changes and refactored code to resolve Sonar warning * Updated docs --------- Co-authored-by: Graeme Rocher Co-authored-by: Denis Stepanov --- data-connection-jdbc/build.gradle | 1 + .../oracle/OracleClientInfoCondition.java | 70 ++++++ .../OracleClientInfoConnectionCustomizer.java | 201 ++++++++++++++++++ .../data/connection/ConnectionDefinition.java | 13 +- .../DefaultConnectionDefinition.java | 37 +++- .../connection/annotation/ClientInfo.java | 67 ++++++ .../support/AbstractConnectionOperations.java | 27 ++- .../support/ConnectionCustomizer.java | 46 ++++ .../DefaultJdbcRepositoryOperations.java | 45 ++-- .../OracleRepositorySetClientInfoSpec.groovy | 30 +++ .../oraclexe/OracleXEAuthorRepository.java | 2 + .../jdbc/oraclexe/OracleXEBookRepository.java | 11 +- .../internal/sql/DefaultSqlPreparedQuery.java | 6 + .../internal/sql/SqlPreparedQuery.java | 10 + .../guide/dbc/jdbc/jdbcConfiguration.adoc | 17 ++ 15 files changed, 547 insertions(+), 36 deletions(-) create mode 100644 data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoCondition.java create mode 100644 data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java create mode 100644 data-connection/src/main/java/io/micronaut/data/connection/annotation/ClientInfo.java create mode 100644 data-connection/src/main/java/io/micronaut/data/connection/support/ConnectionCustomizer.java create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleRepositorySetClientInfoSpec.groovy diff --git a/data-connection-jdbc/build.gradle b/data-connection-jdbc/build.gradle index 7df18343c9b..55ee535bc48 100644 --- a/data-connection-jdbc/build.gradle +++ b/data-connection-jdbc/build.gradle @@ -13,6 +13,7 @@ dependencies { api projects.micronautDataConnection implementation mnSql.micronaut.jdbc implementation mn.micronaut.aop + implementation mn.micronaut.runtime testAnnotationProcessor mn.micronaut.inject.java diff --git a/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoCondition.java b/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoCondition.java new file mode 100644 index 00000000000..b3571dac082 --- /dev/null +++ b/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoCondition.java @@ -0,0 +1,70 @@ +/* + * 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.connection.jdbc.oracle; + +import io.micronaut.context.BeanResolutionContext; +import io.micronaut.context.Qualifier; +import io.micronaut.context.condition.Condition; +import io.micronaut.context.condition.ConditionContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.naming.Named; +import io.micronaut.inject.BeanDefinition; + +/** + * A condition that determines whether to customize Oracle client information based on configuration properties. + * + * This condition checks if the data source dialect is set to Oracle and if the 'customize-oracle-client-info' property is enabled. + * + * @author radovanradic + * @since 4.11 + */ +@Internal +final class OracleClientInfoCondition implements Condition { + + static final String DATASOURCES = "datasources"; + private static final Character DOT = '.'; + private static final String DIALECT = "dialect"; + private static final String ORACLE_CLIENT_INFO_ENABLED = "enable-oracle-client-info"; + private static final String ORACLE_DIALECT = "ORACLE"; + + @Override + public boolean matches(ConditionContext context) { + BeanResolutionContext beanResolutionContext = context.getBeanResolutionContext(); + String dataSourceName; + if (beanResolutionContext == null) { + return true; + } else { + Qualifier currentQualifier = beanResolutionContext.getCurrentQualifier(); + if (currentQualifier == null && context.getComponent() instanceof BeanDefinition definition) { + currentQualifier = definition.getDeclaredQualifier(); + } + if (currentQualifier instanceof Named named) { + dataSourceName = named.getName(); + } else { + dataSourceName = "default"; + } + } + + String dialectProperty = DATASOURCES + DOT + dataSourceName + DOT + DIALECT; + String dialect = context.getProperty(dialectProperty, String.class).orElse(null); + if (!ORACLE_DIALECT.equalsIgnoreCase(dialect)) { + return false; + } + + String property = DATASOURCES + DOT + dataSourceName + DOT + ORACLE_CLIENT_INFO_ENABLED; + return context.getProperty(property, Boolean.class, false); + } +} diff --git a/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java b/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java new file mode 100644 index 00000000000..3f542e13cd7 --- /dev/null +++ b/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java @@ -0,0 +1,201 @@ +/* + * 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.connection.jdbc.oracle; + +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +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.ConnectionDefinition; +import io.micronaut.data.connection.ConnectionStatus; +import io.micronaut.data.connection.annotation.ClientInfo; +import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource; +import io.micronaut.data.connection.support.AbstractConnectionOperations; +import io.micronaut.data.connection.support.ConnectionCustomizer; +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.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * A customizer for Oracle database connections that sets client information after opening and clears before closing. + * + * This customizer checks if the connection is an Oracle database connection and then sets the client information + * (client ID, module, and action) after opening the connection. It also clears these properties before closing the connection. + * + * @author radovanradic + * @since 4.11 + */ +@EachBean(DataSource.class) +@Requires(condition = OracleClientInfoCondition.class) +@Context +@Internal +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"; + + /** + * 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; + + OracleClientInfoConnectionCustomizer(@NonNull DataSource dataSource, + @NonNull @Parameter AbstractConnectionOperations connectionOperations, + @Nullable ApplicationConfiguration applicationConfiguration) { + this.applicationName = applicationConfiguration != null ? applicationConfiguration.getName().orElse(null) : null; + try { + Connection connection = DelegatingDataSource.unwrapDataSource(dataSource).getConnection(); + if (isOracleConnection(connection)) { + connectionOperations.addConnectionCustomizer(this); + } + } catch (SQLException e) { + LOG.error("Failed to get connection for oracle connection listener", e); + } + } + + @Override + public Function, R> intercept(Function, R> operation) { + return connectionStatus -> { + ConnectionDefinition connectionDefinition = connectionStatus.getDefinition(); + // Set client info for connection if Oracle before issue JDBC call + Map connectionClientInfo = getConnectionClientInfo(connectionDefinition); + applyClientInfo(connectionStatus, connectionClientInfo); + try { + return operation.apply(connectionStatus); + } finally { + // Clear client info for connection if it was Oracle connection and client info was set previously + clearClientInfo(connectionStatus, connectionClientInfo); + } + + }; + } + + private void applyClientInfo(@NonNull ConnectionStatus connectionStatus, @NonNull Map connectionClientInfo) { + if (CollectionUtils.isNotEmpty(connectionClientInfo)) { + Connection connection = connectionStatus.getConnection(); + 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); + } + } + } + + private void clearClientInfo(@NonNull ConnectionStatus connectionStatus, @NonNull Map connectionClientInfo) { + if (CollectionUtils.isNotEmpty(connectionClientInfo)) { + try { + Connection connection = connectionStatus.getConnection(); + for (String key : connectionClientInfo.keySet()) { + connection.setClientInfo(key, null); + } + } catch (SQLClientInfoException e) { + LOG.debug("Failed to clear connection tracing info", e); + } + } + } + + /** + * 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 connectionDefinition The connection definition info + * @return The connection client info or null if not configured to be used + */ + private @NonNull Map getConnectionClientInfo(@NonNull ConnectionDefinition connectionDefinition) { + AnnotationMetadata annotationMetadata = connectionDefinition.getAnnotationMetadata(); + AnnotationValue annotation = annotationMetadata.getAnnotation(ClientInfo.class); + List> clientInfoValues = annotation != null ? annotation.getAnnotations(VALUE_MEMBER) : Collections.emptyList(); + 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); + } + if (annotationMetadata instanceof MethodInvocationContext methodInvocationContext) { + clientInfoAttributes.putIfAbsent(ORACLE_MODULE, + MODULE_CLASS_MAP.computeIfAbsent(methodInvocationContext.getTarget().getClass(), + clazz -> clazz.getName().replace(INTERCEPTED_SUFFIX, "")) + ); + clientInfoAttributes.putIfAbsent(ORACLE_ACTION, methodInvocationContext.getName()); + } + return clientInfoAttributes; + } +} diff --git a/data-connection/src/main/java/io/micronaut/data/connection/ConnectionDefinition.java b/data-connection/src/main/java/io/micronaut/data/connection/ConnectionDefinition.java index 705f1965f67..b085463754a 100644 --- a/data-connection/src/main/java/io/micronaut/data/connection/ConnectionDefinition.java +++ b/data-connection/src/main/java/io/micronaut/data/connection/ConnectionDefinition.java @@ -16,6 +16,8 @@ package io.micronaut.data.connection; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -28,7 +30,7 @@ * @author Denis Stepanov * @since 4.0.0 */ -public interface ConnectionDefinition { +public interface ConnectionDefinition extends AnnotationMetadataProvider { /** * Use the default propagation value. @@ -117,6 +119,15 @@ enum Propagation { @NonNull ConnectionDefinition withName(String name); + /** + * Connection definition with new annotation metadata. + * + * @param annotationMetadata The new annotation metadata + * @return A new connection definition with specified annotation metadata + */ + @NonNull + ConnectionDefinition withAnnotationMetadata(AnnotationMetadata annotationMetadata); + /** * Create a new {@link ConnectionDefinition} for the given behaviour. * diff --git a/data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionDefinition.java b/data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionDefinition.java index 3bfbd7a46f0..f67259bd975 100644 --- a/data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionDefinition.java +++ b/data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionDefinition.java @@ -15,6 +15,7 @@ */ package io.micronaut.data.connection; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -25,10 +26,11 @@ /** * Default implementation of the {@link ConnectionDefinition} interface. * - * @param name The connection name - * @param propagationBehavior The propagation behaviour - * @param timeout The timeout - * @param readOnlyValue The read only + * @param name The connection name + * @param propagationBehavior The propagation behaviour + * @param timeout The timeout + * @param readOnlyValue The read only + * @param annotationMetadata The annotation metadata * @author Denis Stepanov * @since 4.0.0 */ @@ -37,19 +39,25 @@ public record DefaultConnectionDefinition( @Nullable String name, Propagation propagationBehavior, @Nullable Duration timeout, - Boolean readOnlyValue + Boolean readOnlyValue, + @NonNull AnnotationMetadata annotationMetadata ) implements ConnectionDefinition { DefaultConnectionDefinition(String name) { - this(name, PROPAGATION_DEFAULT, null, null); + this(name, PROPAGATION_DEFAULT, null, null, AnnotationMetadata.EMPTY_METADATA); } public DefaultConnectionDefinition(Propagation propagationBehaviour) { - this(null, propagationBehaviour, null, null); + this(null, propagationBehaviour, null, null, AnnotationMetadata.EMPTY_METADATA); } public DefaultConnectionDefinition(String name, boolean readOnly) { - this(name, PROPAGATION_DEFAULT, null, readOnly); + this(name, PROPAGATION_DEFAULT, null, readOnly, AnnotationMetadata.EMPTY_METADATA); + } + + public DefaultConnectionDefinition(String name, Propagation propagationBehavior, Duration timeout, + Boolean readOnlyValue) { + this(name, propagationBehavior, timeout, readOnlyValue, AnnotationMetadata.EMPTY_METADATA); } @Override @@ -76,13 +84,22 @@ public String getName() { @Override public ConnectionDefinition withPropagation(Propagation propagation) { - return new DefaultConnectionDefinition(name, propagation, timeout, readOnlyValue); + return new DefaultConnectionDefinition(name, propagation, timeout, readOnlyValue, annotationMetadata); } @Override public ConnectionDefinition withName(String name) { - return new DefaultConnectionDefinition(name, propagationBehavior, timeout, readOnlyValue); + return new DefaultConnectionDefinition(name, propagationBehavior, timeout, readOnlyValue, annotationMetadata); } + @Override + public ConnectionDefinition withAnnotationMetadata(AnnotationMetadata newAnnotationMetadata) { + return new DefaultConnectionDefinition(name, propagationBehavior, timeout, readOnlyValue, newAnnotationMetadata); + } + + @Override + public @NonNull AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } } diff --git a/data-connection/src/main/java/io/micronaut/data/connection/annotation/ClientInfo.java b/data-connection/src/main/java/io/micronaut/data/connection/annotation/ClientInfo.java new file mode 100644 index 00000000000..bb9b87c8eaf --- /dev/null +++ b/data-connection/src/main/java/io/micronaut/data/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.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-connection/src/main/java/io/micronaut/data/connection/support/AbstractConnectionOperations.java b/data-connection/src/main/java/io/micronaut/data/connection/support/AbstractConnectionOperations.java index 665ce79aac3..f9dfe18894c 100644 --- a/data-connection/src/main/java/io/micronaut/data/connection/support/AbstractConnectionOperations.java +++ b/data-connection/src/main/java/io/micronaut/data/connection/support/AbstractConnectionOperations.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.order.OrderUtil; import io.micronaut.core.propagation.PropagatedContext; import io.micronaut.core.propagation.PropagatedContextElement; import io.micronaut.data.connection.exceptions.ConnectionException; @@ -29,6 +30,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; @@ -45,6 +48,23 @@ public abstract class AbstractConnectionOperations implements ConnectionOpera protected final Logger logger = LoggerFactory.getLogger(getClass()); + private final List> connectionCustomizers = new ArrayList<>(10); + + /** + * Adds a connection customizer to the list of customizers that will be notified before or after a call to the underlying data repository + * is issues. + * + * The added customizer will be sorted according to its order using the {@link OrderUtil#sort(List)} method. + * + * @param connectionCustomizer the connection customizer to add + * + * @since 4.11 + */ + public void addConnectionCustomizer(@NonNull ConnectionCustomizer connectionCustomizer) { + connectionCustomizers.add(connectionCustomizer); + OrderUtil.sort(connectionCustomizers); + } + /** * Opens a new connection. * @@ -84,6 +104,10 @@ private Optional> findContextElement() { @Override public final R execute(@NonNull ConnectionDefinition definition, @NonNull Function, R> callback) { ConnectionPropagatedContextElement existingConnection = findContextElement().orElse(null); + for (ConnectionCustomizer connectionCustomizer : connectionCustomizers) { + callback = connectionCustomizer.intercept(callback); + } + @NonNull Function, R> finalCallback = callback; return switch (definition.getPropagationBehavior()) { case REQUIRED -> { if (existingConnection == null) { @@ -101,7 +125,7 @@ public final R execute(@NonNull ConnectionDefinition definition, @NonNull Fu if (existingConnection == null) { yield executeWithNewConnection(definition, callback); } - yield suspend(existingConnection, () -> executeWithNewConnection(definition, callback)); + yield suspend(existingConnection, () -> executeWithNewConnection(definition, finalCallback)); } }; } @@ -236,7 +260,6 @@ public void executionComplete() { return newStatus; } - private record ConnectionPropagatedContextElement( ConnectionOperations connectionOperations, ConnectionStatus status) implements PropagatedContextElement { diff --git a/data-connection/src/main/java/io/micronaut/data/connection/support/ConnectionCustomizer.java b/data-connection/src/main/java/io/micronaut/data/connection/support/ConnectionCustomizer.java new file mode 100644 index 00000000000..1a79841ae1a --- /dev/null +++ b/data-connection/src/main/java/io/micronaut/data/connection/support/ConnectionCustomizer.java @@ -0,0 +1,46 @@ +/* + * 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.connection.support; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.order.Ordered; +import io.micronaut.data.connection.ConnectionStatus; + +import java.util.function.Function; + +/** + * Customizes connection before or after data repository call based on the provided {@link ConnectionStatus}. + * + * Implementations of this interface can modify the behavior of connections created by Micronaut Data + * or do what might be needed before or after call to the data repository (for example JDBC statement call). + * + * @see ConnectionStatus + * @param The connection type + * + * @author radovanradic + * @since 4.11 + */ +@Experimental +public interface ConnectionCustomizer extends Ordered { + + /** + * Intercept the connection operation. + * @param operation The operation + * @param The result + * @return the operation callback + */ + Function, R> intercept(Function, R> operation); +} 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..906b014fd5e 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 @@ -29,6 +29,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.data.connection.ConnectionDefinition; import io.micronaut.data.connection.ConnectionOperations; import io.micronaut.data.connection.annotation.Connectable; import io.micronaut.data.exceptions.DataAccessException; @@ -343,7 +344,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(connection -> findOne(connection, getSqlPreparedQuery(pq)), sqlPreparedQuery.getInvocationContext()); } private R findOne(Connection connection, SqlPreparedQuery preparedQuery) { @@ -425,9 +427,9 @@ private List findAll(SqlStoredQuery sqlStoredQuery, ResultSet rs @Override public boolean exists(@NonNull PreparedQuery pq) { + SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); return executeRead(connection -> { try { - SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, false, true)) { preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery)); try (ResultSet rs = ps.executeQuery()) { @@ -437,7 +439,7 @@ public boolean exists(@NonNull PreparedQuery pq) { } catch (SQLException e) { throw new DataAccessException("Error executing SQL query: " + e.getMessage(), e); } - }); + }, preparedQuery.getInvocationContext()); } @NonNull @@ -534,14 +536,15 @@ 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(connection -> findAll(connection, sqlPreparedQuery, true), sqlPreparedQuery.getInvocationContext()); } @NonNull @Override public Optional executeUpdate(@NonNull PreparedQuery pq) { + SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); return executeWrite(connection -> { - SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); try (PreparedStatement ps = prepareStatement(connection::prepareStatement, preparedQuery, true, false)) { preparedQuery.bindParameters(new JdbcParameterBinder(connection, ps, preparedQuery)); int result = ps.executeUpdate(); @@ -555,13 +558,13 @@ public Optional executeUpdate(@NonNull PreparedQuery pq) { } catch (SQLException e) { throw sqlExceptionToDataAccessException(e, preparedQuery.getDialect(), sqlException -> new DataAccessException("Error executing SQL UPDATE: " + sqlException.getMessage(), sqlException)); } - }); + }, preparedQuery.getInvocationContext()); } @Override public List execute(PreparedQuery pq) { + SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); return executeWrite(connection -> { - SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq); try { if (preparedQuery.isProcedure()) { return callProcedure(connection, preparedQuery); @@ -571,7 +574,7 @@ public List execute(PreparedQuery pq) { } catch (SQLException e) { throw sqlExceptionToDataAccessException(e, preparedQuery.getDialect(), sqlException -> new DataAccessException("Error executing SQL UPDATE: " + sqlException.getMessage(), sqlException)); } - }); + }, preparedQuery.getInvocationContext()); } private List callProcedure(Connection connection, SqlPreparedQuery preparedQuery) throws SQLException { @@ -623,7 +626,7 @@ public Optional deleteAll(@NonNull DeleteBatchOperation operation return op.rowsUpdated; }) ); - })); + }, operation.getInvocationContext())); } @Override @@ -634,7 +637,7 @@ public int delete(@NonNull DeleteOperation operation) { JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery.getPersistentEntity(), operation.getEntity(), storedQuery); op.delete(); return op; - }).rowsUpdated; + }, operation.getInvocationContext()).rowsUpdated; } @Override @@ -645,7 +648,7 @@ public R deleteReturning(DeleteReturningOperation operation) { JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery.getPersistentEntity(), operation.getEntity(), storedQuery); op.delete(); return (R) op.getEntity(); - }); + }, operation.getInvocationContext()); } @Override @@ -665,7 +668,7 @@ public List deleteAllReturning(DeleteReturningBatchOperation ope op.delete(); return op.getEntity(); }).toList(); - }); + }, operation.getInvocationContext()); } @NonNull @@ -677,7 +680,7 @@ public T update(@NonNull UpdateOperation operation) { JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery.getPersistentEntity(), operation.getEntity(), storedQuery); op.update(); return op.getEntity(); - }); + }, operation.getInvocationContext()); } @NonNull @@ -700,7 +703,7 @@ public Iterable updateAll(@NonNull UpdateBatchOperation operation) { JdbcEntitiesOperations op = new JdbcEntitiesOperations<>(ctx, persistentEntity, operation, storedQuery); op.update(); return op.getEntities(); - }); + }, operation.getInvocationContext()); } @NonNull @@ -712,7 +715,7 @@ public T persist(@NonNull InsertOperation operation) { JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery, storedQuery.getPersistentEntity(), operation.getEntity(), true); op.persist(); return op; - }).getEntity(); + }, operation.getInvocationContext()).getEntity(); } @Nullable @@ -764,25 +767,25 @@ public Iterable persistAll(@NonNull InsertBatchOperation operation) { return op.getEntities(); } - }); + }, operation.getInvocationContext()); } - private I executeRead(Function fn) { + private I executeRead(Function fn, AnnotationMetadata annotationMetadata) { if (!jdbcConfiguration.isAllowConnectionPerOperation() && connectionOperations.findConnectionStatus().isEmpty()) { throw connectionNotFoundAndNewNotAllowed(); } - return connectionOperations.executeRead(status -> { + return connectionOperations.execute(ConnectionDefinition.READ_ONLY.withAnnotationMetadata(annotationMetadata), status -> { Connection connection = status.getConnection(); applySchema(connection); return fn.apply(connection); }); } - private I executeWrite(Function fn) { + private I executeWrite(Function fn, AnnotationMetadata annotationMetadata) { if (!jdbcConfiguration.isAllowConnectionPerOperation() && connectionOperations.findConnectionStatus().isEmpty()) { throw connectionNotFoundAndNewNotAllowed(); } - return connectionOperations.executeWrite(status -> { + return connectionOperations.execute(ConnectionDefinition.DEFAULT.withAnnotationMetadata(annotationMetadata), status -> { Connection connection = status.getConnection(); applySchema(connection); return fn.apply(connection); @@ -846,7 +849,7 @@ public R execute(@NonNull ConnectionCallback callback) { } catch (SQLException e) { throw new DataAccessException("Error executing SQL Callback: " + e.getMessage(), e); } - }); + }, AnnotationMetadata.EMPTY_METADATA); } @NonNull 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..741085f5c81 --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleRepositorySetClientInfoSpec.groovy @@ -0,0 +1,30 @@ +/* + * 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..63d9327f4f2 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 @@ -16,6 +16,7 @@ package io.micronaut.data.jdbc.oraclexe; import io.micronaut.data.annotation.Join; +import io.micronaut.data.connection.annotation.ClientInfo; import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.tck.entities.Author; @@ -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..2db834042f2 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,11 +15,12 @@ */ 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.connection.annotation.ClientInfo; import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.DataType; import io.micronaut.data.model.query.builder.sql.Dialect; @@ -31,6 +32,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,7 +55,12 @@ public OracleXEBookRepository(OracleXEAuthorRepository authorRepository) { @Procedure("add1") public abstract int add1Aliased(int input); -// public abstract Book updateReturning(Book book); + @Override + @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..9f9c6cc1aba 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,13 @@ public interface SqlPreparedQuery extends BindableParametersPreparedQuery< */ @Override QueryResultInfo getQueryResultInfo(); + + /** + * Returns the invocation context associated with this prepared query. + * + * @return the invocation context + */ + @Nullable + @SuppressWarnings("java:S1452") + InvocationContext getInvocationContext(); } diff --git a/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc b/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc index 8b5a2299ecc..ad5e3e0acae 100644 --- a/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc +++ b/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc @@ -61,5 +61,22 @@ 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. +=== 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.connection.annotation.ClientInfo[] annotation or ann:data.connection.annotation.ClientInfo.Attribute[] for individual client info. + +Note that the ann:data.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 connection client info: `OCSID.MODULE`, `OCSID.ACTION` and `OCSID.CLIENTID` and provided in ann:data.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..enable-oracle-client-info=true` on a per datasource basis. + 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.