Skip to content

Commit fd8ec0a

Browse files
Add support for configuring PreparedStatementCache
- Make PreparedStatementCache (and implementations) public - Expose preparedStatementCache discovery option. Similar to what r2dbc-postgresql exposes: -1 = indefinite cache, 0 = no cache, n = lru cache with max size n. - Add LRUPreparedStatementCache and NoPreparedStatementCache implementations. [r2dbc#227] Signed-off-by: Suraj Vijayakumar <[email protected]>
1 parent 6b9f927 commit fd8ec0a

12 files changed

+466
-8
lines changed

src/main/java/io/r2dbc/mssql/IndefinitePreparedStatementCache.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
*
2828
* @author Mark Paluch
2929
*/
30-
class IndefinitePreparedStatementCache implements PreparedStatementCache {
30+
public class IndefinitePreparedStatementCache implements PreparedStatementCache {
3131

3232
private final Map<String, Integer> preparedStatements = new ConcurrentHashMap<>();
3333

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2018-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.r2dbc.mssql;
18+
19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
21+
import java.util.function.Function;
22+
23+
import static io.r2dbc.mssql.util.Assert.isTrue;
24+
import static io.r2dbc.mssql.util.Assert.requireNonNull;
25+
26+
/**
27+
* {@link PreparedStatementCache} implementation that maintains a simple "least recently used" cache.
28+
* By default, this cache has a maximum size of 32.
29+
*
30+
* @author Suraj Vijayakumar
31+
*/
32+
public class LRUPreparedStatementCache implements PreparedStatementCache {
33+
34+
private static final int DEFAULT_MAX_SIZE = 32;
35+
36+
private final Map<String, Integer> handleCache;
37+
private final Map<String, Object> sqlCache;
38+
39+
public LRUPreparedStatementCache() {
40+
this(DEFAULT_MAX_SIZE);
41+
}
42+
43+
public LRUPreparedStatementCache(int maxSize) {
44+
isTrue(maxSize > 0, "Max cache size must be > 0");
45+
46+
handleCache = new LRUCache<>(maxSize);
47+
sqlCache = new LRUCache<>(maxSize);
48+
}
49+
50+
@Override
51+
public int getHandle(String sql, Binding binding) {
52+
requireNonNull(sql, "SQL query must not be null");
53+
requireNonNull(binding, "Binding must not be null");
54+
55+
String key = createKey(sql, binding);
56+
return handleCache.getOrDefault(key, UNPREPARED);
57+
}
58+
59+
@Override
60+
public void putHandle(int handle, String sql, Binding binding) {
61+
requireNonNull(sql, "SQL query must not be null");
62+
requireNonNull(binding, "Binding must not be null");
63+
64+
String key = createKey(sql, binding);
65+
handleCache.put(key, handle);
66+
}
67+
68+
@SuppressWarnings("unchecked")
69+
@Override
70+
public <T> T getParsedSql(String sql, Function<String, T> parseFunction) {
71+
requireNonNull(sql, "SQL query must not be null");
72+
requireNonNull(parseFunction, "Parse function must not be null");
73+
74+
return (T) sqlCache.computeIfAbsent(sql, parseFunction);
75+
}
76+
77+
@Override
78+
public int size() {
79+
return handleCache.size();
80+
}
81+
82+
private static String createKey(String sql, Binding binding) {
83+
return sql + "-" + binding.getFormalParameters();
84+
}
85+
86+
private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
87+
88+
private final int maxSize;
89+
90+
LRUCache(int maxSize) {
91+
super(16, .75f, true);
92+
93+
this.maxSize = maxSize;
94+
}
95+
96+
@Override
97+
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
98+
return size() > maxSize;
99+
}
100+
}
101+
}

src/main/java/io/r2dbc/mssql/MssqlConnectionConfiguration.java

+21-5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ public final class MssqlConnectionConfiguration {
8686

8787
private final Predicate<String> preferCursoredExecution;
8888

89+
private final PreparedStatementCache preparedStatementCache;
90+
8991
@Nullable
9092
private final Duration lockWaitTimeout;
9193

@@ -118,8 +120,8 @@ public final class MssqlConnectionConfiguration {
118120
private final String username;
119121

120122
private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable UUID connectionId, Duration connectTimeout, @Nullable String database, String host, String hostNameInCertificate,
121-
@Nullable Duration lockWaitTimeout, CharSequence password, Predicate<String> preferCursoredExecution, int port, boolean sendStringParametersAsUnicode,
122-
boolean ssl,
123+
@Nullable Duration lockWaitTimeout, CharSequence password, Predicate<String> preferCursoredExecution, PreparedStatementCache preparedStatementCache,
124+
int port, boolean sendStringParametersAsUnicode, boolean ssl,
123125
Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer,
124126
@Nullable Function<SslContextBuilder, SslContextBuilder> sslTunnelSslContextBuilderCustomizer, boolean tcpKeepAlive, boolean tcpNoDelay,
125127
boolean trustServerCertificate, @Nullable File trustStore, @Nullable String trustStoreType,
@@ -134,6 +136,7 @@ private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable
134136
this.lockWaitTimeout = lockWaitTimeout;
135137
this.password = Assert.requireNonNull(password, "password must not be null");
136138
this.preferCursoredExecution = Assert.requireNonNull(preferCursoredExecution, "preferCursoredExecution must not be null");
139+
this.preparedStatementCache = Assert.requireNonNull(preparedStatementCache, "preparedStatementCache must not be null");
137140
this.port = port;
138141
this.sendStringParametersAsUnicode = sendStringParametersAsUnicode;
139142
this.ssl = ssl;
@@ -182,7 +185,7 @@ MssqlConnectionConfiguration withRedirect(Redirect redirect) {
182185

183186
return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectTimeout, this.database, redirectServerName, hostNameInCertificate, this.lockWaitTimeout,
184187
this.password,
185-
this.preferCursoredExecution, redirect.getPort(), this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer,
188+
this.preferCursoredExecution, this.preparedStatementCache, redirect.getPort(), this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer,
186189
this.sslTunnelSslContextBuilderCustomizer, this.tcpKeepAlive, this.tcpNoDelay, this.trustServerCertificate, this.trustStore, this.trustStoreType, this.trustStorePassword, this.username);
187190
}
188191

@@ -192,7 +195,7 @@ ClientConfiguration toClientConfiguration() {
192195
}
193196

194197
ConnectionOptions toConnectionOptions() {
195-
return new ConnectionOptions(this.preferCursoredExecution, new DefaultCodecs(), new IndefinitePreparedStatementCache(), this.sendStringParametersAsUnicode);
198+
return new ConnectionOptions(this.preferCursoredExecution, new DefaultCodecs(), this.preparedStatementCache, this.sendStringParametersAsUnicode);
196199
}
197200

198201
@Override
@@ -355,6 +358,8 @@ public static final class Builder {
355358

356359
private Predicate<String> preferCursoredExecution = sql -> false;
357360

361+
private PreparedStatementCache preparedStatementCache = new IndefinitePreparedStatementCache();
362+
358363
private CharSequence password;
359364

360365
private int port = DEFAULT_PORT;
@@ -551,6 +556,17 @@ public Builder preferCursoredExecution(Predicate<String> preference) {
551556
return this;
552557
}
553558

559+
/**
560+
* Configures the {@link PreparedStatementCache}. By default, uses {@link IndefinitePreparedStatementCache}.
561+
*
562+
* @param cache the cache implementation to use (must not be null).
563+
* @return this {@link Builder}
564+
*/
565+
public Builder preparedStatementCache(PreparedStatementCache cache) {
566+
this.preparedStatementCache = Assert.requireNonNull(cache, "Prepared statement cache must not be null");
567+
return this;
568+
}
569+
554570
/**
555571
* Configure the port. Defaults to {@code 5432}.
556572
*
@@ -714,7 +730,7 @@ public MssqlConnectionConfiguration build() {
714730

715731
return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectTimeout, this.database, this.host, this.hostNameInCertificate, this.lockWaitTimeout,
716732
this.password,
717-
this.preferCursoredExecution, this.port, this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer,
733+
this.preferCursoredExecution, this.preparedStatementCache, this.port, this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer,
718734
this.sslTunnelSslContextBuilderCustomizer, this.tcpKeepAlive,
719735
this.tcpNoDelay, this.trustServerCertificate, this.trustStore,
720736
this.trustStoreType, this.trustStorePassword, this.username);

src/main/java/io/r2dbc/mssql/MssqlConnectionFactoryProvider.java

+11
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ public final class MssqlConnectionFactoryProvider implements ConnectionFactoryPr
7171
*/
7272
public static final Option<Object> PREFER_CURSORED_EXECUTION = Option.valueOf("preferCursoredExecution");
7373

74+
/**
75+
* Configures the prepared statement cache to use.
76+
* The value can be an {@link Integer}, a {@link PreparedStatementCache} or a {@link Class class name}.
77+
* <p>
78+
* A value of 0 disables the cache ({@link NoPreparedStatementCache}).<br/>
79+
* A value of -1 (or any negative number) caches items indefinitely ({@link IndefinitePreparedStatementCache}) - this is the default value.<br/>
80+
* Any other integer creates an LRU cache of that size ({@link LRUPreparedStatementCache}).<br/>
81+
*/
82+
public static final Option<Object> PREPARED_STATEMENT_CACHE = Option.valueOf("preparedStatementCache");
83+
7484
/**
7585
* Configure whether to send character data as unicode (NVARCHAR, NCHAR, NTEXT) or whether to use the database encoding. Enabled by default.
7686
* If disabled, {@link CharSequence} data is sent using the database-specific collation such as ASCII/MBCS instead of Unicode.
@@ -162,6 +172,7 @@ public MssqlConnectionFactory create(ConnectionFactoryOptions connectionFactoryO
162172
mapper.from(LOCK_WAIT_TIMEOUT).map(OptionMapper::toDuration).to(builder::lockWaitTimeout);
163173
mapper.from(PORT).map(OptionMapper::toInteger).to(builder::port);
164174
mapper.from(PREFER_CURSORED_EXECUTION).map(OptionMapper::toStringPredicate).to(builder::preferCursoredExecution);
175+
mapper.from(PREPARED_STATEMENT_CACHE).map(OptionMapper::toPreparedStatementCache).to(builder::preparedStatementCache);
165176
mapper.from(SEND_STRING_PARAMETERS_AS_UNICODE).map(OptionMapper::toBoolean).to(builder::sendStringParametersAsUnicode);
166177
mapper.from(SSL).to(builder::enableSsl);
167178
mapper.fromTyped(SSL_CONTEXT_BUILDER_CUSTOMIZER).to(builder::sslContextBuilderCustomizer);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2018-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.r2dbc.mssql;
18+
19+
import java.util.function.Function;
20+
21+
/**
22+
* {@link PreparedStatementCache} implementation that does not cache anything.
23+
*
24+
* @author Suraj Vijayakumar
25+
*/
26+
public class NoPreparedStatementCache implements PreparedStatementCache {
27+
28+
@Override
29+
public int getHandle(String sql, Binding binding) {
30+
return PreparedStatementCache.UNPREPARED;
31+
}
32+
33+
@Override
34+
public void putHandle(int handle, String sql, Binding binding) {
35+
}
36+
37+
@Override
38+
public <T> T getParsedSql(String sql, Function<String, T> parseFunction) {
39+
return parseFunction.apply(sql);
40+
}
41+
42+
@Override
43+
public int size() {
44+
return 0;
45+
}
46+
}

src/main/java/io/r2dbc/mssql/OptionMapper.java

+48-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ public static OptionMapper create(ConnectionFactoryOptions options) {
5656
* Construct a new {@link Source} for a {@link Option}. Options without a value are not bound or mapped in the later stages of {@link Source}.
5757
*
5858
* @param option the option to apply.
59-
* @param <T> inferred option type.
6059
* @return the source object.
6160
*/
6261
public Source<Object> from(Option<?> option) {
@@ -192,6 +191,25 @@ static Predicate<String> toStringPredicate(Object value) {
192191
throw new IllegalArgumentException(String.format("Cannot convert value %s to Predicate", value));
193192
}
194193

194+
/**
195+
* Parse an {@link Option} to a {@link PreparedStatementCache}.
196+
*/
197+
static PreparedStatementCache toPreparedStatementCache(Object value) {
198+
if (value instanceof PreparedStatementCache) {
199+
return (PreparedStatementCache) value;
200+
}
201+
202+
if (value instanceof Integer) {
203+
return toPreparedStatementCache((Integer) value);
204+
}
205+
206+
if (value instanceof String) {
207+
return toPreparedStatementCache((String) value);
208+
}
209+
210+
throw new IllegalArgumentException(String.format("Cannot convert value %s to PreparedStatementCache", value));
211+
}
212+
195213
/**
196214
* Parse an {@link Option} to {@link UUID}.
197215
*/
@@ -208,6 +226,35 @@ static UUID toUuid(Object value) {
208226
throw new IllegalArgumentException(String.format("Cannot convert value %s to UUID", value));
209227
}
210228

229+
private static PreparedStatementCache toPreparedStatementCache(Integer value) {
230+
if (value < 0) {
231+
return new IndefinitePreparedStatementCache();
232+
} else if (value == 0) {
233+
return new NoPreparedStatementCache();
234+
} else {
235+
return new LRUPreparedStatementCache(value);
236+
}
237+
}
238+
239+
private static PreparedStatementCache toPreparedStatementCache(String value) {
240+
try {
241+
Integer number = Integer.parseInt(value);
242+
return toPreparedStatementCache(number);
243+
} catch (NumberFormatException ignore) {
244+
// ignore - value is not a number
245+
}
246+
247+
try {
248+
Object cache = Class.forName(value).getDeclaredConstructor().newInstance();
249+
if (cache instanceof PreparedStatementCache) {
250+
return (PreparedStatementCache) cache;
251+
}
252+
throw new IllegalArgumentException("Value '" + value + "' must be an instance of PreparedStatementCache");
253+
} catch (ReflectiveOperationException e) {
254+
throw new IllegalArgumentException("Cannot instantiate '" + value + "'", e);
255+
}
256+
}
257+
211258
public interface Source<T> {
212259

213260
/**

src/main/java/io/r2dbc/mssql/PreparedStatementCache.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020

2121
/**
2222
* Cache for prepared statements.
23+
* <p>
24+
* Implementations will need a default no-arg constructor for the {@link OptionMapper}
25+
* to instantiate them from a discovery option.
2326
*
2427
* @author Mark Paluch
2528
*/
26-
interface PreparedStatementCache {
29+
public interface PreparedStatementCache {
2730

2831
/**
2932
* Marker for no prepared statement found/no prepared statement.

0 commit comments

Comments
 (0)