diff --git a/README.md b/README.md index 1988cb5..a0d8fdc 100644 --- a/README.md +++ b/README.md @@ -17,43 +17,27 @@ Add maven dependency: com.github.buckelieg jdbc-fn - 0.3 + 1.0 ``` ### Setup database There are a couple of ways to set up the things: ```java -// 1. Provide DataSource -DataSource ds = // obtain ds (e.g. via JNDI or other way) -DB db = new DB(ds); -// 2. Provide connection supplier -DB db = new DB(ds::getConnection); +DataSource ds = ... // obtain ds (e.g. via JNDI or other way) +DB db = DB.create(ds::getConnection); // shortcut for DB.builder().build(ds::getConnection) // or -DB db = new DB(() -> {/*sophisticated connection supplier function*/}); +DB db = DB.builder() + .withMaxConnections(10) // defaults to Runtime.getRuntime().availableProcessors() + .build(() -> DriverManager.getConnection("jdbc:derby:memory:test;create=true")); // do things... -db.close(); -// DB can be used in try-with-resources statements -try (DB db = new DB(/*init*/)) { - ... -} finally { - -} -``` -Note that providing connection supplier function with plain connection -
like this: DB db = new DB(() -> connection)); -
or this:   DB db = new DB(() -> DriverManager.getConnection("vendor-specific-string")); -
e.g - if supplier function always return the same connection -
the concept of transactions will be partially broken: see [Transactions](#transactions) section. +db.close(); // cleaning used resources: closes underlying connection pool, executor service (if configured to do so) etc... +``` ### Select Use question marks: ```java Collection names = db.select("SELECT name FROM TEST WHERE ID IN (?, ?)", 1, 2).execute(rs -> rs.getString("name")).collect(Collectors.toList()); -// an alias for execute method is stream - for better readability -Collection names = db.select("SELECT name FROM TEST WHERE ID IN (?, ?)", 1, 2).stream(rs -> rs.getString("name")).collect(Collectors.toList()); -// or use shorthands for stream reduction -Collection names = db.select("SELECT name FROM TEST WHERE ID IN (?, ?)", 1, 2).list(rs -> rs.getString("name")); ``` or use named parameters: ```java @@ -75,6 +59,41 @@ Collection names = db.select("SELECT name FROM TEST WHERE 1=1 AND ID IN Parameter names are CASE SENSITIVE! 'Name' and 'name' are considered different parameter names.
Parameters may be provided with or without leading colon. +###### The N+1 problem resolution +For the cases when it is needed to process (say - enrich) each mapped row with an additional data the Select.ForBatch can be used + +```java +long res = db.select("SELECT * FROM HUGE_TABLE") + .forBatch(/* map resultSet here to needed type*/) + .size(1000) + .execute(batchOfObjects -> { + // list of mapped rows with size not more than 1000 + batchOfObjects.forEach(obj -> obj.setSomethingElse()); + }); +``` +For cases where it is needed to issue any additional queries to database use: + +```java +Stream users = db.select("SELECT * FROM HUGE_TABLE") + .forBatch(/* map resultSet here to needed type*/) + .size(1000) + .execute((batch, session) -> { + // list of mapped rows (to resulting type) with size not more than 1000 + // session maps to currently used connection + Map attrs = session.select("SELECT * FROM USER_ATTR WHERE id IN (:ids)", batch.stream().map(User::getId).collect(Collectors.toList())) + .execute(/* map to collection of domain objects that represents a user attribute */) + .groupingBy(UserAttr::userId); + batch.forEach(user -> user.addAttrs(attrs.getOrDefault(user.getId, Collections.emptyList()))); + }); +// stream of users objects will consist of updated (enriched) objects +``` +Using this to process batches you must keep some things in mind: +
    +
  • Executor service is used internally to power parallel processing
  • +
  • All batches are processed regarding any short circuits possible
  • +
  • Select.fetchSize and Select.ForBatch.size are not the same but connected
  • +
+ ### Insert with question marks: ```java @@ -104,7 +123,7 @@ long res = db.update("UPDATE TEST SET NAME=:name WHERE NAME=:new_name", entry(": ###### Batch mode For batch operation use: ```java -long res = db.update("INSERT INTO TEST(name) VALUES(?)", new Object[][]{ {"name1"}, {"name2"} }).batch(true).execute(); +long res = db.update("INSERT INTO TEST(name) VALUES(?)", new Object[][]{ {"name1"}, {"name2"} }).batch(2).execute(); ``` ### Delete ```java @@ -128,7 +147,7 @@ db.script("CREATE TABLE TEST (id INTEGER NOT NULL, name VARCHAR(255));INSERT INT ``` 2) Provide a file with an SQL script ```java - db.script(new File("path/to/script.sql")).timeout(60).execute(); +db.script(new File("path/to/script.sql")).timeout(60).execute(); ``` Script:
Can contain single- and multiline comments. @@ -140,52 +159,47 @@ Script: ### Transactions There are a couple of methods provides transaction support.
Tell whether to create new transaction or not, provide isolation level and transaction logic function. + ```java // suppose we have to insert a bunch of new users by name and get the latest one filled with its attributes.... -User latestUser = db.transaction(TransactionIsolation.SERIALIZABLE, db1 -> - // here db.equals(db1) will return true - // but if we claim to create new transaction it will not, because a new connection is obtained and new DB instance is created - // so everything inside a transaction (in this case) MUST be done through db1 reference since it will operate on newly created connection - db1.update("INSERT INTO users(name) VALUES(?)", new Object[][]{ {"name1"}, {"name2"}, {"name3"} }) - .skipWarnings(false) - .timeout(1, TimeUnit.MINUTES) - .print() - .execute( - rs -> rs.getLong(1), - ids -> db1.select("SELECT * FROM users WHERE id=?", ids.peek(id -> db1.procedure("{call PROCESS_USER_CREATED_EVENT(?)}", id).call()).max(Comparator.comparing(i -> i)).orElse(-1L)) - .print() - .single(rs -> { - User u = new User(); - u.setId(rs.getLong("id")); - u.setName(rs.getString("name")); - //... fill other user's attributes... - return u; - }) - ) - .orElse(null) + +Logger LOG = getLogger(); //... logger used in application +User latestUser = db.transaction() + .isolation(Transaction.Isolation.SERIALIZABLE) + .execute(session -> + session.update("INSERT INTO users(name) VALUES(?)", new Object[][]{{"name1"}, {"name2"}, {"name3"}}) + .skipWarnings(false) + .timeout(1, TimeUnit.MINUTES) + .print(LOG::debug) + .execute(rs -> rs.getLong(1)) + .stream() + .peek(id -> session.procedure("{call PROCESS_USER_CREATED_EVENT(?)}", id).call()) + .max(Comparator.comparing(i -> i)) + .flatMap(id -> session.select("SELECT * FROM users WHERE id=?", id).print(LOG::debug).single(rs -> { + User u = new User(); + u.setId(rs.getLong("id")); + u.setName(rs.getString("name")); + // ...fill other user's attributes... + return user; + })) + .orElse(null) ); ``` -As the rule of thumb: always use lambda parameter to do the things inside the transaction -###### Nested transactions -This must be used with care. -
When calling transaction() method createNew flag (if set to true) implies obtaining new connection via DataSource or connection supplier function provided at the DB class [initialization](#setup-database) stage. -
If provided connection supplier function will not return a new connection - then UnsupportedOperationException is thrown: -```java -DB db = new DB(() -> connection); -db.transaction(TransactionIsolation.SERIALIZABLE, db1 -> db1.transaction(true, db2 -> ...)) -// throws UnsupportedOperationException -``` -Using nested transactions with various isolation levels may result in deadlocks: +###### Nested transactions and deadlocks + +Providing connection supplier function with plain connection +
like this: DB db = DB.create(() -> connection)); +
or this:   DB db = DB.builder().withMaxConnections(1).build(() -> DriverManager.getConnection("vendor-specific-string")); +
e.g - if supplier function always return the same connection +
the concept of transactions will be partially broken. + +The simplest case: ```java -DB db = new DB(datasourceInstance); -db.transaction(TransactionIsolation.READ_UNCOMMITED, db1 -> { - // do inserts, updates etc... - long someGeneratedId = .... - return db1.transaction(true, TransactionIsolation.SERIALIZABLE, db2 -> db2.select("SELECT * FROM TEST WHERE id=?", someGeneratedId).list(rs -> rs.getString("name"))); -}); -// nested transaction will be done over newly obtained connection but will not able to complete or see the generated values before enclosing transaction is committed and will eventually fail +DB db = DB.create(() -> connection); // or DB.builder().withMaxConnections(1).build(ds::getConnection) +db.transaction().run(session1 -> db.transaction().run(session2 -> {})) +// runs forever since each transaction tries to obtain new connection and the second one cannot be provided with new one ``` -Whenever desired transaction isolation level is not supported by RDBMS the IllegalArgumentException is thrown. + ### Logging & Debugging Convenient logging methods provided. ```java @@ -209,40 +223,11 @@ This will print out to standard output two lines:
Calling print() on Script will print out the whole sql script with parameters substituted.
Custom logging handler may also be provided for both cases. -### Helper: Queries -For cases when it is all about query execution on existing connection with no tuning, logging and other stuff the Queries helper class can be used: -```java -Connection conn = ... // somewhere previously created connection -List names = Queries.list(conn, rs -> rs.getString("name"), "SELECT name FROM TEST WHERE id IN (:ids)", new SimpleImmutableEntry("ids", new long[]{1, 2, 3})); -``` -There are plenty of pre-defined cases implemented: -
list - for list selection -
single - for single object selection, -
callForList - calling StoredProcedure which returns a ResultSet, -
call - call a StoredProcedure either with results or without, -
update - to execute various updates, -
execute - to execute atomic queries and/or scripts -
-
There is an option to set up the connection with helper class to reduce a number of method arguments: -```java -Connection conn = ... // somewhere previously created connection -Queries.setConnection(conn); -// all subsequent calls will be done on connection set. -List names = Queries.list(rs -> rs.getString("name"), "SELECT name FROM TEST WHERE id IN (:ids)", new SimpleImmutableEntry("ids", new long[]{1, 2, 3})); -List names = Queries.callForList(rs -> rs.getString(1), "{call GETALLNAMES()}"); -``` -Note that connection must be closed explicitly after using Queries helper. ### Built-in mappers All Select query methods which takes a mapper function has a companion one without.
Calling that mapper-less methods will imply mapping a tuple as String alias to Object value: ```java -// DB -DB db = new DB(datasourceInstance); -List> = db.select("SELECT name FROM TEST").list(); -// Queries -Connection conn = ... // somewhere previously created connection -Queries.setConnection(conn); -List> names = Queries.list("SELECT name FROM TEST WHERE id IN (:ids)", new SimpleImmutableEntry("ids", new long[]{1, 2, 3})); +List> = db.select("SELECT name FROM TEST").execute().collect(Collectors.toList()); ``` ### Prerequisites diff --git a/src/main/java/buckelieg/jdbc/DefaultConnectionManager.java b/src/main/java/buckelieg/jdbc/DefaultConnectionManager.java index 9c93845..23c7428 100644 --- a/src/main/java/buckelieg/jdbc/DefaultConnectionManager.java +++ b/src/main/java/buckelieg/jdbc/DefaultConnectionManager.java @@ -76,7 +76,9 @@ public Connection getConnection() throws SQLException { public void close(@Nullable Connection connection) throws SQLException { if (null == connection) return; connection.setAutoCommit(true); - pool.offer(connection); + if (!pool.offer(connection)) { + throw new SQLException("Connection pool is full"); + } } @Override diff --git a/src/main/java/buckelieg/jdbc/Update.java b/src/main/java/buckelieg/jdbc/Update.java index c4aeae8..1664663 100644 --- a/src/main/java/buckelieg/jdbc/Update.java +++ b/src/main/java/buckelieg/jdbc/Update.java @@ -51,7 +51,7 @@ public interface Update extends Query { * } * * @param generatedValuesMapper generated values ResultSet mapper function - * @return a {@link List} of mapped generated results + * @return a {@link List} of mapped generated results or empty if this query did not produce any generated keys * @throws NullPointerException if generatedValuesHandler or valueMapper is null * @see java.sql.Connection#prepareStatement(String, int) */ @@ -68,7 +68,7 @@ public interface Update extends Query { * * @param generatedValuesMapper generated values ResultSet mapper function * @param colNames column names with generated keys - * @return a {@link List} of mapped generated results + * @return a {@link List} of mapped generated results or empty if this query did not produce any generated keys * @throws NullPointerException if colNames or generatedValuesHandler or valueMapper is null * @throws IllegalArgumentException if colNames is empty * @see java.sql.Connection#prepareStatement(String, String[]) @@ -86,7 +86,7 @@ public interface Update extends Query { * * @param generatedValuesMapper generated values ResultSet mapper function * @param colIndices indices of the columns with generated keys - * @return a {@link List} of mapped generated results + * @return a {@link List} of mapped generated results or empty if this query did not produce any generated keys * @throws NullPointerException if colIndices or generatedValuesHandler or valueMapper is null * @throws IllegalArgumentException if colIndices is empty * @see java.sql.Connection#prepareStatement(String, int[]) diff --git a/src/main/java/buckelieg/jdbc/UpdateQuery.java b/src/main/java/buckelieg/jdbc/UpdateQuery.java index d35c8f1..d817416 100644 --- a/src/main/java/buckelieg/jdbc/UpdateQuery.java +++ b/src/main/java/buckelieg/jdbc/UpdateQuery.java @@ -20,11 +20,13 @@ import buckelieg.jdbc.fn.TrySupplier; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import javax.annotation.concurrent.NotThreadSafe; import java.sql.*; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -197,7 +199,9 @@ private int executeBatch(List acc) throws SQLException { return longs.length; } - private List toList(ResultSet resultSet, TryFunction mapper) throws SQLException { + private List toList(@Nullable ResultSet resultSet, TryFunction mapper) throws SQLException { + if (null == resultSet) + return Collections.emptyList(); // derby (current version - 10.14.2.0) returns null instead of empty resultSet object ValueReader valueReader = ValueGetters.reader(new RSMeta(getConnection()::getMetaData, resultSet::getMetaData, new ConcurrentHashMap<>()), resultSet); List result = new ArrayList<>(); while (resultSet.next()) diff --git a/src/test/java/buckelieg/jdbc/DBTestSuite.java b/src/test/java/buckelieg/jdbc/DBTestSuite.java index 41392d6..4b749e2 100644 --- a/src/test/java/buckelieg/jdbc/DBTestSuite.java +++ b/src/test/java/buckelieg/jdbc/DBTestSuite.java @@ -554,7 +554,7 @@ public void testDeadlocksMultiConnectionSupplierMaxConnections1() throws Excepti AssertionFailedError.class, () -> Assertions.assertTimeoutPreemptively( Duration.ofSeconds(5), - () -> db1.transaction().run(session -> db1.transaction().run(session1 -> Thread.sleep(500))), + () -> db1.transaction().run(session -> db1.transaction().run(session1 -> {})), "execution timed out after 5000 ms" ) ); @@ -563,7 +563,8 @@ public void testDeadlocksMultiConnectionSupplierMaxConnections1() throws Excepti @Test public void testMaxConnectionsDriverManagerConnectionProvider() throws Exception { - DB db1 = DB.builder().withMaxConnections(3) + DB db1 = DB.builder() + .withMaxConnections(3) .build(() -> DriverManager.getConnection("jdbc:derby:memory:test_dm;create=true")); db1.transaction().run(s1 -> db1.transaction().run(s2 -> db1.transaction().run(s3 -> {}))); Assertions.assertThrows( @@ -779,7 +780,7 @@ public void testSelectNotThreadSafe() throws Exception { latch.countDown(); } }; - for(int i = 0; i < count; i++) { + for (int i = 0; i < count; i++) { service.execute(list::get); } latch.await();