1 /* 2 * Copyright (c) 2012-2023, jcabi.com 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 1) Redistributions of source code must retain the above 8 * copyright notice, this list of conditions and the following 9 * disclaimer. 2) Redistributions in binary form must reproduce the above 10 * copyright notice, this list of conditions and the following 11 * disclaimer in the documentation and/or other materials provided 12 * with the distribution. 3) Neither the name of the jcabi.com nor 13 * the names of its contributors may be used to endorse or promote 14 * products derived from this software without specific prior written 15 * permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 19 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 21 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 22 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 26 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 28 * OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 package com.jcabi.jdbc; 31 32 import java.sql.Connection; 33 import java.sql.PreparedStatement; 34 import java.sql.ResultSet; 35 import java.sql.SQLException; 36 import java.util.Collection; 37 import java.util.LinkedList; 38 import java.util.UUID; 39 import java.util.concurrent.atomic.AtomicReference; 40 import javax.sql.DataSource; 41 import lombok.EqualsAndHashCode; 42 import lombok.ToString; 43 44 /** 45 * Universal JDBC wrapper. 46 * 47 * <p>Execute a simple SQL query over a JDBC data source:</p> 48 * 49 * <pre> String name = new JdbcSession(source) 50 * .sql("SELECT name FROM foo WHERE id = ?") 51 * .set(123) 52 * .select( 53 * new Outcome<String>() { 54 * @Override 55 * public String handle(final ResultSet rset) throws SQLException { 56 * rset.next(); 57 * return rset.getString(1); 58 * } 59 * } 60 * );</pre> 61 * 62 * <p>There are a number of convenient pre-defined outcomes, like 63 * {@link Outcome#VOID}, {@link Outcome#NOT_EMPTY}, {@link Outcome#UPDATE_COUNT} 64 * {@link SingleOutcome}, etc.</p> 65 * 66 * <p>Methods {@link #insert(Outcome)}, 67 * {@link #update(Outcome)}, 68 * {@link #execute()}, and 69 * {@link #select(Outcome)} clean the list of arguments pre-set by 70 * {@link #set(Object)}. The class can be used for a complex transaction, when 71 * it's necessary to perform a number of SQL statements in a group. For 72 * example, the following construct will execute two SQL queries, in a single 73 * transaction and will "commit" at the end (or rollback the entire transaction 74 * in case of any error in between):</p> 75 * 76 * <pre> new JdbcSession(source) 77 * .autocommit(false) 78 * .sql("START TRANSACTION") 79 * .execute() 80 * .sql("DELETE FROM foo WHERE id = ?") 81 * .set(444) 82 * .execute() 83 * .set(555) 84 * .execute() 85 * .commit();</pre> 86 * 87 * <p>The following SQL queries will be sent to the database:</p> 88 * 89 * <pre> START TRANSACTION; 90 * DELETE FROM foo WHERE id = 444; 91 * DELETE FROM foo WHERE id = 555; 92 * COMMIT;</pre> 93 * 94 * <p>{@link #autocommit(boolean)} (with {@code false} as an argument) 95 * can be used when it's necessary to execute 96 * a statement and leave the connection open. For example when shutting down 97 * the database through SQL:</p> 98 * 99 * <pre> new JdbcSession(/* H2 Database data source */) 100 * .autocommit(false) 101 * .sql("SHUTDOWN COMPACT") 102 * .execute();</pre> 103 * 104 * <p><b>IMPORTANT:</b></p> 105 * 106 * <p>If you rely on one specific {@link Connection} instance, be careful if 107 * you are using it in more places, especially if more references of this class 108 * use it - one of those references might close the connection if you forget 109 * to call {@link JdbcSession#autocommit(boolean)} with {@code false} as an argument, 110 * for example:</p> 111 * 112 * <pre> Connection connection = [...]; 113 * DataSource ds = new StaticSource(connection); 114 * new JdbcSession(ds) 115 * .sql("SQL STATEMENT") 116 * .execute(); 117 * new JdbcSession(ds) 118 * .sql("SQL STATEMENT 2") 119 * .execute();</pre> 120 121 * <p>The above example will <b>fail</b> because the first JdbcSession closes 122 * the connection, and the next one tries to work with it closed. In order to 123 * not have this failure, the first session has to call 124 * {@link #autocommit(boolean)} with {@code false} as an argument, like this:</p> 125 * 126 * <pre> Connection connection = [...]; 127 * DataSource ds = new StaticSource(connection); 128 * new JdbcSession(ds) 129 * .autocommit(false) 130 * .sql("SQL STATEMENT") 131 * .execute(); 132 * new JdbcSession(ds) 133 * .sql("SQL STATEMENT 2") 134 * .execute();</pre> 135 * 136 * <p>This class is thread-safe.</p> 137 * 138 * @since 0.1.8 139 * @todo #51:30min Refactor this class to avoid too much coupling. 140 * For instance, CRUD operations could be performed by another class. 141 * Don't forget to remove the suppressions that become obsolete afterwards. 142 */ 143 @ToString 144 @EqualsAndHashCode(of = { "source", "connection", "args", "auto", "query" }) 145 @SuppressWarnings({ "PMD.TooManyMethods", "PMD.CloseResource" }) 146 public final class JdbcSession { 147 148 /** 149 * JDBC DataSource to get connections from. 150 */ 151 private final transient DataSource source; 152 153 /** 154 * Arguments. 155 * 156 * <p>Every time this attribute is modified, we must synchronize, because 157 * a non-thread-safe {@link LinkedList} is assigned to it.</p> 158 */ 159 private final transient Collection<Object> args; 160 161 /** 162 * Arguments. 163 * 164 * <p>Every time this attribute is modified, we must synchronize, because 165 * a non-thread-safe {@link LinkedList} is assigned to it.</p> 166 * 167 * @since 0.13 168 */ 169 private final transient Collection<Preparation> preparations; 170 171 /** 172 * Connection currently open. 173 */ 174 private final transient AtomicReference<Connection> connection; 175 176 /** 177 * Shall we close/autocommit automatically? 178 */ 179 private transient boolean auto; 180 181 /** 182 * The query to use. 183 */ 184 private transient String query; 185 186 /** 187 * Public ctor. 188 * 189 * <p>If all you have is a {@link Connection}, wrap it inside our 190 * {@link StaticSource}, but make sure you understand the autocommit 191 * mechanism we have in place here. Read the class' javadoc (especially the 192 * last paragraph, marked with <b>IMPORTANT</b>).</p> 193 * 194 * @param src Data source 195 */ 196 @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors") 197 public JdbcSession(final DataSource src) { 198 this.args = new LinkedList<>(); 199 this.preparations = new LinkedList<>(); 200 this.connection = new AtomicReference<>(); 201 this.auto = true; 202 this.source = src; 203 this.preparations.add(new PrepareArgs(this.args)); 204 } 205 206 /** 207 * Use this SQL query (with optional parameters inside). 208 * 209 * <p>The query will be used in {@link PreparedStatement}, that's why 210 * you can use the same formatting as there. Arguments shall be marked 211 * as {@code "?"} (question marks). For example:</p> 212 * 213 * <pre> String name = new JdbcSession(source) 214 * .sql("INSERT INTO foo (id, name) VALUES (?, ?)") 215 * .set(556677) 216 * .set("Jeffrey Lebowski") 217 * .insert(Outcome.VOID);</pre> 218 * 219 * @param sql The SQL query to use 220 * @return This object 221 */ 222 public JdbcSession sql(final String sql) { 223 synchronized (this.args) { 224 this.query = sql; 225 } 226 return this; 227 } 228 229 /** 230 * Shall we auto-commit? 231 * 232 * <p>By default this flag is set to TRUE, which means that methods 233 * {@link #insert(Outcome)}, {@link #execute()}, and 234 * {@link #select(Outcome)} will 235 * call {@link Connection#commit()} after 236 * their successful execution.</p> 237 * 238 * @param autocommit Shall we? 239 * @return This object 240 */ 241 public JdbcSession autocommit(final boolean autocommit) { 242 synchronized (this.args) { 243 this.auto = autocommit; 244 } 245 return this; 246 } 247 248 /** 249 * Set new parameter for the query. 250 * 251 * <p>The following types are supported: {@link Boolean}, 252 * {@link java.sql.Date}, 253 * {@link Utc}, {@link Long}, {@link Float}, byte[], {@link Integer}, {@link UUID}. 254 * All other types will be converted to {@link String} using 255 * their {@code toString()} methods.</p> 256 * 257 * @param value The value to add 258 * @return This object 259 */ 260 public JdbcSession set(final Object value) { 261 synchronized (this.args) { 262 this.args.add(value); 263 } 264 return this; 265 } 266 267 /** 268 * Run this preparation before executing the statement. 269 * 270 * @param prp Preparation 271 * @return This object 272 * @since 0.13 273 */ 274 public JdbcSession prepare(final Preparation prp) { 275 synchronized (this.args) { 276 this.preparations.add(prp); 277 } 278 return this; 279 } 280 281 /** 282 * Clear all pre-set parameters (args, preparations, etc). 283 * 284 * @return This object 285 * @since 0.13 286 */ 287 public JdbcSession clear() { 288 synchronized (this.args) { 289 this.args.clear(); 290 this.preparations.clear(); 291 this.preparations.add(new PrepareArgs(this.args)); 292 } 293 return this; 294 } 295 296 /** 297 * Commit the transaction (calls {@link Connection#commit()} and then 298 * {@link Connection#close()}). 299 * 300 * @throws SQLException If fails to do the SQL operation 301 */ 302 public void commit() throws SQLException { 303 final Connection conn = this.connection.get(); 304 if (conn == null) { 305 throw new IllegalStateException( 306 "Connection is not open, can't commit" 307 ); 308 } 309 conn.commit(); 310 this.disconnect(); 311 } 312 313 /** 314 * Rollback the transaction (calls {@link Connection#rollback()} and then 315 * {@link Connection#close()}). 316 * 317 * @throws SQLException If fails to do the SQL operation 318 */ 319 public void rollback() throws SQLException { 320 final Connection conn = this.connection.get(); 321 if (conn == null) { 322 throw new IllegalStateException( 323 "Connection is not open, can't rollback" 324 ); 325 } 326 conn.rollback(); 327 this.disconnect(); 328 } 329 330 /** 331 * Make SQL {@code INSERT} request. 332 * 333 * <p>{@link Outcome} will receive 334 * a {@link ResultSet} of generated keys.</p> 335 * 336 * <p>JDBC connection is opened and, optionally, closed by this method.</p> 337 * 338 * @param outcome The outcome of the operation 339 * @param <T> Type of response 340 * @return The result 341 * @throws SQLException If fails 342 */ 343 public <T> T insert(final Outcome<T> outcome) 344 throws SQLException { 345 return this.run( 346 outcome, 347 new Connect.WithKeys(this.query), 348 Request.EXECUTE 349 ); 350 } 351 352 /** 353 * Make SQL {@code UPDATE} request. 354 * 355 * <p>JDBC connection is opened and, optionally, closed by this method.</p> 356 * 357 * @param outcome Outcome of the operation 358 * @param <T> Type of result expected 359 * @return This object 360 * @throws SQLException If fails 361 */ 362 public <T> T update(final Outcome<T> outcome) 363 throws SQLException { 364 return this.run( 365 outcome, 366 new Connect.WithKeys(this.query), 367 Request.EXECUTE_UPDATE 368 ); 369 } 370 371 /** 372 * Call an SQL stored procedure. 373 * 374 * <p>JDBC connection is opened and, optionally, committed by this 375 * method, depending on the <b>autocommit</b> class attribute: 376 * if it's value is true, the connection will be committed after 377 * this call.</p> 378 * 379 * @param outcome Outcome of the operation 380 * @param <T> Type of result expected 381 * @return Result of type T 382 * @throws SQLException If fails 383 */ 384 public <T> T call(final Outcome<T> outcome) 385 throws SQLException { 386 return this.run( 387 outcome, new Connect.Call(this.query), Request.EXECUTE_UPDATE 388 ); 389 } 390 391 /** 392 * Make SQL request expecting no response from the server. 393 * 394 * <p>This method should be used for schema manipulation statements, 395 * like CREATE TABLE, CREATE INDEX, DROP COLUMN, etc. and server-side 396 * instructions that return no data back. Main difference between this 397 * one and {@code #execute()} is that the later requests JDBC to return 398 * generated keys. When SQL server doesn't return any keys this may 399 * cause runtime exceptions in JDBC.</p> 400 * 401 * <p>JDBC connection is opened and, optionally, closed by this method.</p> 402 * 403 * @return This object 404 * @throws SQLException If fails 405 * @since 0.9 406 */ 407 public JdbcSession execute() throws SQLException { 408 final String vendor; 409 try (Connection conn = this.source.getConnection()) { 410 vendor = conn.getMetaData().getDatabaseProductName(); 411 } 412 final Connect connect; 413 if (vendor.equalsIgnoreCase("mysql")) { 414 connect = new Connect.WithKeys(this.query); 415 } else { 416 connect = new Connect.Plain(this.query); 417 } 418 this.run(Outcome.VOID, connect, Request.EXECUTE); 419 return this; 420 } 421 422 /** 423 * Make SQL {@code SELECT} request. 424 * 425 * <p>JDBC connection is opened and, optionally, closed by this method.</p> 426 * 427 * @param outcome The outcome of the operation 428 * @param <T> Type of response 429 * @return The result 430 * @throws SQLException If fails 431 */ 432 public <T> T select(final Outcome<T> outcome) 433 throws SQLException { 434 return this.run( 435 outcome, 436 new Connect.Plain(this.query), 437 Request.EXECUTE_QUERY 438 ); 439 } 440 441 /** 442 * Run with this outcome, and this fetcher. 443 * 444 * @param outcome The outcome of the operation 445 * @param connect Connect 446 * @param request Request 447 * @param <T> Type of response 448 * @return The result 449 * @throws SQLException If fails 450 * @checkstyle ExecutableStatementCount (100 lines) 451 */ 452 private <T> T run(final Outcome<T> outcome, 453 final Connect connect, final Request request) 454 throws SQLException { 455 if (this.query == null) { 456 throw new IllegalStateException("Call #sql() first"); 457 } 458 final Connection conn = this.connect(); 459 conn.setAutoCommit(this.auto); 460 try { 461 return this.fetch(outcome, request, connect.open(conn)); 462 } catch (final SQLException ex) { 463 this.rollbackOnFailure(conn, ex); 464 throw new SQLException(ex); 465 } finally { 466 if (this.auto) { 467 this.disconnect(); 468 } 469 this.clear(); 470 } 471 } 472 473 /** 474 * Fetch the result. 475 * @param outcome The outcome of the operation 476 * @param request Request 477 * @param stmt Statement 478 * @param <T> Type of response 479 * @return The result 480 * @throws SQLException If fails 481 */ 482 private <T> T fetch(final Outcome<T> outcome, 483 final Request request, final PreparedStatement stmt) throws SQLException { 484 final T result; 485 try { 486 this.configure(stmt); 487 final ResultSet rset = request.fetch(stmt); 488 // @checkstyle NestedTryDepth (5 lines) 489 try { 490 result = outcome.handle(rset, stmt); 491 } finally { 492 if (rset != null) { 493 rset.close(); 494 } 495 } 496 } finally { 497 stmt.close(); 498 } 499 return result; 500 } 501 502 /** 503 * Rollback in case of error. 504 * @param conn The connection 505 * @param failure The original failure 506 * @throws SQLException If fails 507 */ 508 @SuppressWarnings("PMD.PreserveStackTrace") 509 private void rollbackOnFailure(final Connection conn, final SQLException failure) 510 throws SQLException { 511 if (!this.auto) { 512 try { 513 conn.rollback(); 514 this.disconnect(); 515 } catch (final SQLException exc) { 516 throw new SQLException( 517 String.format( 518 "Failed to rollback after failure: %s", 519 exc.getMessage() 520 ), 521 failure 522 ); 523 } 524 } 525 } 526 527 /** 528 * Open connection and cache it locally in the class. 529 * 530 * @return Connection to use 531 * @throws SQLException If fails 532 */ 533 private Connection connect() throws SQLException { 534 synchronized (this.args) { 535 if (this.connection.get() == null) { 536 this.connection.set(this.source.getConnection()); 537 } 538 return this.connection.get(); 539 } 540 } 541 542 /** 543 * Close connection if it's open (runtime exception otherwise). 544 * 545 * @throws SQLException If fails to do the SQL operation 546 */ 547 private void disconnect() throws SQLException { 548 final Connection conn = this.connection.getAndSet(null); 549 if (conn == null) { 550 throw new IllegalStateException( 551 "Connection is not open, can't close" 552 ); 553 } 554 conn.close(); 555 } 556 557 /** 558 * Configure the statement. 559 * 560 * @param stmt Statement 561 * @throws SQLException If fails 562 */ 563 private void configure(final PreparedStatement stmt) throws SQLException { 564 for (final Preparation prep : this.preparations) { 565 prep.prepare(stmt); 566 } 567 } 568 569 }