Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
JdbcDatabaseClient.java
Go to the documentation of this file.
1package dev.rafex.ether.jdbc.client;
2
3/*-
4 * #%L
5 * ether-jdbc
6 * %%
7 * Copyright (C) 2025 - 2026 Raúl Eduardo González Argote
8 * %%
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 * #L%
27 */
28
29import java.sql.Connection;
30import java.sql.PreparedStatement;
31import java.sql.SQLException;
32import java.util.ArrayList;
33import java.util.List;
34import java.util.Objects;
35import java.util.Optional;
36
37import javax.sql.DataSource;
38
39import dev.rafex.ether.database.core.exceptions.DatabaseAccessException;
40import dev.rafex.ether.database.core.DatabaseClient;
41import dev.rafex.ether.database.core.mapping.ResultSetExtractor;
42import dev.rafex.ether.database.core.mapping.RowMapper;
43import dev.rafex.ether.database.core.sql.SqlParameter;
44import dev.rafex.ether.database.core.sql.SqlQuery;
45import dev.rafex.ether.database.core.sql.StatementBinder;
46import dev.rafex.ether.database.core.transaction.TransactionCallback;
47
48/**
49 * Implementation of {@link DatabaseClient} using JDBC.
50 *
51 * <p>This class delegates database operations to a {@link DataSource} and handles
52 * connection management, statement binding, and exception wrapping.</p>
53 */
54public final class JdbcDatabaseClient implements DatabaseClient {
55
56 private final DataSource dataSource;
57
58 /**
59 * Creates a new {@code JdbcDatabaseClient} with the specified data source.
60 *
61 * @param dataSource the data source to use for database connections
62 * @throws NullPointerException if {@code dataSource} is {@code null}
63 */
64 public JdbcDatabaseClient(final DataSource dataSource) {
65 this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
66 }
67
68 /**
69 * Executes a query and extracts the result using the provided extractor.
70 *
71 * @param query the SQL query to execute
72 * @param extractor the result set extractor
73 * @param <T> the type of the result
74 * @return the extracted result
75 * @throws DatabaseAccessException if the query fails
76 */
77 @Override
78 public <T> T query(final SqlQuery query, final ResultSetExtractor<T> extractor) {
79 try (Connection connection = dataSource.getConnection();
80 PreparedStatement statement = connection.prepareStatement(query.sql())) {
81 bind(statement, connection, query.parameters());
82 try (var resultSet = statement.executeQuery()) {
83 return extractor.extract(resultSet);
84 }
85 } catch (SQLException e) {
86 throw new DatabaseAccessException("Query failed", e);
87 }
88 }
89
90 /**
91 * Executes a query and maps each row to a list of objects.
92 *
93 * @param query the SQL query to execute
94 * @param mapper the row mapper
95 * @param <T> the type of the result objects
96 * @return a list of mapped objects
97 * @throws DatabaseAccessException if the query fails
98 */
99 @Override
100 public <T> List<T> queryList(final SqlQuery query, final RowMapper<T> mapper) {
101 return query(query, resultSet -> {
102 final List<T> result = new ArrayList<>();
103 while (resultSet.next()) {
104 result.add(mapper.map(resultSet));
105 }
106 return List.copyOf(result);
107 });
108 }
109
110 /**
111 * Executes a query and maps a single row to an object.
112 *
113 * @param query the SQL query to execute
114 * @param mapper the row mapper
115 * @param <T> the type of the result object
116 * @return an optional containing the mapped object, or empty if no row exists
117 * @throws DatabaseAccessException if the query fails or returns more than one row
118 */
119 @Override
120 public <T> Optional<T> queryOne(final SqlQuery query, final RowMapper<T> mapper) {
121 return query(query, resultSet -> {
122 if (!resultSet.next()) {
123 return Optional.empty();
124 }
125 final T result = mapper.map(resultSet);
126 if (resultSet.next()) {
127 throw new SQLException("Expected at most one row");
128 }
129 return Optional.of(result);
130 });
131 }
132
133 /**
134 * Executes a SQL command (UPDATE, INSERT, DELETE) and returns the number of affected rows.
135 *
136 * @param query the SQL command to execute
137 * @return the number of rows affected
138 * @throws DatabaseAccessException if the execution fails
139 */
140 @Override
141 public int execute(final SqlQuery query) {
142 try (Connection connection = dataSource.getConnection();
143 PreparedStatement statement = connection.prepareStatement(query.sql())) {
144 bind(statement, connection, query.parameters());
145 statement.execute();
146 return statement.getUpdateCount();
147 } catch (SQLException e) {
148 throw new DatabaseAccessException("Execute failed", e);
149 }
150 }
151
152 /**
153 * Executes a batch of SQL commands with the specified binders.
154 *
155 * @param sql the SQL command to execute
156 * @param binders the list of statement binders
157 * @return an array of update counts for each batch command
158 * @throws DatabaseAccessException if the batch execution fails
159 */
160 @Override
161 public long[] batch(final String sql, final List<StatementBinder> binders) {
162 try (Connection connection = dataSource.getConnection();
163 PreparedStatement statement = connection.prepareStatement(sql)) {
164 for (final StatementBinder binder : binders) {
165 binder.bind(connection, statement);
166 statement.addBatch();
167 }
168 return statement.executeLargeBatch();
169 } catch (SQLException e) {
170 throw new DatabaseAccessException("Batch execution failed", e);
171 }
172 }
173
174 /**
175 * Executes a transactional callback within a database transaction.
176 *
177 * @param callback the transaction callback
178 * @param <T> the type of the result
179 * @return the result of the transaction
180 * @throws DatabaseAccessException if the transaction fails
181 */
182 @Override
183 public <T> T inTransaction(final TransactionCallback<T> callback) {
184 try (Connection connection = dataSource.getConnection()) {
185 final boolean previousAutoCommit = connection.getAutoCommit();
186 connection.setAutoCommit(false);
187 try {
188 final T result = callback.execute(connection);
189 connection.commit();
190 connection.setAutoCommit(previousAutoCommit);
191 return result;
192 } catch (Exception e) {
193 connection.rollback();
194 connection.setAutoCommit(previousAutoCommit);
195 throw wrap("Transaction failed", e);
196 }
197 } catch (SQLException e) {
198 throw new DatabaseAccessException("Transaction setup failed", e);
199 }
200 }
201
202 private static void bind(final PreparedStatement statement, final Connection connection,
203 final List<SqlParameter> parameters) throws SQLException {
204 for (int i = 0; i < parameters.size(); i++) {
205 final SqlParameter parameter = parameters.get(i);
206 final int index = i + 1;
207 if (parameter.arrayElementType() != null) {
208 if (parameter.value() == null) {
209 statement.setNull(index, parameter.sqlType() == null ? java.sql.Types.ARRAY : parameter.sqlType());
210 } else {
211 statement.setArray(index,
212 connection.createArrayOf(parameter.arrayElementType(), (Object[]) parameter.value()));
213 }
214 continue;
215 }
216 if (parameter.value() == null) {
217 statement.setNull(index, parameter.sqlType() == null ? java.sql.Types.NULL : parameter.sqlType());
218 continue;
219 }
220 if (parameter.sqlType() != null) {
221 statement.setObject(index, parameter.value(), parameter.sqlType());
222 continue;
223 }
224 statement.setObject(index, parameter.value());
225 }
226 }
227
228 private static RuntimeException wrap(final String message, final Exception exception) {
229 return switch (exception) {
230 case RuntimeException re -> re;
231 case SQLException se -> new DatabaseAccessException(message, se);
232 default -> new DatabaseAccessException(message, exception);
233 };
234 }
235}
int execute(final SqlQuery query)
Executes a SQL command (UPDATE, INSERT, DELETE) and returns the number of affected rows.
JdbcDatabaseClient(final DataSource dataSource)
Creates a new JdbcDatabaseClient with the specified data source.
long[] batch(final String sql, final List< StatementBinder > binders)
Executes a batch of SQL commands with the specified binders.