001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2025 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import javax.annotation.Nonnull; 020import javax.annotation.Nullable; 021import javax.annotation.concurrent.NotThreadSafe; 022import javax.annotation.concurrent.ThreadSafe; 023import javax.sql.DataSource; 024import java.sql.Connection; 025import java.sql.DatabaseMetaData; 026import java.sql.PreparedStatement; 027import java.sql.ResultSet; 028import java.sql.SQLException; 029import java.time.Duration; 030import java.time.ZoneId; 031import java.util.ArrayDeque; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.Deque; 035import java.util.List; 036import java.util.Optional; 037import java.util.concurrent.atomic.AtomicInteger; 038import java.util.function.Consumer; 039import java.util.logging.Logger; 040import java.util.stream.Collectors; 041 042import static java.lang.String.format; 043import static java.lang.System.nanoTime; 044import static java.util.Objects.requireNonNull; 045import static java.util.logging.Level.WARNING; 046 047/** 048 * Main class for performing database access operations. 049 * 050 * @author <a href="https://www.revetkn.com">Mark Allen</a> 051 * @since 1.0.0 052 */ 053@ThreadSafe 054public final class Database { 055 @Nonnull 056 private static final ThreadLocal<Deque<Transaction>> TRANSACTION_STACK_HOLDER; 057 058 static { 059 TRANSACTION_STACK_HOLDER = ThreadLocal.withInitial(() -> new ArrayDeque<>()); 060 } 061 062 @Nonnull 063 private final DataSource dataSource; 064 @Nonnull 065 private final DatabaseType databaseType; 066 @Nonnull 067 private final ZoneId timeZone; 068 @Nonnull 069 private final InstanceProvider instanceProvider; 070 @Nonnull 071 private final PreparedStatementBinder preparedStatementBinder; 072 @Nonnull 073 private final ResultSetMapper resultSetMapper; 074 @Nonnull 075 private final StatementLogger statementLogger; 076 @Nonnull 077 private final AtomicInteger defaultIdGenerator; 078 @Nonnull 079 private final Logger logger; 080 081 @Nonnull 082 private volatile DatabaseOperationSupportStatus executeLargeBatchSupported; 083 @Nonnull 084 private volatile DatabaseOperationSupportStatus executeLargeUpdateSupported; 085 086 protected Database(@Nonnull Builder builder) { 087 requireNonNull(builder); 088 089 this.dataSource = requireNonNull(builder.dataSource); 090 this.databaseType = requireNonNull(builder.databaseType); 091 this.timeZone = builder.timeZone == null ? ZoneId.systemDefault() : builder.timeZone; 092 this.instanceProvider = builder.instanceProvider == null ? new InstanceProvider() {} : builder.instanceProvider; 093 this.preparedStatementBinder = builder.preparedStatementBinder == null ? PreparedStatementBinder.withDefaultConfiguration() : builder.preparedStatementBinder; 094 this.resultSetMapper = builder.resultSetMapper == null ? ResultSetMapper.withDefaultConfiguration() : builder.resultSetMapper; 095 this.statementLogger = builder.statementLogger == null ? (statementLog) -> {} : builder.statementLogger; 096 this.defaultIdGenerator = new AtomicInteger(); 097 this.logger = Logger.getLogger(getClass().getName()); 098 this.executeLargeBatchSupported = DatabaseOperationSupportStatus.UNKNOWN; 099 this.executeLargeUpdateSupported = DatabaseOperationSupportStatus.UNKNOWN; 100 } 101 102 /** 103 * Provides a {@link Database} builder for the given {@link DataSource}. 104 * 105 * @param dataSource data source used to create the {@link Database} builder 106 * @return a {@link Database} builder 107 */ 108 @Nonnull 109 public static Builder withDataSource(@Nonnull DataSource dataSource) { 110 requireNonNull(dataSource); 111 return new Builder(dataSource); 112 } 113 114 /** 115 * Gets a reference to the current transaction, if any. 116 * 117 * @return the current transaction 118 */ 119 @Nonnull 120 public Optional<Transaction> currentTransaction() { 121 Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get(); 122 return Optional.ofNullable(transactionStack.isEmpty() ? null : transactionStack.peek()); 123 } 124 125 /** 126 * Performs an operation transactionally. 127 * <p> 128 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 129 * 130 * @param transactionalOperation the operation to perform transactionally 131 */ 132 public void transaction(@Nonnull TransactionalOperation transactionalOperation) { 133 requireNonNull(transactionalOperation); 134 135 transaction(() -> { 136 transactionalOperation.perform(); 137 return Optional.empty(); 138 }); 139 } 140 141 /** 142 * Performs an operation transactionally with the given isolation level. 143 * <p> 144 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 145 * 146 * @param transactionIsolation the desired database transaction isolation level 147 * @param transactionalOperation the operation to perform transactionally 148 */ 149 public void transaction(@Nonnull TransactionIsolation transactionIsolation, 150 @Nonnull TransactionalOperation transactionalOperation) { 151 requireNonNull(transactionIsolation); 152 requireNonNull(transactionalOperation); 153 154 transaction(transactionIsolation, () -> { 155 transactionalOperation.perform(); 156 return Optional.empty(); 157 }); 158 } 159 160 /** 161 * Performs an operation transactionally and optionally returns a value. 162 * <p> 163 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 164 * 165 * @param transactionalOperation the operation to perform transactionally 166 * @param <T> the type to be returned 167 * @return the result of the transactional operation 168 */ 169 @Nonnull 170 public <T> Optional<T> transaction(@Nonnull ReturningTransactionalOperation<T> transactionalOperation) { 171 requireNonNull(transactionalOperation); 172 return transaction(TransactionIsolation.DEFAULT, transactionalOperation); 173 } 174 175 /** 176 * Performs an operation transactionally with the given isolation level, optionally returning a value. 177 * <p> 178 * The transaction will be automatically rolled back if an exception bubbles out of {@code transactionalOperation}. 179 * 180 * @param transactionIsolation the desired database transaction isolation level 181 * @param transactionalOperation the operation to perform transactionally 182 * @param <T> the type to be returned 183 * @return the result of the transactional operation 184 */ 185 @Nonnull 186 public <T> Optional<T> transaction(@Nonnull TransactionIsolation transactionIsolation, 187 @Nonnull ReturningTransactionalOperation<T> transactionalOperation) { 188 requireNonNull(transactionIsolation); 189 requireNonNull(transactionalOperation); 190 191 Transaction transaction = new Transaction(dataSource, transactionIsolation); 192 TRANSACTION_STACK_HOLDER.get().push(transaction); 193 boolean committed = false; 194 195 try { 196 Optional<T> returnValue = transactionalOperation.perform(); 197 198 // Safeguard in case user code accidentally returns null instead of Optional.empty() 199 if (returnValue == null) 200 returnValue = Optional.empty(); 201 202 if (transaction.isRollbackOnly()) { 203 transaction.rollback(); 204 } else { 205 transaction.commit(); 206 committed = true; 207 } 208 209 return returnValue; 210 } catch (RuntimeException e) { 211 try { 212 transaction.rollback(); 213 } catch (Exception rollbackException) { 214 logger.log(WARNING, "Unable to roll back transaction", rollbackException); 215 } 216 217 throw e; 218 } catch (Throwable t) { 219 try { 220 transaction.rollback(); 221 } catch (Exception rollbackException) { 222 logger.log(WARNING, "Unable to roll back transaction", rollbackException); 223 } 224 225 throw new RuntimeException(t); 226 } finally { 227 Deque<Transaction> transactionStack = TRANSACTION_STACK_HOLDER.get(); 228 229 transactionStack.pop(); 230 231 // Ensure txn stack is fully cleaned up 232 if (transactionStack.isEmpty()) 233 TRANSACTION_STACK_HOLDER.remove(); 234 235 try { 236 try { 237 transaction.restoreTransactionIsolationIfNeeded(); 238 239 if (transaction.getInitialAutoCommit().isPresent() && transaction.getInitialAutoCommit().get()) 240 // Autocommit was true initially, so restoring to true now that transaction has completed 241 transaction.setAutoCommit(true); 242 } finally { 243 if (transaction.hasConnection()) 244 closeConnection(transaction.getConnection()); 245 } 246 } finally { 247 // Execute any user-supplied post-execution hooks 248 for (Consumer<TransactionResult> postTransactionOperation : transaction.getPostTransactionOperations()) 249 postTransactionOperation.accept(committed ? TransactionResult.COMMITTED : TransactionResult.ROLLED_BACK); 250 } 251 } 252 } 253 254 protected void closeConnection(@Nonnull Connection connection) { 255 requireNonNull(connection); 256 257 try { 258 connection.close(); 259 } catch (SQLException e) { 260 throw new DatabaseException("Unable to close database connection", e); 261 } 262 } 263 264 /** 265 * Performs an operation in the context of a pre-existing transaction. 266 * <p> 267 * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes. 268 * <p> 269 * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only. 270 * 271 * @param transaction the transaction in which to participate 272 * @param transactionalOperation the operation that should participate in the transaction 273 */ 274 public void participate(@Nonnull Transaction transaction, 275 @Nonnull TransactionalOperation transactionalOperation) { 276 requireNonNull(transaction); 277 requireNonNull(transactionalOperation); 278 279 participate(transaction, () -> { 280 transactionalOperation.perform(); 281 return Optional.empty(); 282 }); 283 } 284 285 /** 286 * Performs an operation in the context of a pre-existing transaction, optionall returning a value. 287 * <p> 288 * No commit or rollback on the transaction will occur when {@code transactionalOperation} completes. 289 * <p> 290 * However, if an exception bubbles out of {@code transactionalOperation}, the transaction will be marked as rollback-only. 291 * 292 * @param transaction the transaction in which to participate 293 * @param transactionalOperation the operation that should participate in the transaction 294 * @param <T> the type to be returned 295 * @return the result of the transactional operation 296 */ 297 @Nonnull 298 public <T> Optional<T> participate(@Nonnull Transaction transaction, 299 @Nonnull ReturningTransactionalOperation<T> transactionalOperation) { 300 requireNonNull(transaction); 301 requireNonNull(transactionalOperation); 302 303 TRANSACTION_STACK_HOLDER.get().push(transaction); 304 305 try { 306 Optional<T> returnValue = transactionalOperation.perform(); 307 return returnValue == null ? Optional.empty() : returnValue; 308 } catch (RuntimeException e) { 309 transaction.setRollbackOnly(true); 310 throw e; 311 } catch (Throwable t) { 312 transaction.setRollbackOnly(true); 313 throw new RuntimeException(t); 314 } finally { 315 TRANSACTION_STACK_HOLDER.get().pop(); 316 } 317 } 318 319 /** 320 * Performs a SQL query that is expected to return 0 or 1 result rows. 321 * 322 * @param sql the SQL query to execute 323 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 324 * @param parameters {@link PreparedStatement} parameters, if any 325 * @param <T> the type to be returned 326 * @return a single result (or no result) 327 * @throws DatabaseException if > 1 row is returned 328 */ 329 @Nonnull 330 public <T> Optional<T> queryForObject(@Nonnull String sql, 331 @Nonnull Class<T> resultSetRowType, 332 @Nullable Object... parameters) { 333 requireNonNull(sql); 334 requireNonNull(resultSetRowType); 335 336 return queryForObject(Statement.of(generateId(), sql), resultSetRowType, parameters); 337 } 338 339 /** 340 * Performs a SQL query that is expected to return 0 or 1 result rows. 341 * 342 * @param statement the SQL statement to execute 343 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 344 * @param parameters {@link PreparedStatement} parameters, if any 345 * @param <T> the type to be returned 346 * @return a single result (or no result) 347 * @throws DatabaseException if > 1 row is returned 348 */ 349 public <T> Optional<T> queryForObject(@Nonnull Statement statement, 350 @Nonnull Class<T> resultSetRowType, 351 @Nullable Object... parameters) { 352 requireNonNull(statement); 353 requireNonNull(resultSetRowType); 354 355 List<T> list = queryForList(statement, resultSetRowType, parameters); 356 357 if (list.size() > 1) 358 throw new DatabaseException(format("Expected 1 row in resultset but got %s instead", list.size())); 359 360 return Optional.ofNullable(list.isEmpty() ? null : list.get(0)); 361 } 362 363 /** 364 * Performs a SQL query that is expected to return any number of result rows. 365 * 366 * @param sql the SQL query to execute 367 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 368 * @param parameters {@link PreparedStatement} parameters, if any 369 * @param <T> the type to be returned 370 * @return a list of results 371 */ 372 @Nonnull 373 public <T> List<T> queryForList(@Nonnull String sql, 374 @Nonnull Class<T> resultSetRowType, 375 @Nullable Object... parameters) { 376 requireNonNull(sql); 377 requireNonNull(resultSetRowType); 378 379 return queryForList(Statement.of(generateId(), sql), resultSetRowType, parameters); 380 } 381 382 /** 383 * Performs a SQL query that is expected to return any number of result rows. 384 * 385 * @param statement the SQL statement to execute 386 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 387 * @param parameters {@link PreparedStatement} parameters, if any 388 * @param <T> the type to be returned 389 * @return a list of results 390 */ 391 @Nonnull 392 public <T> List<T> queryForList(@Nonnull Statement statement, 393 @Nonnull Class<T> resultSetRowType, 394 @Nullable Object... parameters) { 395 requireNonNull(statement); 396 requireNonNull(resultSetRowType); 397 398 List<T> list = new ArrayList<>(); 399 StatementContext<T> statementContext = StatementContext.<T>with(statement, this) 400 .resultSetRowType(resultSetRowType) 401 .parameters(parameters) 402 .build(); 403 404 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 405 406 performDatabaseOperation(statementContext, parametersAsList, (PreparedStatement preparedStatement) -> { 407 long startTime = nanoTime(); 408 409 try (ResultSet resultSet = preparedStatement.executeQuery()) { 410 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 411 startTime = nanoTime(); 412 413 while (resultSet.next()) { 414 try { 415 T listElement = getResultSetMapper().map(statementContext, resultSet, statementContext.getResultSetRowType().get(), getInstanceProvider()).orElse(null); 416 list.add(listElement); 417 } catch (SQLException e) { 418 throw new DatabaseException(format("Unable to map JDBC %s row to %s", ResultSet.class.getSimpleName(), statementContext.getResultSetRowType().get()), e); 419 } 420 } 421 422 Duration resultSetMappingDuration = Duration.ofNanos(nanoTime() - startTime); 423 return new DatabaseOperationResult(executionDuration, resultSetMappingDuration); 424 } 425 }); 426 427 return list; 428 } 429 430 /** 431 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}; 432 * or a SQL statement that returns nothing, such as a DDL statement. 433 * 434 * @param sql the SQL to execute 435 * @param parameters {@link PreparedStatement} parameters, if any 436 * @return the number of rows affected by the SQL statement 437 */ 438 @Nonnull 439 public Long execute(@Nonnull String sql, 440 @Nullable Object... parameters) { 441 requireNonNull(sql); 442 return execute(Statement.of(generateId(), sql), parameters); 443 } 444 445 /** 446 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}; 447 * or a SQL statement that returns nothing, such as a DDL statement. 448 * 449 * @param statement the SQL statement to execute 450 * @param parameters {@link PreparedStatement} parameters, if any 451 * @return the number of rows affected by the SQL statement 452 */ 453 @Nonnull 454 public Long execute(@Nonnull Statement statement, 455 @Nullable Object... parameters) { 456 requireNonNull(statement); 457 458 ResultHolder<Long> resultHolder = new ResultHolder<>(); 459 StatementContext<Void> statementContext = StatementContext.with(statement, this) 460 .parameters(parameters) 461 .build(); 462 463 List<Object> parametersAsList = parameters == null ? List.of() : Arrays.asList(parameters); 464 465 performDatabaseOperation(statementContext, parametersAsList, (PreparedStatement preparedStatement) -> { 466 long startTime = nanoTime(); 467 468 DatabaseOperationSupportStatus executeLargeUpdateSupported = getExecuteLargeUpdateSupported(); 469 470 // Use the appropriate "large" value if we know it. 471 // If we don't know it, detect it and store it. 472 if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.YES) { 473 resultHolder.value = preparedStatement.executeLargeUpdate(); 474 } else if (executeLargeUpdateSupported == DatabaseOperationSupportStatus.NO) { 475 resultHolder.value = (long) preparedStatement.executeUpdate(); 476 } else { 477 // If the driver doesn't support executeLargeUpdate, then UnsupportedOperationException is thrown. 478 try { 479 resultHolder.value = preparedStatement.executeLargeUpdate(); 480 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.YES); 481 } catch (UnsupportedOperationException e) { 482 setExecuteLargeUpdateSupported(DatabaseOperationSupportStatus.NO); 483 resultHolder.value = (long) preparedStatement.executeUpdate(); 484 } 485 } 486 487 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 488 return new DatabaseOperationResult(executionDuration, null); 489 }); 490 491 return resultHolder.value; 492 } 493 494 /** 495 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 496 * which returns 0 or 1 rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 497 * 498 * @param sql the SQL query to execute 499 * @param resultSetRowType the type to which the {@link ResultSet} row should be marshaled 500 * @param parameters {@link PreparedStatement} parameters, if any 501 * @param <T> the type to be returned 502 * @return a single result (or no result) 503 * @throws DatabaseException if > 1 row is returned 504 */ 505 @Nonnull 506 public <T> Optional<T> executeForObject(@Nonnull String sql, 507 @Nonnull Class<T> resultSetRowType, 508 @Nullable Object... parameters) { 509 requireNonNull(sql); 510 requireNonNull(resultSetRowType); 511 512 return executeForObject(Statement.of(generateId(), sql), resultSetRowType, parameters); 513 } 514 515 /** 516 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 517 * which returns 0 or 1 rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 518 * 519 * @param statement the SQL statement to execute 520 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 521 * @param parameters {@link PreparedStatement} parameters, if any 522 * @param <T> the type to be returned 523 * @return a single result (or no result) 524 * @throws DatabaseException if > 1 row is returned 525 */ 526 public <T> Optional<T> executeForObject(@Nonnull Statement statement, 527 @Nonnull Class<T> resultSetRowType, 528 @Nullable Object... parameters) { 529 requireNonNull(statement); 530 requireNonNull(resultSetRowType); 531 532 // Ultimately we just delegate to queryForObject. 533 // Having `executeForList` is to allow for users to explicitly express intent 534 // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for 535 // logging, or delegation to a writable master as opposed to a read replica) 536 return queryForObject(statement, resultSetRowType, parameters); 537 } 538 539 /** 540 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 541 * which returns any number of rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 542 * 543 * @param sql the SQL to execute 544 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 545 * @param parameters {@link PreparedStatement} parameters, if any 546 * @param <T> the type to be returned 547 * @return a list of results 548 */ 549 @Nonnull 550 public <T> List<T> executeForList(@Nonnull String sql, 551 @Nonnull Class<T> resultSetRowType, 552 @Nullable Object... parameters) { 553 requireNonNull(sql); 554 requireNonNull(resultSetRowType); 555 556 return executeForList(Statement.of(generateId(), sql), resultSetRowType, parameters); 557 } 558 559 /** 560 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE}, 561 * which returns any number of rows, e.g. with Postgres/Oracle's {@code RETURNING} clause. 562 * 563 * @param statement the SQL statement to execute 564 * @param resultSetRowType the type to which {@link ResultSet} rows should be marshaled 565 * @param parameters {@link PreparedStatement} parameters, if any 566 * @param <T> the type to be returned 567 * @return a list of results 568 */ 569 @Nonnull 570 public <T> List<T> executeForList(@Nonnull Statement statement, 571 @Nonnull Class<T> resultSetRowType, 572 @Nullable Object... parameters) { 573 requireNonNull(statement); 574 requireNonNull(resultSetRowType); 575 576 // Ultimately we just delegate to queryForList. 577 // Having `executeForList` is to allow for users to explicitly express intent 578 // and make static analysis of code easier (e.g. maybe you'd like to hook all of your "execute" statements for 579 // logging, or delegation to a writable master as opposed to a read replica) 580 return queryForList(statement, resultSetRowType, parameters); 581 } 582 583 /** 584 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE} 585 * in "batch" over a set of parameter groups. 586 * <p> 587 * Useful for bulk-inserting or updating large amounts of data. 588 * 589 * @param sql the SQL to execute 590 * @param parameterGroups Groups of {@link PreparedStatement} parameters 591 * @return the number of rows affected by the SQL statement per-group 592 */ 593 @Nonnull 594 public List<Long> executeBatch(@Nonnull String sql, 595 @Nonnull List<List<Object>> parameterGroups) { 596 requireNonNull(sql); 597 requireNonNull(parameterGroups); 598 599 return executeBatch(Statement.of(generateId(), sql), parameterGroups); 600 } 601 602 /** 603 * Executes a SQL Data Manipulation Language (DML) statement, such as {@code INSERT}, {@code UPDATE}, or {@code DELETE} 604 * in "batch" over a set of parameter groups. 605 * <p> 606 * Useful for bulk-inserting or updating large amounts of data. 607 * 608 * @param statement the SQL statement to execute 609 * @param parameterGroups Groups of {@link PreparedStatement} parameters 610 * @return the number of rows affected by the SQL statement per-group 611 */ 612 @Nonnull 613 public List<Long> executeBatch(@Nonnull Statement statement, 614 @Nonnull List<List<Object>> parameterGroups) { 615 requireNonNull(statement); 616 requireNonNull(parameterGroups); 617 618 ResultHolder<List<Long>> resultHolder = new ResultHolder<>(); 619 StatementContext<List<Long>> statementContext = StatementContext.with(statement, this) 620 .parameters((List) parameterGroups) 621 .resultSetRowType(List.class) 622 .build(); 623 624 performDatabaseOperation(statementContext, (preparedStatement) -> { 625 for (List<Object> parameterGroup : parameterGroups) { 626 if (parameterGroup != null && parameterGroup.size() > 0) 627 performPreparedStatementBinding(statementContext, preparedStatement, parameterGroup); 628 629 preparedStatement.addBatch(); 630 } 631 }, (PreparedStatement preparedStatement) -> { 632 long startTime = nanoTime(); 633 List<Long> result; 634 635 DatabaseOperationSupportStatus executeLargeBatchSupported = getExecuteLargeBatchSupported(); 636 637 // Use the appropriate "large" value if we know it. 638 // If we don't know it, detect it and store it. 639 if (executeLargeBatchSupported == DatabaseOperationSupportStatus.YES) { 640 long[] resultArray = preparedStatement.executeLargeBatch(); 641 result = Arrays.stream(resultArray).boxed().collect(Collectors.toList()); 642 } else if (executeLargeBatchSupported == DatabaseOperationSupportStatus.NO) { 643 int[] resultArray = preparedStatement.executeBatch(); 644 result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 645 } else { 646 // If the driver doesn't support executeLargeBatch, then UnsupportedOperationException is thrown. 647 try { 648 long[] resultArray = preparedStatement.executeLargeBatch(); 649 result = Arrays.stream(resultArray).boxed().collect(Collectors.toList()); 650 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.YES); 651 } catch (UnsupportedOperationException e) { 652 setExecuteLargeBatchSupported(DatabaseOperationSupportStatus.NO); 653 int[] resultArray = preparedStatement.executeBatch(); 654 result = Arrays.stream(resultArray).asLongStream().boxed().collect(Collectors.toList()); 655 } 656 } 657 658 resultHolder.value = result; 659 Duration executionDuration = Duration.ofNanos(nanoTime() - startTime); 660 return new DatabaseOperationResult(executionDuration, null); 661 }); 662 663 return resultHolder.value; 664 } 665 666 /** 667 * Exposes a temporary handle to JDBC {@link DatabaseMetaData}, which provides comprehensive vendor-specific information about this database as a whole. 668 * <p> 669 * This method acquires {@link DatabaseMetaData} on its own newly-borrowed connection, which it manages internally. 670 * <p> 671 * It does <strong>not</strong> participate in the active transaction, if one exists. 672 * <p> 673 * The connection is closed as soon as {@link DatabaseMetaDataReader#read(DatabaseMetaData)} completes. 674 * <p> 675 * See <a href="https://docs.oracle.com/en/java/javase/24/docs/api/java.sql/java/sql/DatabaseMetaData.html">{@code DatabaseMetaData} Javadoc</a> for details. 676 */ 677 public void readDatabaseMetaData(@Nonnull DatabaseMetaDataReader databaseMetaDataReader) { 678 requireNonNull(databaseMetaDataReader); 679 680 performRawConnectionOperation((connection -> { 681 databaseMetaDataReader.read(connection.getMetaData()); 682 return Optional.empty(); 683 }), false); 684 } 685 686 protected <T> void performDatabaseOperation(@Nonnull StatementContext<T> statementContext, 687 @Nonnull List<Object> parameters, 688 @Nonnull DatabaseOperation databaseOperation) { 689 requireNonNull(statementContext); 690 requireNonNull(parameters); 691 requireNonNull(databaseOperation); 692 693 performDatabaseOperation(statementContext, (preparedStatement) -> { 694 if (parameters.size() > 0) 695 performPreparedStatementBinding(statementContext, preparedStatement, parameters); 696 }, databaseOperation); 697 } 698 699 protected <T> void performPreparedStatementBinding(@Nonnull StatementContext<T> statementContext, 700 @Nonnull PreparedStatement preparedStatement, 701 @Nonnull List<Object> parameters) { 702 requireNonNull(statementContext); 703 requireNonNull(preparedStatement); 704 requireNonNull(parameters); 705 706 try { 707 for (int i = 0; i < parameters.size(); ++i) { 708 Object parameter = parameters.get(i); 709 710 if (parameter != null) 711 getPreparedStatementBinder().bindParameter(statementContext, preparedStatement, i + 1, parameter); 712 else 713 preparedStatement.setObject(i + 1, parameter); 714 } 715 } catch (Exception e) { 716 throw new DatabaseException(e); 717 } 718 } 719 720 @FunctionalInterface 721 protected interface RawConnectionOperation<R> { 722 @Nonnull 723 Optional<R> perform(@Nonnull Connection connection) throws Exception; 724 } 725 726 /** 727 * @since 3.0.0 728 */ 729 @Nonnull 730 public DatabaseType getDatabaseType() { 731 return this.databaseType; 732 } 733 734 /** 735 * @since 3.0.0 736 */ 737 @Nonnull 738 public ZoneId getTimeZone() { 739 return this.timeZone; 740 } 741 742 /** 743 * Useful for single-shot "utility" calls that operate outside of normal query operations, e.g. pulling DB metadata. 744 * <p> 745 * Example: {@link #readDatabaseMetaData(DatabaseMetaDataReader)}. 746 */ 747 @Nonnull 748 protected <R> Optional<R> performRawConnectionOperation(@Nonnull RawConnectionOperation<R> rawConnectionOperation, 749 @Nonnull Boolean shouldParticipateInExistingTransactionIfPossible) { 750 requireNonNull(rawConnectionOperation); 751 requireNonNull(shouldParticipateInExistingTransactionIfPossible); 752 753 if (shouldParticipateInExistingTransactionIfPossible) { 754 // Try to participate in txn if it's available 755 Connection connection = null; 756 757 try { 758 connection = acquireConnection(); 759 return rawConnectionOperation.perform(connection); 760 } catch (DatabaseException e) { 761 throw e; 762 } catch (Exception e) { 763 throw new DatabaseException(e); 764 } finally { 765 // If this was a single-shot operation (not in a transaction), close the connection 766 if (connection != null && !currentTransaction().isPresent()) 767 closeConnection(connection); 768 } 769 } else { 770 boolean acquiredConnection = false; 771 772 // Always get a fresh connection no matter what and close it afterwards 773 try (Connection connection = getDataSource().getConnection()) { 774 acquiredConnection = true; 775 return rawConnectionOperation.perform(connection); 776 } catch (Exception e) { 777 if (acquiredConnection) 778 throw new DatabaseException(e); 779 else 780 throw new DatabaseException("Unable to acquire database connection", e); 781 } 782 } 783 } 784 785 protected <T> void performDatabaseOperation(@Nonnull StatementContext<T> statementContext, 786 @Nonnull PreparedStatementBindingOperation preparedStatementBindingOperation, 787 @Nonnull DatabaseOperation databaseOperation) { 788 requireNonNull(statementContext); 789 requireNonNull(preparedStatementBindingOperation); 790 requireNonNull(databaseOperation); 791 792 long startTime = nanoTime(); 793 Duration connectionAcquisitionDuration = null; 794 Duration preparationDuration = null; 795 Duration executionDuration = null; 796 Duration resultSetMappingDuration = null; 797 Exception exception = null; 798 Connection connection = null; 799 800 try { 801 boolean alreadyHasConnection = currentTransaction().isPresent() && currentTransaction().get().hasConnection(); 802 connection = acquireConnection(); 803 connectionAcquisitionDuration = alreadyHasConnection ? null : Duration.ofNanos(nanoTime() - startTime); 804 startTime = nanoTime(); 805 806 try (PreparedStatement preparedStatement = connection.prepareStatement(statementContext.getStatement().getSql())) { 807 preparedStatementBindingOperation.perform(preparedStatement); 808 preparationDuration = Duration.ofNanos(nanoTime() - startTime); 809 810 DatabaseOperationResult databaseOperationResult = databaseOperation.perform(preparedStatement); 811 executionDuration = databaseOperationResult.getExecutionDuration().orElse(null); 812 resultSetMappingDuration = databaseOperationResult.getResultSetMappingDuration().orElse(null); 813 } 814 } catch (DatabaseException e) { 815 exception = e; 816 throw e; 817 } catch (Exception e) { 818 exception = e; 819 throw new DatabaseException(e); 820 } finally { 821 try { 822 // If this was a single-shot operation (not in a transaction), close the connection 823 if (connection != null && !currentTransaction().isPresent()) 824 closeConnection(connection); 825 } finally { 826 StatementLog statementLog = 827 StatementLog.withStatementContext(statementContext) 828 .connectionAcquisitionDuration(connectionAcquisitionDuration) 829 .preparationDuration(preparationDuration) 830 .executionDuration(executionDuration) 831 .resultSetMappingDuration(resultSetMappingDuration) 832 .exception(exception) 833 .build(); 834 835 getStatementLogger().log(statementLog); 836 } 837 } 838 } 839 840 @Nonnull 841 protected Connection acquireConnection() { 842 Optional<Transaction> transaction = currentTransaction(); 843 844 if (transaction.isPresent()) 845 return transaction.get().getConnection(); 846 847 try { 848 return getDataSource().getConnection(); 849 } catch (SQLException e) { 850 throw new DatabaseException("Unable to acquire database connection", e); 851 } 852 } 853 854 @Nonnull 855 protected DataSource getDataSource() { 856 return this.dataSource; 857 } 858 859 @Nonnull 860 protected InstanceProvider getInstanceProvider() { 861 return this.instanceProvider; 862 } 863 864 @Nonnull 865 protected PreparedStatementBinder getPreparedStatementBinder() { 866 return this.preparedStatementBinder; 867 } 868 869 @Nonnull 870 protected ResultSetMapper getResultSetMapper() { 871 return this.resultSetMapper; 872 } 873 874 @Nonnull 875 protected StatementLogger getStatementLogger() { 876 return this.statementLogger; 877 } 878 879 @Nonnull 880 protected DatabaseOperationSupportStatus getExecuteLargeBatchSupported() { 881 return this.executeLargeBatchSupported; 882 } 883 884 protected void setExecuteLargeBatchSupported(@Nonnull DatabaseOperationSupportStatus executeLargeBatchSupported) { 885 requireNonNull(executeLargeBatchSupported); 886 this.executeLargeBatchSupported = executeLargeBatchSupported; 887 } 888 889 @Nonnull 890 protected DatabaseOperationSupportStatus getExecuteLargeUpdateSupported() { 891 return this.executeLargeUpdateSupported; 892 } 893 894 protected void setExecuteLargeUpdateSupported(@Nonnull DatabaseOperationSupportStatus executeLargeUpdateSupported) { 895 requireNonNull(executeLargeUpdateSupported); 896 this.executeLargeUpdateSupported = executeLargeUpdateSupported; 897 } 898 899 @Nonnull 900 protected Object generateId() { 901 // "Unique" keys 902 return format("com.pyranid.%s", this.defaultIdGenerator.incrementAndGet()); 903 } 904 905 @FunctionalInterface 906 protected interface DatabaseOperation { 907 @Nonnull 908 DatabaseOperationResult perform(@Nonnull PreparedStatement preparedStatement) throws Exception; 909 } 910 911 @FunctionalInterface 912 protected interface PreparedStatementBindingOperation { 913 void perform(@Nonnull PreparedStatement preparedStatement) throws Exception; 914 } 915 916 /** 917 * Builder used to construct instances of {@link Database}. 918 * <p> 919 * This class is intended for use by a single thread. 920 * 921 * @author <a href="https://www.revetkn.com">Mark Allen</a> 922 * @since 1.0.0 923 */ 924 @NotThreadSafe 925 public static class Builder { 926 @Nonnull 927 private final DataSource dataSource; 928 @Nonnull 929 private final DatabaseType databaseType; 930 @Nullable 931 private ZoneId timeZone; 932 @Nullable 933 private InstanceProvider instanceProvider; 934 @Nullable 935 private PreparedStatementBinder preparedStatementBinder; 936 @Nullable 937 private ResultSetMapper resultSetMapper; 938 @Nullable 939 private StatementLogger statementLogger; 940 941 private Builder(@Nonnull DataSource dataSource) { 942 this.dataSource = requireNonNull(dataSource); 943 this.databaseType = DatabaseType.fromDataSource(dataSource); 944 } 945 946 @Nonnull 947 public Builder timeZone(@Nullable ZoneId timeZone) { 948 this.timeZone = timeZone; 949 return this; 950 } 951 952 @Nonnull 953 public Builder instanceProvider(@Nullable InstanceProvider instanceProvider) { 954 this.instanceProvider = instanceProvider; 955 return this; 956 } 957 958 @Nonnull 959 public Builder preparedStatementBinder(@Nullable PreparedStatementBinder preparedStatementBinder) { 960 this.preparedStatementBinder = preparedStatementBinder; 961 return this; 962 } 963 964 @Nonnull 965 public Builder resultSetMapper(@Nullable ResultSetMapper resultSetMapper) { 966 this.resultSetMapper = resultSetMapper; 967 return this; 968 } 969 970 @Nonnull 971 public Builder statementLogger(@Nullable StatementLogger statementLogger) { 972 this.statementLogger = statementLogger; 973 return this; 974 } 975 976 @Nonnull 977 public Database build() { 978 return new Database(this); 979 } 980 } 981 982 @ThreadSafe 983 static class DatabaseOperationResult { 984 @Nullable 985 private final Duration executionDuration; 986 @Nullable 987 private final Duration resultSetMappingDuration; 988 989 public DatabaseOperationResult(@Nullable Duration executionDuration, 990 @Nullable Duration resultSetMappingDuration) { 991 this.executionDuration = executionDuration; 992 this.resultSetMappingDuration = resultSetMappingDuration; 993 } 994 995 @Nonnull 996 public Optional<Duration> getExecutionDuration() { 997 return Optional.ofNullable(this.executionDuration); 998 } 999 1000 @Nonnull 1001 public Optional<Duration> getResultSetMappingDuration() { 1002 return Optional.ofNullable(this.resultSetMappingDuration); 1003 } 1004 } 1005 1006 @NotThreadSafe 1007 static class ResultHolder<T> { 1008 T value; 1009 } 1010 1011 enum DatabaseOperationSupportStatus { 1012 UNKNOWN, 1013 YES, 1014 NO 1015 } 1016}