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

jhannes / fluent-jdbc / #172

01 Aug 2024 07:53PM UTC coverage: 94.146% (-1.3%) from 95.478%
#172

push

jhannes
DatabaseUpdatable.setField with expression allows to insert and update rows with calculated column values

41 of 45 new or added lines in 7 files covered. (91.11%)

32 existing lines in 12 files now uncovered.

1174 of 1247 relevant lines covered (94.15%)

0.94 hits per line

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

95.7
/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.HashMap;
13
import java.util.List;
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 DbContextSqlBuilder select(String... columns) {
60
        return new DbContextSqlBuilder(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, List<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
        try {
198
            getThreadConnection().setAutoCommit(false);
1✔
199
        } catch (SQLException e) {
1✔
UNCOV
200
            throw ExceptionUtil.softenCheckedException(e);
×
201
        }
1✔
202
        currentTransaction.set(new TopLevelTransaction());
1✔
203
        return getCurrentTransaction();
1✔
204
    }
205

206
    public DbTransaction getCurrentTransaction() {
207
        return currentTransaction.get();
1✔
208
    }
209

210
    private static class NestedTransactionContext implements DbTransaction {
211
        private boolean complete = false;
1✔
212
        private final DbTransaction outerTransaction;
213

214
        public NestedTransactionContext(DbTransaction outerTransaction) {
1✔
215
            this.outerTransaction = outerTransaction;
1✔
216
        }
1✔
217

218
        @Override
219
        public void close() {
220
            if (!complete) {
1✔
221
                outerTransaction.setRollback();
1✔
222
                logger.debug("Nested transaction rollback");
1✔
223
            } else {
224
                logger.debug("Nested transaction commit");
1✔
225
            }
226
        }
1✔
227

228
        @Override
229
        public void setRollback() {
230
            complete = false;
1✔
231
        }
1✔
232

233
        @Override
234
        public void setComplete() {
235
            complete = true;
1✔
236
        }
1✔
237
    }
238

239
    private class TopLevelTransaction implements DbTransaction {
1✔
240
        boolean complete = false;
1✔
241
        boolean rollback = false;
1✔
242

243
        @Override
244
        public void setComplete() {
245
            complete = true;
1✔
246
        }
1✔
247

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

253
        @Override
254
        public void close() {
255
            currentTransaction.remove();
1✔
256
            try {
257
                if (!complete || rollback) {
1✔
258
                    logger.debug("Rollback");
1✔
259
                    getThreadConnection().rollback();
1✔
260
                } else {
261
                    logger.debug("Commit");
1✔
262
                    getThreadConnection().commit();
1✔
263
                }
264
                getThreadConnection().setAutoCommit(false);
1✔
265
            } catch (SQLException e) {
1✔
UNCOV
266
                throw ExceptionUtil.softenCheckedException(e);
×
267
            }
1✔
268
        }
1✔
269
    }
270

271
    static class TopLevelDbContextConnection implements DbContextConnection {
272

273
        private final ConnectionSupplier connectionSupplier;
274
        private Connection connection;
275
        private final DbContext context;
276

277
        TopLevelDbContextConnection(ConnectionSupplier connectionSupplier, DbContext context) {
1✔
278
            this.connectionSupplier = connectionSupplier;
1✔
279
            this.context = context;
1✔
280
        }
1✔
281

282
        @Override
283
        public void close() {
284
            if (connection != null) {
1✔
285
                try {
286
                    connection.close();
1✔
287
                } catch (SQLException e) {
1✔
UNCOV
288
                    throw ExceptionUtil.softenCheckedException(e);
×
289
                }
1✔
290
            }
291
            context.removeFromThread();
1✔
292
        }
1✔
293

294
        Connection getConnection() {
295
            if (connection == null) {
1✔
296
                try {
297
                    connection = connectionSupplier.getConnection();
1✔
298
                } catch (SQLException e) {
1✔
UNCOV
299
                    throw ExceptionUtil.softenCheckedException(e);
×
300
                }
1✔
301
            }
302
            return connection;
1✔
303
        }
304

305
    }
306

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