From e51d224da3e77a37799fc6200742e5e7ae28866e Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Fri, 5 Sep 2025 11:06:49 +0200 Subject: [PATCH] [jOOQ/jOOQ#19003] Make DefaultExecuteContext available to internals via ThreadLocal --- .../java/org/jooq/impl/AbstractQuery.java | 2 +- .../java/org/jooq/impl/AbstractRoutine.java | 2 +- .../java/org/jooq/impl/BatchMultiple.java | 2 +- .../main/java/org/jooq/impl/BatchSingle.java | 2 +- jOOQ/src/main/java/org/jooq/impl/Cache.java | 43 ++- .../main/java/org/jooq/impl/CacheType.java | 37 ++- .../main/java/org/jooq/impl/CursorImpl.java | 2 +- .../java/org/jooq/impl/DefaultDSLContext.java | 2 +- .../org/jooq/impl/DefaultExecuteContext.java | 24 +- .../java/org/jooq/impl/ExecuteListeners.java | 26 +- .../main/java/org/jooq/impl/LazyCopyMap.java | 260 ++++++++++++++++++ jOOQ/src/main/java/org/jooq/impl/Tools.java | 11 +- 12 files changed, 376 insertions(+), 37 deletions(-) create mode 100644 jOOQ/src/main/java/org/jooq/impl/LazyCopyMap.java diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractQuery.java b/jOOQ/src/main/java/org/jooq/impl/AbstractQuery.java index 8e5b2d1016..5767abd7c4 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractQuery.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractQuery.java @@ -276,7 +276,7 @@ abstract class AbstractQuery extends AbstractAttachableQueryPa // in case this Query / Configuration was previously // deserialised DefaultExecuteContext ctx = new DefaultExecuteContext(c, this); - ExecuteListener listener = ExecuteListeners.get(ctx); + ExecuteListener listener = ExecuteListeners.get(ctx, true); int result = 0; try { diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractRoutine.java b/jOOQ/src/main/java/org/jooq/impl/AbstractRoutine.java index b5d6d65745..f1f25839d7 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractRoutine.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractRoutine.java @@ -650,7 +650,7 @@ implements private final int executeCallableStatement() { ExecuteContext ctx = new DefaultExecuteContext(configuration, this); - ExecuteListener listener = ExecuteListeners.get(ctx); + ExecuteListener listener = ExecuteListeners.get(ctx, true); try { // [#8968] Keep start() event inside of lifecycle management diff --git a/jOOQ/src/main/java/org/jooq/impl/BatchMultiple.java b/jOOQ/src/main/java/org/jooq/impl/BatchMultiple.java index d6309cc809..14224d7580 100644 --- a/jOOQ/src/main/java/org/jooq/impl/BatchMultiple.java +++ b/jOOQ/src/main/java/org/jooq/impl/BatchMultiple.java @@ -105,7 +105,7 @@ final class BatchMultiple extends AbstractBatch { return Stream.of(queries).mapToInt(configuration.dsl()::execute).toArray(); DefaultExecuteContext ctx = new DefaultExecuteContext(configuration, BatchMode.MULTIPLE, queries); - ExecuteListener listener = ExecuteListeners.get(ctx); + ExecuteListener listener = ExecuteListeners.get(ctx, true); try { diff --git a/jOOQ/src/main/java/org/jooq/impl/BatchSingle.java b/jOOQ/src/main/java/org/jooq/impl/BatchSingle.java index 3b1bb41bb5..41bcc7ee2b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/BatchSingle.java +++ b/jOOQ/src/main/java/org/jooq/impl/BatchSingle.java @@ -199,7 +199,7 @@ final class BatchSingle extends AbstractBatch implements BatchBindStep { private final int[] executePrepared() { DefaultExecuteContext ctx = new DefaultExecuteContext(configuration, BatchMode.SINGLE, new Query[] { query }); - ExecuteListener listener = ExecuteListeners.get(ctx); + ExecuteListener listener = ExecuteListeners.get(ctx, true); try { // [#8968] Keep start() event inside of lifecycle management diff --git a/jOOQ/src/main/java/org/jooq/impl/Cache.java b/jOOQ/src/main/java/org/jooq/impl/Cache.java index 971d3c4335..7ee0ac2093 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Cache.java +++ b/jOOQ/src/main/java/org/jooq/impl/Cache.java @@ -41,11 +41,9 @@ import static org.jooq.tools.StringUtils.defaultIfNull; import java.io.Serializable; import java.util.Map; -import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; -import org.jooq.impl.CacheType; import org.jooq.Configuration; /** @@ -74,17 +72,38 @@ final class Cache { if (!type.category.predicate.test(configuration.settings())) return operation.get(); - Object cacheOrNull = configuration.data(type); - if (cacheOrNull == null) { - synchronized (type) { - cacheOrNull = configuration.data(type); + Object cacheOrNull = null; + DefaultExecuteContext ctx = null; - if (cacheOrNull == null) - configuration.data(type, cacheOrNull = defaultIfNull( - configuration.cacheProvider().provide(new DefaultCacheContext(configuration, type)), - NULL - )); + // [#19003] If a cache type allows for being lazy copied, then the current ExecuteContext + // may hold a smaller instance of the cache containing only the data relevant + // to the current execution, not the globally available data. Accessing this + // thread-bound local copy is much faster as there is no contention among threads + // when the data is being accessed repeatedly, as for example DefaultRecordMapper + // instances when using heavily nested collection mapping. + if (type.lazyCopy) + ctx = DefaultExecuteContext.globalExecuteContext(); + + if (ctx != null) + cacheOrNull = ctx.data(type); + + if (cacheOrNull == null) { + cacheOrNull = configuration.data(type); + + if (cacheOrNull == null) { + synchronized (type) { + cacheOrNull = configuration.data(type); + + if (cacheOrNull == null) + configuration.data(type, cacheOrNull = defaultIfNull( + configuration.cacheProvider().provide(new DefaultCacheContext(configuration, type)), + NULL + )); + } } + + if (ctx != null && cacheOrNull != NULL) + ctx.data(type, cacheOrNull = new LazyCopyMap<>((Map) cacheOrNull)); } if (cacheOrNull == NULL) @@ -94,8 +113,6 @@ final class Cache { // contract. However since we cannot use ConcurrentHashMap.computeIfAbsent() // recursively, we have to revert to double checked locking nonetheless. // See also: https://stackoverflow.com/q/28840047/521799 - // [#18999] Our new ConcurrentReadWriteMap could deadlock when nesting writes - // in internal iterations Map cache = (Map) cacheOrNull; Object k = key.get(); Object v = cache.get(k); diff --git a/jOOQ/src/main/java/org/jooq/impl/CacheType.java b/jOOQ/src/main/java/org/jooq/impl/CacheType.java index 3f614f6ac3..e2769d0672 100644 --- a/jOOQ/src/main/java/org/jooq/impl/CacheType.java +++ b/jOOQ/src/main/java/org/jooq/impl/CacheType.java @@ -42,11 +42,13 @@ import static org.jooq.impl.CacheType.CacheCategory.PARSING_CONNECTION; import static org.jooq.impl.CacheType.CacheCategory.RECORD_MAPPER; import static org.jooq.impl.CacheType.CacheCategory.REFLECTION; +import java.util.concurrent.ConcurrentMap; import java.util.function.Predicate; import org.jooq.CacheProvider; import org.jooq.Configuration; import org.jooq.DSLContext; +import org.jooq.ExecuteContext; import org.jooq.RecordMapper; import org.jooq.RecordType; import org.jooq.conf.Settings; @@ -72,63 +74,78 @@ public enum CacheType { * A reflection cache for lookups of JPA annotated getters in * {@link DefaultRecordMapper}. */ - REFLECTION_CACHE_GET_ANNOTATED_GETTER(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-getter"), + REFLECTION_CACHE_GET_ANNOTATED_GETTER(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-getter", false), /** * A reflection cache for lookups of JPA annotated members in * {@link DefaultRecordMapper}. */ - REFLECTION_CACHE_GET_ANNOTATED_MEMBERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-members"), + REFLECTION_CACHE_GET_ANNOTATED_MEMBERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-members", false), /** * A reflection cache for lookups of JPA annotated setters in * {@link DefaultRecordMapper}. */ - REFLECTION_CACHE_GET_ANNOTATED_SETTERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-setters"), + REFLECTION_CACHE_GET_ANNOTATED_SETTERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-setters", false), /** * A reflection cache for lookups of getters matched by name in * {@link DefaultRecordMapper}. */ - REFLECTION_CACHE_GET_MATCHING_GETTER(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-getter"), + REFLECTION_CACHE_GET_MATCHING_GETTER(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-getter", false), /** * A reflection cache for lookups of members matched by name in * {@link DefaultRecordMapper}. */ - REFLECTION_CACHE_GET_MATCHING_MEMBERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-members"), + REFLECTION_CACHE_GET_MATCHING_MEMBERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-members", false), /** * A reflection cache for lookups of setters matched by name in * {@link DefaultRecordMapper}. */ - REFLECTION_CACHE_GET_MATCHING_SETTERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-setters"), + REFLECTION_CACHE_GET_MATCHING_SETTERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-setters", false), /** * A reflection cache to check if a type has any JPA annotations at all, in * {@link DefaultRecordMapper}. */ - REFLECTION_CACHE_HAS_COLUMN_ANNOTATIONS(REFLECTION, "org.jooq.configuration.reflection-cache.has-column-annotations"), + REFLECTION_CACHE_HAS_COLUMN_ANNOTATIONS(REFLECTION, "org.jooq.configuration.reflection-cache.has-column-annotations", false), /** * A cache used by the {@link DefaultRecordMapperProvider} to cache all * {@link RecordMapper} instances and their possibly expensive * initialisations per {@link RecordType} and {@link Class} pairs. */ - CACHE_RECORD_MAPPERS(RECORD_MAPPER, "org.jooq.configuration.cache.record-mappers"), + CACHE_RECORD_MAPPERS(RECORD_MAPPER, "org.jooq.configuration.cache.record-mappers", true), /** * [#8334] A cache for SQL to SQL translations in the * {@link DSLContext#parsingConnection()}, to speed up its usage. */ - CACHE_PARSING_CONNECTION(PARSING_CONNECTION, "org.jooq.configuration.cache.parsing-connection"); + CACHE_PARSING_CONNECTION(PARSING_CONNECTION, "org.jooq.configuration.cache.parsing-connection", false); + /** + * The category of the cache, indicating the logic that enables / disables + * it. + */ final CacheCategory category; + + /** + * The cache key in the backing {@link ConcurrentMap}. + */ final String key; - CacheType(CacheCategory category, String key) { + /** + * Whether the backing {@link ConcurrentMap} should be lazy copied in the + * scope of an {@link ExecuteContext} to reduce contention. + */ + final boolean lazyCopy; + + CacheType(CacheCategory category, String key, boolean lazyCopy) { this.category = category; this.key = key; + this.lazyCopy = lazyCopy; } enum CacheCategory { diff --git a/jOOQ/src/main/java/org/jooq/impl/CursorImpl.java b/jOOQ/src/main/java/org/jooq/impl/CursorImpl.java index e92bbdb5d5..d793343dd4 100644 --- a/jOOQ/src/main/java/org/jooq/impl/CursorImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/CursorImpl.java @@ -122,7 +122,7 @@ final class CursorImpl extends AbstractCursor { super(ctx.configuration(), (AbstractRow) Tools.row0(fields)); this.ctx = ctx; - this.listener = (listener != null ? listener : ExecuteListeners.getAndStart(ctx)); + this.listener = (listener != null ? listener : ExecuteListeners.getAndStart(ctx, true)); this.factory = recordFactory(table, type, this.fields); this.keepStatement = keepStatement; this.keepResultSet = keepResultSet; diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultDSLContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultDSLContext.java index 7ca4fd914e..2967f151e8 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultDSLContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultDSLContext.java @@ -1300,7 +1300,7 @@ public class DefaultDSLContext extends AbstractScope implements DSLContext, Seri @Override public Cursor fetchLazy(ResultSet rs, Field... fields) { ExecuteContext ctx = new DefaultExecuteContext(configuration()); - ExecuteListener listener = ExecuteListeners.getAndStart(ctx); + ExecuteListener listener = ExecuteListeners.getAndStart(ctx, true); ctx.resultSet(rs); return new CursorImpl<>(ctx, listener, fields, false, true); diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultExecuteContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultExecuteContext.java index d6f9419d6a..beacf807b6 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultExecuteContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultExecuteContext.java @@ -196,6 +196,7 @@ class DefaultExecuteContext implements ExecuteContext { } LOCAL_CONNECTION.remove(); + GLOBAL_EXECUTE_CONTEXT.remove(); } /** @@ -244,7 +245,7 @@ class DefaultExecuteContext implements ExecuteContext { // XXX: Static utility methods for handling Configuration lifecycle // ------------------------------------------------------------------------ - private static final ThreadLocal LOCAL_EXECUTE_CONTEXT = new ThreadLocal<>(); + static final ThreadLocal GLOBAL_EXECUTE_CONTEXT = new ThreadLocal<>(); /** * Get the registered {@link ExecuteContext}. @@ -253,20 +254,41 @@ class DefaultExecuteContext implements ExecuteContext { * {@link ExecuteContext} has been established, until the statement is * closed. */ + static final DefaultExecuteContext globalExecuteContext() { + return GLOBAL_EXECUTE_CONTEXT.get(); + } + + private static final ThreadLocal LOCAL_EXECUTE_CONTEXT = new ThreadLocal<>(); + + /** + * Get the registered {@link ExecuteContext}. + * + * @deprecated - [#19003] - 3.21.0 - This is no longer necessary, now that + * we have {@link #globalExecuteContext()}. + */ + @Deprecated static final ExecuteContext localExecuteContext() { return LOCAL_EXECUTE_CONTEXT.get(); } /** * Run a runnable with a new {@link #localExecuteContext()}. + * + * @deprecated - [#19003] - 3.21.0 - This is no longer necessary, now that + * we have {@link #globalExecuteContext()}. */ + @Deprecated static final void localExecuteContext(ExecuteContext ctx, ThrowingRunnable runnable) throws E { localExecuteContext(ctx, () -> { runnable.run(); return null; }); } /** * Run a supplier with a new {@link #localExecuteContext()}. + * + * @deprecated - [#19003] - 3.21.0 - This is no longer necessary, now that + * we have {@link #globalExecuteContext()}. */ + @Deprecated static final T localExecuteContext(ExecuteContext ctx, ThrowingSupplier supplier) throws E { ExecuteContext old = localExecuteContext(); diff --git a/jOOQ/src/main/java/org/jooq/impl/ExecuteListeners.java b/jOOQ/src/main/java/org/jooq/impl/ExecuteListeners.java index b4aedee0b9..ba98c3397b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ExecuteListeners.java +++ b/jOOQ/src/main/java/org/jooq/impl/ExecuteListeners.java @@ -39,6 +39,7 @@ package org.jooq.impl; import static java.lang.Boolean.FALSE; import static org.jooq.conf.InvocationOrder.REVERSE; +import static org.jooq.impl.DefaultExecuteContext.GLOBAL_EXECUTE_CONTEXT; import static org.jooq.impl.Tools.EMPTY_EXECUTE_LISTENER; import java.util.ArrayList; @@ -67,6 +68,7 @@ final class ExecuteListeners implements ExecuteListener { private final ExecuteListener[][] listeners; + private final boolean threadbound; // In some setups, these two events may get mixed up chronologically by the // Cursor. Postpone fetchEnd event until after resultEnd event, if there is @@ -76,14 +78,18 @@ final class ExecuteListeners implements ExecuteListener { /** * Initialise the provided {@link ExecuteListener} set and return a wrapper. + * + * @param ctx The {@link ExecuteContext}. + * @param threadbound Whether the {@link ExecuteContext} is thread bound, + * e.g. in ordinary JDBC use-cases, not reactive execution. */ - static ExecuteListener get(ExecuteContext ctx) { + static final ExecuteListener get(ExecuteContext ctx, boolean threadbound) { ExecuteListener[][] listeners = listeners(ctx); if (listeners == null) return EMPTY_LISTENER; else - return new ExecuteListeners(listeners); + return new ExecuteListeners(listeners, threadbound); } /** @@ -91,9 +97,13 @@ final class ExecuteListeners implements ExecuteListener { *

* Call this if the {@link ExecuteListener#start(ExecuteContext)} event * should be triggered eagerly. + * + * @param ctx The {@link ExecuteContext}. + * @param threadbound Whether the {@link ExecuteContext} is thread bound, + * e.g. in ordinary JDBC use-cases, not reactive execution. */ - static ExecuteListener getAndStart(ExecuteContext ctx) { - ExecuteListener result = get(ctx); + static final ExecuteListener getAndStart(ExecuteContext ctx, boolean threadbound) { + ExecuteListener result = get(ctx, threadbound); result.start(ctx); return result; } @@ -150,12 +160,16 @@ final class ExecuteListeners implements ExecuteListener { return result == null ? new ArrayList<>() : result; } - private ExecuteListeners(ExecuteListener[][] listeners) { + private ExecuteListeners(ExecuteListener[][] listeners, boolean threadbound) { this.listeners = listeners; + this.threadbound = threadbound; } @Override public final void start(ExecuteContext ctx) { + if (threadbound && ctx instanceof DefaultExecuteContext c) + GLOBAL_EXECUTE_CONTEXT.set(c); + for (ExecuteListener listener : listeners[0]) listener.start(ctx); } @@ -296,6 +310,8 @@ final class ExecuteListeners implements ExecuteListener { public final void end(ExecuteContext ctx) { for (ExecuteListener listener : listeners[1]) listener.end(ctx); + + GLOBAL_EXECUTE_CONTEXT.remove(); } @Override diff --git a/jOOQ/src/main/java/org/jooq/impl/LazyCopyMap.java b/jOOQ/src/main/java/org/jooq/impl/LazyCopyMap.java new file mode 100644 index 0000000000..db5f2081d0 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/LazyCopyMap.java @@ -0,0 +1,260 @@ +/* + * 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. + * + * Other licenses: + * ----------------------------------------------------------------------------- + * Commercial licenses for this work are available. These replace the above + * Apache-2.0 license and offer limited warranties, support, maintenance, and + * commercial database integrations. + * + * For more information, please visit: https://www.jooq.org/legal/licensing + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.impl; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.jooq.ExecuteContext; + + +/** + * A lazy copy of a backing map, typically to speed up cache access locally, for + * example for the scope of an {@link ExecuteContext}. + *

+ * The lazy copy map may not be in sync with the backing map, meaning that if + * the backing map is an {@link LRUCache}, or a similar cache like structure + * with eviction policies, it may be the case that an eviction happens in the + * backing map, but not in the local copy. While methods like {@link Map#size()} + * or {@link Map#entrySet()} will call through to the backing data structure, + * {@link Map#get(Object)} or {@link Map#containsKey(Object)} may not, but find + * a stale object that is no longer in the backing cache. The assumption here is + * that in the cache use-case, the backing cache would fill up the the same + * missing value again in case of a {@link Map#get(Object)} call, evicting + * another entry. These caveats will not produce any issues if only "cache + * relevant" methods like {@link Map#get(Object)} and + * {@link Map#put(Object, Object)} are called. + *

+ * The same is true if the backing map is modified for any other reason. + *

+ * This map is not thread safe, although it may reference a thread safe backing + * map. It is intended for thread local usage. + *

+ * This map does not support null keys or values. + * + * @author Lukas Eder + */ +final class LazyCopyMap extends AbstractMap { + + final Map copy; + final Map delegate; + + LazyCopyMap(Map delegate) { + this.copy = new HashMap<>(); + this.delegate = delegate; + } + + @SuppressWarnings("unchecked") + @Override + public V get(Object key) { + if (key == null) + throw new NullPointerException(); + + V value = copy.get(key); + + if (value == null) { + value = delegate.get(key); + + if (value != null) + copy.put((K) key, value); + } + + return value; + } + + @Override + public V put(K key, V value) { + if (key == null || value == null) + throw new NullPointerException(); + + copy.put(key, value); + return delegate.put(key, value); + } + + @Override + public Set> entrySet() { + + // [#19003] To keep things simple, Iterator::remove isn't supported + // [#19003] TODO: This currently doesn't copy contents, but it could + return Collections.unmodifiableSet(delegate.entrySet()); + } + + // keySet() and valueSet() from AbstractMap call through to entrySet() + + @Override + public V putIfAbsent(K key, V value) { + if (key == null || value == null) + throw new NullPointerException(); + + get(key); + copy.putIfAbsent(key, value); + return delegate.putIfAbsent(key, value); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + if (key == null || oldValue == null || newValue == null) + throw new NullPointerException(); + + copy.replace(key, oldValue, newValue); + return delegate.replace(key, oldValue, newValue); + } + + @Override + public V replace(K key, V value) { + if (key == null || value == null) + throw new NullPointerException(); + + copy.replace(key, value); + return delegate.replace(key, value); + } + + @Override + public void replaceAll(BiFunction function) { + copy.replaceAll(function); + delegate.replaceAll(function); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + if (key == null) + throw new NullPointerException(); + + get(key); + copy.computeIfAbsent(key, mappingFunction); + return delegate.computeIfAbsent(key, mappingFunction); + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + if (key == null) + throw new NullPointerException(); + + get(key); + copy.computeIfPresent(key, remappingFunction); + return delegate.computeIfPresent(key, remappingFunction); + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + if (key == null) + throw new NullPointerException(); + + get(key); + copy.compute(key, remappingFunction); + return delegate.compute(key, remappingFunction); + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + if (key == null || value == null) + throw new NullPointerException(); + + get(key); + copy.merge(key, value, remappingFunction); + return delegate.merge(key, value, remappingFunction); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return copy.isEmpty() + && delegate.isEmpty(); + } + + @Override + public boolean containsValue(Object value) { + return copy.containsValue(value) + || delegate.containsValue(value); + } + + @Override + public boolean containsKey(Object key) { + return copy.containsKey(key) + || delegate.containsKey(key); + } + + @Override + public V remove(Object key) { + copy.remove(key); + return delegate.remove(key); + } + + @Override + public boolean remove(Object key, Object value) { + copy.remove(key, value); + return delegate.remove(key, value); + } + + @Override + public void putAll(Map m) { + copy.putAll(m); + delegate.putAll(m); + } + + @Override + public void clear() { + copy.clear(); + delegate.clear(); + } + + @Override + public boolean equals(Object o) { + return delegate.equals(o); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public String toString() { + return delegate.toString(); + } +} + diff --git a/jOOQ/src/main/java/org/jooq/impl/Tools.java b/jOOQ/src/main/java/org/jooq/impl/Tools.java index 97a6262880..25a49cc784 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Tools.java +++ b/jOOQ/src/main/java/org/jooq/impl/Tools.java @@ -7814,11 +7814,18 @@ final class Tools { } static final ConverterContext converterContext(Attachable attachable) { - return new DefaultConverterContext(configuration(attachable)); + return converterContext(configuration(attachable)); } static final ConverterContext converterContext(Configuration configuration) { - return new DefaultConverterContext(configuration(configuration)); + + // [#19003] Allow for accessing a cached instance of the ConverterContext, where available. + DefaultExecuteContext c = DefaultExecuteContext.globalExecuteContext(); + + if (c != null && c.originalConfiguration() == configuration) + return c.converterContext(); + else + return new DefaultConverterContext(configuration(configuration)); } /**