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

jhannes / fluent-jdbc / #204

07 Nov 2025 01:36PM UTC coverage: 91.59% (-0.06%) from 91.654%
#204

push

jhannes-test
revert autoCommit after top level transaction finishes

7 of 9 new or added lines in 1 file covered. (77.78%)

1198 of 1308 relevant lines covered (91.59%)

0.92 hits per line

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

94.74
/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

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

50
    public DbContext(DatabaseStatementFactory factory) {
1✔
51
        this.factory = factory;
1✔
52
    }
1✔
53

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

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

73
    public DatabaseStatementFactory getStatementFactory() {
74
        return factory;
1✔
75
    }
76

77
    /**
78
     * A {@link java.util.function.Supplier} for {@link Connection} objects. Like {@link java.util.function.Supplier},
79
     * but can throw {@link SQLException}. Used as an alternative to a {@link DataSource}
80
     */
81
    @FunctionalInterface
82
    public interface ConnectionSupplier {
83
        Connection getConnection() throws SQLException;
84
    }
85

86
    private final ThreadLocal<TopLevelDbContextConnection> currentConnection = new ThreadLocal<>();
1✔
87
    private final ThreadLocal<HashMap<String, HashMap<Object, SingleRow<?>>>> currentCache = new ThreadLocal<>();
1✔
88
    private final ThreadLocal<DbTransaction> currentTransaction = new ThreadLocal<>();
1✔
89

90
    /**
91
     * Creates a {@link DbContextTable} associated with this DbContext. All operations will be executed
92
     * with the connection from this {@link DbContext}
93
     */
94
    @CheckReturnValue
95
    public DbContextTable table(@Nonnull String tableName) {
96
        return table(new DatabaseTableImpl(tableName, factory));
1✔
97
    }
98

99
    /**
100
     * Creates a {@link #table(String)} which automatically updates <code>created_at</code> and
101
     * <code>updated_at</code> columns in the underlying database.
102
     *
103
     * @see DatabaseTableWithTimestamps
104
     */
105
    @CheckReturnValue
106
    public DbContextTable tableWithTimestamps(String tableName) {
107
        return table(new DatabaseTableWithTimestamps(tableName, factory));
1✔
108
    }
109

110
    /**
111
     * Associate a custom implementation of {@link DatabaseTable} with this {@link DbContext}
112
     */
113
    @CheckReturnValue
114
    public DbContextTable table(DatabaseTable table) {
115
        return new DbContextTable(table, this);
1✔
116
    }
117

118
    /**
119
     * Binds a database connection to this {@link DbContext} for the current thread. All database
120
     * operations on {@link DbContextTable} objects created from this {@link DbContext} in the current
121
     * thread will be executed using this connection. Calls to startConnection can be nested.
122
     *
123
     * <p>Example:</p>
124
     * <pre>
125
     * try (DbContextConnection ignored = context.startConnection(dataSource)) {
126
     *     return table.where("id", id).orderBy("data").listLongs("data");
127
     * }
128
     * </pre>
129
     */
130
    @CheckReturnValue
131
    public DbContextConnection startConnection(DataSource dataSource) {
132
        return startConnection(dataSource::getConnection);
1✔
133
    }
134

135
    /**
136
     * Gets a connection from {@link ConnectionSupplier} and assigns it to the current thread.
137
     * Associates the cache with the current thread as well
138
     *
139
     * @see #startConnection(DataSource)
140
     */
141
    @CheckReturnValue
142
    public DbContextConnection startConnection(ConnectionSupplier connectionSupplier) {
143
        if (currentConnection.get() != null) {
1✔
144
            return () -> { };
1✔
145
        }
146
        currentConnection.set(new TopLevelDbContextConnection(connectionSupplier, this));
1✔
147
        currentCache.set(new HashMap<>());
1✔
148
        return currentConnection.get();
1✔
149
    }
150

151
    /**
152
     * Returns the connection associated with the current thread or throws exception if
153
     * {@link #startConnection(DataSource)} has not been called yet
154
     */
155
    @CheckReturnValue
156
    public Connection getThreadConnection() {
157
        if (currentConnection.get() == null) {
1✔
158
            throw new IllegalStateException("Call startConnection first");
1✔
159
        }
160
        return currentConnection.get().getConnection();
1✔
161
    }
162

163
    void removeFromThread() {
164
        currentCache.get().clear();
1✔
165
        currentCache.remove();
1✔
166
        currentConnection.remove();
1✔
167
    }
1✔
168

169
    /**
170
     * Retrieves the underlying or cached value of the retriever argument. This cache is per
171
     * {@link DbContextConnection} and is evicted when the connection is closed
172
     */
173
    @CheckReturnValue
174
    public <ENTITY, KEY> SingleRow<ENTITY> cache(String tableName, KEY key, RetrieveMethod<KEY, ENTITY> retriever) {
175
        if (!currentCache.get().containsKey(tableName)) {
1✔
176
            currentCache.get().put(tableName, new HashMap<>());
1✔
177
        }
178
        if (!currentCache.get().get(tableName).containsKey(key)) {
1✔
179
            SingleRow<ENTITY> value = retriever.retrieve(key);
1✔
180
            currentCache.get().get(tableName).put(key, value);
1✔
181
        }
182
        //noinspection unchecked
183
        return (SingleRow<ENTITY>) currentCache.get().get(tableName).get(key);
1✔
184
    }
185

186
    /**
187
     * Turns off auto-commit for the current thread until the {@link DbTransaction} is closed. Returns
188
     * a {@link DbTransaction} object which can be used to control commit and rollback. Can be nested
189
     */
190
    @CheckReturnValue
191
    public DbTransaction ensureTransaction() {
192
        if (getCurrentTransaction() != null) {
1✔
193
            logger.debug("Starting nested transaction");
1✔
194
            return new NestedTransactionContext(getCurrentTransaction());
1✔
195
        }
196
        logger.debug("Starting new transaction");
1✔
197
        currentTransaction.set(new TopLevelTransaction(getThreadConnection()));
1✔
198
        return getCurrentTransaction();
1✔
199
    }
200

201
    public DbTransaction getCurrentTransaction() {
202
        return currentTransaction.get();
1✔
203
    }
204

205
    private static class NestedTransactionContext implements DbTransaction {
206
        private boolean complete = false;
1✔
207
        private final DbTransaction outerTransaction;
208

209
        public NestedTransactionContext(DbTransaction outerTransaction) {
1✔
210
            this.outerTransaction = outerTransaction;
1✔
211
        }
1✔
212

213
        @Override
214
        public void close() {
215
            if (!complete) {
1✔
216
                outerTransaction.setRollback();
1✔
217
                logger.debug("Nested transaction rollback");
1✔
218
            } else {
219
                logger.debug("Nested transaction commit");
1✔
220
            }
221
        }
1✔
222

223
        @Override
224
        public void setRollback() {
225
            complete = false;
1✔
226
        }
1✔
227

228
        @Override
229
        public void setComplete() {
230
            complete = true;
1✔
231
        }
1✔
232
    }
233

234
    private class TopLevelTransaction implements DbTransaction {
235
        private final boolean autoCommit;
236
        boolean complete = false;
1✔
237
        boolean rollback = false;
1✔
238

239
        private TopLevelTransaction(Connection connection) {
1✔
240
            try {
241
                this.autoCommit = connection.getAutoCommit();
1✔
242
                getThreadConnection().setAutoCommit(false);
1✔
NEW
243
            } catch (SQLException e) {
×
NEW
244
                throw ExceptionUtil.softenCheckedException(e);
×
245
            }
1✔
246
        }
1✔
247

248
        @Override
249
        public void setComplete() {
250
            complete = true;
1✔
251
        }
1✔
252

253
        @Override
254
        public void setRollback() {
255
            rollback = true;
1✔
256
        }
1✔
257

258
        @Override
259
        public void close() {
260
            currentTransaction.remove();
1✔
261
            try {
262
                if (!complete || rollback) {
1✔
263
                    logger.debug("Rollback");
1✔
264
                    getThreadConnection().rollback();
1✔
265
                } else {
266
                    logger.debug("Commit");
1✔
267
                    getThreadConnection().commit();
1✔
268
                }
269
                getThreadConnection().setAutoCommit(autoCommit);
1✔
270
            } catch (SQLException e) {
1✔
271
                throw ExceptionUtil.softenCheckedException(e);
×
272
            }
1✔
273
        }
1✔
274
    }
275

276
    static class TopLevelDbContextConnection implements DbContextConnection {
277

278
        private final ConnectionSupplier connectionSupplier;
279
        private Connection connection;
280
        private final DbContext context;
281

282
        TopLevelDbContextConnection(ConnectionSupplier connectionSupplier, DbContext context) {
1✔
283
            this.connectionSupplier = connectionSupplier;
1✔
284
            this.context = context;
1✔
285
        }
1✔
286

287
        @Override
288
        public void close() {
289
            if (connection != null) {
1✔
290
                try {
291
                    connection.close();
1✔
292
                } catch (SQLException e) {
1✔
293
                    throw ExceptionUtil.softenCheckedException(e);
×
294
                }
1✔
295
            }
296
            context.removeFromThread();
1✔
297
        }
1✔
298

299
        Connection getConnection() {
300
            if (connection == null) {
1✔
301
                try {
302
                    connection = connectionSupplier.getConnection();
1✔
303
                } catch (SQLException e) {
1✔
304
                    throw ExceptionUtil.softenCheckedException(e);
×
305
                }
1✔
306
            }
307
            return connection;
1✔
308
        }
309

310
    }
311

312
    /**
313
     * Functional interface used to populate the query. Called on when a retrieved value is not in
314
     * the cache. Like {@link java.util.function.Function}, but returns {@link SingleRow}
315
     */
316
    @FunctionalInterface
317
    public interface RetrieveMethod<KEY, ENTITY> {
318
        SingleRow<ENTITY> retrieve(KEY key);
319
    }
320
}
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

© 2026 Coveralls, Inc