• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jhannes / fluent-jdbc / #188

06 Mar 2025 01:14PM UTC coverage: 92.445% (-0.1%) from 92.567%
#188

push

jhannes
Add table name to MDC on exceptions

5 of 6 new or added lines in 2 files covered. (83.33%)

9 existing lines in 1 file now uncovered.

1187 of 1284 relevant lines covered (92.45%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.2
/src/main/java/org/fluentjdbc/DatabaseStatement.java
1
package org.fluentjdbc;
2

3
import org.fluentjdbc.util.ExceptionUtil;
4
import org.slf4j.Logger;
5
import org.slf4j.LoggerFactory;
6
import org.slf4j.MDC;
7

8
import javax.annotation.Nullable;
9
import javax.annotation.ParametersAreNonnullByDefault;
10
import java.io.InputStream;
11
import java.io.Reader;
12
import java.math.BigDecimal;
13
import java.sql.Connection;
14
import java.sql.Date;
15
import java.sql.PreparedStatement;
16
import java.sql.SQLException;
17
import java.sql.Timestamp;
18
import java.time.Instant;
19
import java.time.LocalDate;
20
import java.time.OffsetDateTime;
21
import java.time.ZonedDateTime;
22
import java.util.Collection;
23
import java.util.List;
24
import java.util.Objects;
25
import java.util.UUID;
26
import java.util.function.Function;
27
import java.util.stream.Collectors;
28
import java.util.stream.Stream;
29

30
/**
31
 * Allows the execution of arbitrary SQL statements with parameters and returns the ResultSet.
32
 * Will convert parameters to the statement using {@link #bindParameter(PreparedStatement, int, Object)},
33
 * which supports many more data types than JDBC supports natively. Returns the result via
34
 * {@link #list(Connection, DatabaseResult.RowMapper)}, {@link #singleObject(Connection, DatabaseResult.RowMapper)}
35
 * and {@link #stream(Connection, DatabaseResult.RowMapper)}, which uses {@link DatabaseRow} to convert
36
 * ResultSet types.
37
 */
38
@ParametersAreNonnullByDefault
39
public class DatabaseStatement {
40

41
    @FunctionalInterface
42
    public interface PreparedStatementFunction<T> {
43
        T apply(PreparedStatement stmt) throws SQLException;
44
    }
45

46

47
    protected static final Logger logger = LoggerFactory.getLogger(DatabaseStatement.class);
1✔
48
    private final String tableName;
49
    private final String statement;
50
    private final Collection<?> parameters;
51
    private final DatabaseTableOperationReporter reporter;
52

53
    public DatabaseStatement(String tableName, String statement, Collection<?> parameters, DatabaseTableOperationReporter reporter) {
1✔
54
        this.tableName = tableName;
1✔
55
        this.statement = statement;
1✔
56
        this.parameters = parameters;
1✔
57
        this.reporter = reporter;
1✔
58
    }
1✔
59

60
    /**
61
     * sets all parameters on the statement, calling {@link #bindParameter(PreparedStatement, int, Object)} to
62
     * convert each one
63
     */
64
    public static int bindParameters(PreparedStatement stmt, Collection<?> parameters) throws SQLException {
65
        return bindParameters(stmt, parameters, 1);
1✔
66
    }
67

68
    /**
69
     * sets all parameters on the statement, calling {@link #bindParameter(PreparedStatement, int, Object)} to
70
     * convert each one
71
     */
72
    public static int bindParameters(PreparedStatement stmt, Collection<?> parameters, int start) throws SQLException {
73
        int index = start;
1✔
74
        for (Object parameter : parameters) {
1✔
75
            bindParameter(stmt, index++, parameter);
1✔
76
        }
1✔
77
        return index;
1✔
78
    }
79

80
    /**
81
     * Calls the correct {@link PreparedStatement} <code>setXXX</code> method based on the type of the parameter.
82
     * Supports {@link Instant}, {@link ZonedDateTime}, {@link OffsetDateTime}, {@link LocalDate}, {@link String},
83
     * {@link List} of String or Integer, {@link Enum}, {@link UUID}, {@link Double}
84
     */
85
    public static void bindParameter(PreparedStatement stmt, int index, @Nullable Object parameter) throws SQLException {
86
        if (parameter instanceof Instant) {
1✔
87
            stmt.setTimestamp(index, (Timestamp) toDatabaseType(parameter, stmt.getConnection()));
1✔
88
        } else if (parameter instanceof ZonedDateTime) {
1✔
89
            stmt.setTimestamp(index, (Timestamp) toDatabaseType(parameter, stmt.getConnection()));
1✔
90
        } else if (parameter instanceof OffsetDateTime) {
1✔
91
            stmt.setTimestamp(index, (Timestamp) toDatabaseType(parameter, stmt.getConnection()));
1✔
92
        } else if (parameter instanceof LocalDate) {
1✔
93
            stmt.setDate(index, (Date) toDatabaseType(parameter, stmt.getConnection()));
1✔
94
        } else if (parameter instanceof CharSequence) {
1✔
95
            stmt.setString(index, (String) toDatabaseType(parameter, stmt.getConnection()));
1✔
96
        } else if (parameter instanceof Character) {
1✔
UNCOV
97
            stmt.setString(index, String.valueOf(parameter));
×
98
        } else if (parameter instanceof Enum<?>) {
1✔
99
            stmt.setString(index, (String) toDatabaseType(parameter, stmt.getConnection()));
1✔
100
        } else if (parameter instanceof Collection<?>) {
1✔
101
            //noinspection rawtypes
102
            Object[] elements = ((Collection) parameter).toArray();
1✔
103
            if (elements.length == 0) {
1✔
104
                stmt.setArray(index, stmt.getConnection().createArrayOf(null, elements));
1✔
105
            } else if (elements[0] instanceof Integer) {
1✔
106
                stmt.setArray(index, stmt.getConnection().createArrayOf("integer", elements));
1✔
107
            } else if (elements[0] instanceof String) {
1✔
108
                stmt.setArray(index, stmt.getConnection().createArrayOf("varchar", elements));
1✔
109
            } else {
UNCOV
110
                throw new IllegalArgumentException("Not supported: Arrays of " + elements[0].getClass());
×
111
            }
112
        } else if (parameter instanceof InputStream) {
1✔
113
            stmt.setBinaryStream(index, ((InputStream) parameter));
1✔
114
        } else if (parameter instanceof Reader) {
1✔
115
            stmt.setCharacterStream(index, ((Reader) parameter));
1✔
116
        } else {
117
            stmt.setObject(index, toDatabaseType(parameter, stmt.getConnection()));
1✔
118
        }
119
    }
1✔
120

121
    /**
122
     * Converts parameter to canonical database type.
123
     * Supports {@link Instant}, {@link ZonedDateTime}, {@link OffsetDateTime}, {@link LocalDate}, {@link String},
124
     * {@link Enum}, {@link UUID}, {@link Double}
125
     */
126
    public static Object toDatabaseType(@Nullable Object parameter, Connection connection) {
127
        if (parameter instanceof Instant) {
1✔
128
            return Timestamp.from((Instant) parameter);
1✔
129
        } else if (parameter instanceof ZonedDateTime) {
1✔
130
            return Timestamp.from(Instant.from((ZonedDateTime) parameter));
1✔
131
        } else if (parameter instanceof OffsetDateTime) {
1✔
132
            return Timestamp.from(Instant.from((OffsetDateTime) parameter));
1✔
133
        } else if (parameter instanceof LocalDate) {
1✔
134
            return Date.valueOf((LocalDate) parameter);
1✔
135
        } else if (parameter instanceof UUID) {
1✔
136
            if (isSqlServer(connection)) {
1✔
137
                return parameter.toString().toUpperCase();
×
138
            } else if (isOracle(connection)) {
1✔
UNCOV
139
                return parameter.toString();
×
140
            } else {
141
                return parameter;
1✔
142
            }
143
        } else if (parameter instanceof Double) {
1✔
144
            return BigDecimal.valueOf(((Number) parameter).doubleValue());
1✔
145
        } else if (parameter instanceof CharSequence) {
1✔
146
            return parameter.toString();
1✔
147
        } else if (parameter instanceof Enum<?>) {
1✔
148
            return parameter.toString();
1✔
149
        } else {
150
            return parameter;
1✔
151
        }
152
    }
153

154
    /**
155
     * Binds the parameters and calls {@link PreparedStatement#addBatch()}.
156
     *
157
     * @see #bindParameter(PreparedStatement, int, Object)
158
     */
159
    public static <T> void addBatch(PreparedStatement statement, Iterable<T> objects, Collection<Function<T, ?>> columnValueExtractors) throws SQLException {
160
        for (T object : objects) {
1✔
161
            int columnIndex = 1;
1✔
162
            for (Function<T, ?> f : columnValueExtractors) {
1✔
163
                bindParameter(statement, columnIndex++, f.apply(object));
1✔
164
            }
1✔
165
            statement.addBatch();
1✔
166
        }
1✔
167
    }
1✔
168

169
    /**
170
     * Returns true if the database connection is to SQL server
171
     */
172
    private static boolean isSqlServer(Connection connection) {
173
        return connection.getClass().getName().startsWith("net.sourceforge.jtds.jdbc") ||
1✔
174
               connection.getClass().getName().startsWith("com.microsoft.sqlserver.jdbc");
1✔
175
    }
176

177
    /**
178
     * Returns true if the database connection is to Oracle
179
     */
180
    private static boolean isOracle(Connection connection) {
181
        return connection.getClass().getName().startsWith("oracle.jdbc");
1✔
182
    }
183

184
    /**
185
     * Calls {@link Connection#prepareStatement(String)} with the statement,
186
     * {@link #bindParameters(PreparedStatement, Collection)}, converting each parameter in the process
187
     * and executes the statement
188
     */
189
    public int executeUpdate(Connection connection) {
190
        return execute(connection, PreparedStatement::executeUpdate);
1✔
191
    }
192

193
    /**
194
     * Create a string like <code>?, ?, ?</code> with the parameterCount number of '?'
195
     */
196
    public static String parameterString(int parameterCount) {
197
        StringBuilder parameterString = new StringBuilder("?");
1✔
198
        for (int i = 1; i < parameterCount; i++) {
1✔
199
            parameterString.append(", ?");
1✔
200
        }
201
        return parameterString.toString();
1✔
202
    }
203

204
    /**
205
     * Returns true if the object value equals the specified field name in the database. Converts
206
     * {@link #toDatabaseType(Object, Connection)} to decrease number of false positives
207
     */
208
    public static boolean dbValuesAreEqual(Object value, DatabaseRow row, String field, Connection connection) throws SQLException {
209
        Object canonicalValue = toDatabaseType(value, connection);
1✔
210
        Object dbValue;
211
        if (canonicalValue instanceof Timestamp) {
1✔
212
            dbValue = row.getTimestamp(field);
1✔
213
        } else if (canonicalValue instanceof Integer) {
1✔
214
            dbValue = row.getInt(field);
1✔
215
        } else {
216
            dbValue = row.getObject(field);
1✔
217
        }
218
        return Objects.equals(canonicalValue, toDatabaseType(dbValue, connection));
1✔
219
    }
220

221
    /**
222
     * If the query returns no rows, returns {@link SingleRow#absent}, if exactly one row is returned, maps it and return it,
223
     * if more than one is returned, throws `IllegalStateException`
224
     *
225
     * @param connection Database connection
226
     * @param mapper     Function object to map a single returned row to a object
227
     * @return the mapped row if one row is returned, {@link SingleRow#absent} otherwise
228
     * @throws MultipleRowsReturnedException if more than one row was matched the query
229
     */
230
    public <T> SingleRow<T> singleObject(Connection connection, DatabaseResult.RowMapper<T> mapper) {
231
        return query(connection, result -> result.single(
1✔
232
                mapper,
UNCOV
233
                () -> new NoRowsReturnedException(statement, parameters),
×
234
                () -> new MultipleRowsReturnedException(statement, parameters)
1✔
235
        ));
236
    }
237

238
    /**
239
     * Execute the query and map each return value over the {@link DatabaseResult.RowMapper} function to return a list. Example:
240
     * <pre>
241
     *     List&lt;Instant&gt; creationTimes = table.where("status", status).list(row -&gt; row.getInstant("created_at"))
242
     * </pre>
243
     */
244
    public <OBJECT> List<OBJECT> list(Connection connection, DatabaseResult.RowMapper<OBJECT> mapper) {
UNCOV
245
        return stream(connection, mapper).collect(Collectors.toList());
×
246
    }
247

248

249
    /**
250
     * Executes the <code>SELECT * FROM ...</code> statement and calls back to
251
     * {@link DatabaseResult.RowConsumer} for each returned row
252
     */
253
    public void forEach(Connection connection, DatabaseResult.RowConsumer consumer) {
254
        query(connection, result -> {
1✔
255
            result.forEach(consumer);
1✔
256
            return null;
1✔
257
        });
258
    }
1✔
259

260

261
    /**
262
     * Execute the query and map each return value over the {@link DatabaseResult.RowMapper} function to return a stream. Example:
263
     * <pre>
264
     *     table.where("status", status).stream(row -&gt; row.getInstant("created_at"))
265
     * </pre>
266
     */
267
    public <OBJECT> Stream<OBJECT> stream(Connection connection, DatabaseResult.RowMapper<OBJECT> mapper) {
268
        long startTime = System.currentTimeMillis();
1✔
269
        try {
270
            logger.trace(statement);
1✔
271
            PreparedStatement stmt = connection.prepareStatement(statement);
1✔
272
            bindParameters(stmt, parameters);
1✔
273
            DatabaseResult result = new DatabaseResult(stmt, stmt.executeQuery());
1✔
274
            return result.stream(mapper, statement);
1✔
275
        } catch (SQLException e) {
1✔
276
            MDC.put("fluentjdbc.tablename", tableName);
1✔
UNCOV
277
            throw ExceptionUtil.softenCheckedException(e);
×
278
        } finally {
279
            reporter.reportQuery(statement, System.currentTimeMillis() - startTime);
1✔
280
        }
281
    }
282

283
    /**
284
     * Calls {@link Connection#prepareStatement(String)} with the statement,
285
     * {@link #bindParameters(PreparedStatement, Collection)}, converting each parameter in the process
286
     * and executes the argument function with the statement
287
     */
288
    public <T> T execute(Connection connection, PreparedStatementFunction<T> f) {
289
        long startTime = System.currentTimeMillis();
1✔
290
        logger.trace(statement);
1✔
291
        try (PreparedStatement stmt = connection.prepareStatement(statement)) {
1✔
292
            bindParameters(stmt, parameters);
1✔
293
            return f.apply(stmt);
1✔
294
        } catch (SQLException e) {
1✔
295
            MDC.put("fluentjdbc.tablename", tableName);
1✔
UNCOV
296
            throw ExceptionUtil.softenCheckedException(e);
×
297
        } finally {
298
            reporter.reportQuery(statement, System.currentTimeMillis() - startTime);
1✔
299
        }
300
    }
301

302
    /**
303
     * Calls {@link Connection#prepareStatement(String, String[])} with the statement and columnNames,
304
     * {@link #bindParameters(PreparedStatement, Collection)}, converting each parameter in the process
305
     * and executes the argument function with the statement
306
     */
307
    public <T> T execute(Connection connection, PreparedStatementFunction<T> f, String[] columnNames) {
308
        long startTime = System.currentTimeMillis();
1✔
309
        logger.trace(statement);
1✔
310
        try (PreparedStatement stmt = connection.prepareStatement(statement, columnNames)) {
1✔
311
            bindParameters(stmt, parameters);
1✔
312
            return f.apply(stmt);
1✔
UNCOV
313
        } catch (SQLException e) {
×
NEW
314
            MDC.put("fluentjdbc.tablename", tableName);
×
UNCOV
315
            throw ExceptionUtil.softenCheckedException(e);
×
316
        } finally {
317
            reporter.reportQuery(statement, System.currentTimeMillis() - startTime);
1✔
318
        }
319
    }
320

321
    public <T> T query(Connection connection, DatabaseResult.DatabaseResultMapper<T> resultMapper) {
322
        return execute(connection, stmt -> {
1✔
323
            try (DatabaseResult result = new DatabaseResult(stmt, stmt.executeQuery())) {
1✔
324
                return resultMapper.apply(result);
1✔
325
            }
326
        });
327
    }
328

329
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc