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

jhannes / fluent-jdbc / #213

16 Jul 2026 05:43PM UTC coverage: 91.667% (+0.1%) from 91.572%
#213

push

jhannes
upgrade to JDK 17 in GitHub workflow to support latest junit

1221 of 1332 relevant lines covered (91.67%)

0.92 hits per line

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

95.0
/src/main/java/org/fluentjdbc/DbContext.java
1
package org.fluentjdbc;
2

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

7
import javax.annotation.CheckReturnValue;
8
import javax.annotation.Nonnull;
9
import javax.sql.DataSource;
10
import java.sql.Connection;
11
import java.sql.SQLException;
12
import java.util.Collection;
13
import java.util.HashMap;
14

15
/**
16
 * <p>Provides a starting point for for context oriented database operation. Create one DbContext for your
17
 * application and use {@link #table(String)} to create {@link DbContextTable} object for each table
18
 * you manipulate. All database operations must be nested inside a call to {@link #startConnection(DataSource)}.</p>
19
 *
20
 * <p>Example</p>
21
 * <pre>
22
 * DbContext context = new DbContext();
23
 *
24
 * {@link DbContextTable} table = context.table("database_test_table");
25
 * DataSource dataSource = createDataSource();
26
 *
27
 * try (DbContextConnection ignored = context.startConnection(dataSource)) {
28
 *     Object id = table.insert()
29
 *         .setPrimaryKey("id", null)
30
 *         .setField("code", 1002)
31
 *         .setField("name", "insertTest")
32
 *         .execute();
33
 *
34
 *     assertThat(table.where("name", "insertTest").orderBy("code").listLongs("code"))
35
 *         .contains(1002L);
36
 * }
37
 * </pre>
38
 *
39
 */
40
public class DbContext {
41
    
42
    private static final Logger logger = LoggerFactory.getLogger(DbContext.class);
1✔
43

44
    private final DatabaseStatementFactory factory;
45
    private final DatabaseTransactionReporter transactionReporter;
46

47
    public DbContext() {
48
        this(new DatabaseStatementFactory(DatabaseReporter.LOGGING_REPORTER), DatabaseTransactionReporter.LOGGING_REPORTER);
1✔
49
    }
1✔
50

51
    public DbContext(DatabaseStatementFactory factory, DatabaseTransactionReporter transactionReporter) {
1✔
52
        this.factory = factory;
1✔
53
        this.transactionReporter = transactionReporter;
1✔
54
    }
1✔
55

56
    /**
57
     * Build an arbitrary select statement, e.g.
58
     * <code>dbContext.select("max(age) as max_age").from("persons").where("name", "Johannes").singleLong("max_age")</code>
59
     */
60
    @CheckReturnValue
61
    public DbContextSelectBuilder select(String... columns) {
62
        return new DbContextSelectBuilder(this).select(columns);
1✔
63
    }
64

65
    /**
66
     * Execute an arbitrary SQL statement, e.g.
67
     * <code>dbContext.statement("select 1 as number from dual").singleObject("num")</code> or
68
     * <code>dbContext.statement("update persons set full_name = first_name || ' ' || last_name").executeUpdate()</code>
69
     */
70
    @CheckReturnValue
71
    public DbContextStatement statement(String statement, Collection<Object> parameters) {
72
        return new DbContextStatement(this, statement, parameters);
1✔
73
    }
74

75
    /**
76
     * Execute an arbitrary SQL statement, e.g.
77
     * <code>dbContext.statement("select max(code) as max_code from database_table_test_table").singleInt("max_code")</code> or
78
     * <code>dbContext.statement("update persons set full_name = first_name || ' ' || last_name").executeUpdate()</code>
79
     */
80
    @CheckReturnValue
81
    public DbContextStatement statement(String statement) {
82
        return new DbContextStatement(this, statement);
1✔
83
    }
84

85
    public DatabaseStatementFactory getStatementFactory() {
86
        return factory;
1✔
87
    }
88

89
    /**
90
     * A {@link java.util.function.Supplier} for {@link Connection} objects. Like {@link java.util.function.Supplier},
91
     * but can throw {@link SQLException}. Used as an alternative to a {@link DataSource}
92
     */
93
    @FunctionalInterface
94
    public interface ConnectionSupplier {
95
        Connection getConnection() throws SQLException;
96
    }
97

98
    private final ThreadLocal<TopLevelDbContextConnection> currentConnection = new ThreadLocal<>();
1✔
99
    private final ThreadLocal<HashMap<String, HashMap<Object, SingleRow<?>>>> currentCache = new ThreadLocal<>();
1✔
100
    private final ThreadLocal<DbTransaction> currentTransaction = new ThreadLocal<>();
1✔
101

102
    /**
103
     * Creates a {@link DbContextTable} associated with this DbContext. All operations will be executed
104
     * with the connection from this {@link DbContext}
105
     */
106
    @CheckReturnValue
107
    public DbContextTable table(@Nonnull String tableName) {
108
        return table(new DatabaseTableImpl(tableName, factory));
1✔
109
    }
110

111
    /**
112
     * Creates a {@link #table(String)} which automatically updates <code>created_at</code> and
113
     * <code>updated_at</code> columns in the underlying database.
114
     *
115
     * @see DatabaseTableWithTimestamps
116
     */
117
    @CheckReturnValue
118
    public DbContextTable tableWithTimestamps(String tableName) {
119
        return table(new DatabaseTableWithTimestamps(tableName, factory));
1✔
120
    }
121

122
    /**
123
     * Associate a custom implementation of {@link DatabaseTable} with this {@link DbContext}
124
     */
125
    @CheckReturnValue
126
    public DbContextTable table(DatabaseTable table) {
127
        return new DbContextTable(table, this);
1✔
128
    }
129

130
    /**
131
     * Binds a database connection to this {@link DbContext} for the current thread. All database
132
     * operations on {@link DbContextTable} objects created from this {@link DbContext} in the current
133
     * thread will be executed using this connection. Calls to startConnection can be nested.
134
     *
135
     * <p>Example:</p>
136
     * <pre>
137
     * try (DbContextConnection ignored = context.startConnection(dataSource)) {
138
     *     return table.where("id", id).orderBy("data").listLongs("data");
139
     * }
140
     * </pre>
141
     */
142
    @CheckReturnValue
143
    public DbContextConnection startConnection(DataSource dataSource) {
144
        return startConnection(dataSource::getConnection);
1✔
145
    }
146

147
    /**
148
     * Gets a connection from {@link ConnectionSupplier} and assigns it to the current thread.
149
     * Associates the cache with the current thread as well
150
     *
151
     * @see #startConnection(DataSource)
152
     */
153
    @CheckReturnValue
154
    public DbContextConnection startConnection(ConnectionSupplier connectionSupplier) {
155
        if (currentConnection.get() != null) {
1✔
156
            return () -> { };
1✔
157
        }
158
        currentConnection.set(new TopLevelDbContextConnection(connectionSupplier, this));
1✔
159
        currentCache.set(new HashMap<>());
1✔
160
        return currentConnection.get();
1✔
161
    }
162

163
    /**
164
     * Returns the connection associated with the current thread or throws exception if
165
     * {@link #startConnection(DataSource)} has not been called yet
166
     */
167
    @CheckReturnValue
168
    public Connection getThreadConnection() {
169
        if (currentConnection.get() == null) {
1✔
170
            throw new IllegalStateException("Call startConnection first");
1✔
171
        }
172
        return currentConnection.get().getConnection();
1✔
173
    }
174

175
    void removeFromThread() {
176
        currentCache.get().clear();
1✔
177
        currentCache.remove();
1✔
178
        currentConnection.remove();
1✔
179
    }
1✔
180

181
    /**
182
     * Retrieves the underlying or cached value of the retriever argument. This cache is per
183
     * {@link DbContextConnection} and is evicted when the connection is closed
184
     */
185
    @CheckReturnValue
186
    public <ENTITY, KEY> SingleRow<ENTITY> cache(String tableName, KEY key, RetrieveMethod<KEY, ENTITY> retriever) {
187
        if (!currentCache.get().containsKey(tableName)) {
1✔
188
            currentCache.get().put(tableName, new HashMap<>());
1✔
189
        }
190
        if (!currentCache.get().get(tableName).containsKey(key)) {
1✔
191
            SingleRow<ENTITY> value = retriever.retrieve(key);
1✔
192
            currentCache.get().get(tableName).put(key, value);
1✔
193
        }
194
        //noinspection unchecked
195
        return (SingleRow<ENTITY>) currentCache.get().get(tableName).get(key);
1✔
196
    }
197

198
    /**
199
     * Turns off auto-commit for the current thread until the {@link DbTransaction} is closed. Returns
200
     * a {@link DbTransaction} object which can be used to control commit and rollback. Can be nested
201
     */
202
    @CheckReturnValue
203
    public DbTransaction ensureTransaction() {
204
        if (getCurrentTransaction() != null) {
1✔
205
            logger.debug("Starting nested transaction");
1✔
206
            return new NestedTransactionContext(getCurrentTransaction());
1✔
207
        }
208
        logger.debug("Starting new transaction");
1✔
209
        currentTransaction.set(new TopLevelTransaction(getThreadConnection(), transactionReporter));
1✔
210
        return getCurrentTransaction();
1✔
211
    }
212

213
    public DbTransaction getCurrentTransaction() {
214
        return currentTransaction.get();
1✔
215
    }
216

217
    private static class NestedTransactionContext implements DbTransaction {
218
        private boolean complete = false;
1✔
219
        private final DbTransaction outerTransaction;
220

221
        public NestedTransactionContext(DbTransaction outerTransaction) {
1✔
222
            this.outerTransaction = outerTransaction;
1✔
223
        }
1✔
224

225
        @Override
226
        public void close() {
227
            if (!complete) {
1✔
228
                outerTransaction.setRollback();
1✔
229
                logger.debug("Nested transaction rollback");
1✔
230
            } else {
231
                logger.debug("Nested transaction commit");
1✔
232
            }
233
        }
1✔
234

235
        @Override
236
        public void setRollback() {
237
            complete = false;
1✔
238
        }
1✔
239

240
        @Override
241
        public void setComplete() {
242
            complete = true;
1✔
243
        }
1✔
244
    }
245

246
    private class TopLevelTransaction implements DbTransaction {
247
        private final boolean autoCommit;
248
        boolean complete = false;
1✔
249
        boolean rollback = false;
1✔
250
        private final DatabaseTransactionReporter transactionLog;
251

252
        private TopLevelTransaction(Connection connection, DatabaseTransactionReporter transactionReporter) {
1✔
253
            this.transactionLog = transactionReporter;
1✔
254
            try {
255
                this.autoCommit = connection.getAutoCommit();
1✔
256
                getThreadConnection().setAutoCommit(false);
1✔
257
            } catch (SQLException e) {
×
258
                throw ExceptionUtil.softenCheckedException(e);
×
259
            }
1✔
260
        }
1✔
261

262
        @Override
263
        public void setComplete() {
264
            complete = true;
1✔
265
        }
1✔
266

267
        @Override
268
        public void setRollback() {
269
            rollback = true;
1✔
270
        }
1✔
271

272
        @Override
273
        public void close() {
274
            long start = System.currentTimeMillis();
1✔
275
            currentTransaction.remove();
1✔
276
            try {
277
                if (!complete || rollback) {
1✔
278
                    getThreadConnection().rollback();
1✔
279
                    getThreadConnection().setAutoCommit(autoCommit);
1✔
280
                    transactionLog.logRollback(System.currentTimeMillis() - start);
1✔
281
                } else {
282
                    getThreadConnection().commit();
1✔
283
                    getThreadConnection().setAutoCommit(autoCommit);
1✔
284
                    transactionLog.logCommit(System.currentTimeMillis() - start);
1✔
285
                }
286
            } catch (SQLException e) {
1✔
287
                throw ExceptionUtil.softenCheckedException(e);
×
288
            }
1✔
289
        }
1✔
290
    }
291

292
    static class TopLevelDbContextConnection implements DbContextConnection {
293

294
        private final ConnectionSupplier connectionSupplier;
295
        private Connection connection;
296
        private final DbContext context;
297

298
        TopLevelDbContextConnection(ConnectionSupplier connectionSupplier, DbContext context) {
1✔
299
            this.connectionSupplier = connectionSupplier;
1✔
300
            this.context = context;
1✔
301
        }
1✔
302

303
        @Override
304
        public void close() {
305
            if (connection != null) {
1✔
306
                try {
307
                    connection.close();
1✔
308
                } catch (SQLException e) {
1✔
309
                    throw ExceptionUtil.softenCheckedException(e);
×
310
                }
1✔
311
            }
312
            context.removeFromThread();
1✔
313
        }
1✔
314

315
        Connection getConnection() {
316
            if (connection == null) {
1✔
317
                try {
318
                    connection = connectionSupplier.getConnection();
1✔
319
                } catch (SQLException e) {
1✔
320
                    throw ExceptionUtil.softenCheckedException(e);
×
321
                }
1✔
322
            }
323
            return connection;
1✔
324
        }
325

326
    }
327

328
    /**
329
     * Functional interface used to populate the query. Called on when a retrieved value is not in
330
     * the cache. Like {@link java.util.function.Function}, but returns {@link SingleRow}
331
     */
332
    @FunctionalInterface
333
    public interface RetrieveMethod<KEY, ENTITY> {
334
        SingleRow<ENTITY> retrieve(KEY key);
335
    }
336
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc