├── LICENSE ├── README.md ├── extensions ├── fluent-jdbc-guice-persist │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── codejargon │ │ │ │ └── fluentjdbc │ │ │ │ └── api │ │ │ │ └── integration │ │ │ │ └── guicepersist │ │ │ │ ├── jpa │ │ │ │ ├── JpaConnectionExtractor.java │ │ │ │ ├── JpaConnectionProvider.java │ │ │ │ └── JpaFluentJdbcModule.java │ │ │ │ └── standalone │ │ │ │ ├── StandaloneFluentJdbcModule.java │ │ │ │ ├── StandaloneTxConnectionProvider.java │ │ │ │ └── TransactionInterceptor.java │ │ └── resources │ │ │ └── LICENSE │ │ └── test │ │ ├── groovy │ │ └── org │ │ │ └── codejargon │ │ │ └── fluentjdbc │ │ │ └── api │ │ │ └── integration │ │ │ └── guicepersist │ │ │ ├── TransactionBreaking.groovy │ │ │ ├── TransactionTestRoutine.groovy │ │ │ ├── TransactionTestService.groovy │ │ │ ├── TransactionTestService2.groovy │ │ │ ├── jpa │ │ │ ├── JpaTestInitialization.groovy │ │ │ ├── PersistenceProperties.groovy │ │ │ └── vendors │ │ │ │ └── EclipseLinkTest.groovy │ │ │ └── standalone │ │ │ └── StandaloneTransactionTest.groovy │ │ └── resources │ │ └── META-INF │ │ └── persistence.xml └── pom.xml ├── fluent-jdbc ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── codejargon │ │ │ └── fluentjdbc │ │ │ ├── api │ │ │ ├── FluentJdbc.java │ │ │ ├── FluentJdbcBuilder.java │ │ │ ├── FluentJdbcException.java │ │ │ ├── FluentJdbcSqlException.java │ │ │ ├── ParamSetter.java │ │ │ ├── integration │ │ │ │ ├── ConnectionProvider.java │ │ │ │ ├── QueryConnectionReceiver.java │ │ │ │ └── providers │ │ │ │ │ └── DataSourceConnectionProvider.java │ │ │ ├── mapper │ │ │ │ ├── Mappers.java │ │ │ │ ├── ObjectMapperRsExtractor.java │ │ │ │ └── ObjectMappers.java │ │ │ └── query │ │ │ │ ├── BatchQuery.java │ │ │ │ ├── Mapper.java │ │ │ │ ├── PlainConnectionQuery.java │ │ │ │ ├── Query.java │ │ │ │ ├── SelectQuery.java │ │ │ │ ├── SqlConsumer.java │ │ │ │ ├── SqlErrorHandler.java │ │ │ │ ├── SqlErrorHandling.java │ │ │ │ ├── Transaction.java │ │ │ │ ├── UpdateQuery.java │ │ │ │ ├── UpdateResult.java │ │ │ │ ├── UpdateResultGenKeys.java │ │ │ │ ├── inspection │ │ │ │ ├── DatabaseInspection.java │ │ │ │ ├── MetaDataAccess.java │ │ │ │ ├── MetaDataResultSet.java │ │ │ │ └── MetaDataSelect.java │ │ │ │ └── listen │ │ │ │ ├── AfterQueryListener.java │ │ │ │ └── ExecutionDetails.java │ │ │ └── internal │ │ │ ├── FluentJdbcInternal.java │ │ │ ├── integration │ │ │ └── QueryConnectionReceiverInternal.java │ │ │ ├── mappers │ │ │ ├── DefaultObjectMapperRsExtractors.java │ │ │ └── ObjectMapper.java │ │ │ ├── query │ │ │ ├── BatchQueryInternal.java │ │ │ ├── DatabaseInspectionInternal.java │ │ │ ├── DefaultParamSetters.java │ │ │ ├── DefaultSqlHandler.java │ │ │ ├── ExecutionDetailsInternal.java │ │ │ ├── FetchGenKey.java │ │ │ ├── MetaDataSelectInternal.java │ │ │ ├── ParamAssigner.java │ │ │ ├── PreparedStatementFactory.java │ │ │ ├── QueryConfig.java │ │ │ ├── QueryInternal.java │ │ │ ├── QueryRunnerConnection.java │ │ │ ├── QueryRunnerPreparedStatement.java │ │ │ ├── SelectQueryInternal.java │ │ │ ├── SingleQueryBase.java │ │ │ ├── SqlAndParams.java │ │ │ ├── TransactionInternal.java │ │ │ ├── UpdateQueryInternal.java │ │ │ ├── UpdateResultGenKeysInternal.java │ │ │ ├── UpdateResultInternal.java │ │ │ └── namedparameter │ │ │ │ ├── NamedParameterUtils.java │ │ │ │ ├── NamedTransformedSql.java │ │ │ │ ├── NamedTransformedSqlFactory.java │ │ │ │ ├── ParsedSql.java │ │ │ │ └── SqlAndParamsForNamed.java │ │ │ └── support │ │ │ ├── Arrs.java │ │ │ ├── Ints.java │ │ │ ├── Iterables.java │ │ │ ├── Lists.java │ │ │ ├── Maps.java │ │ │ ├── Preconditions.java │ │ │ ├── Predicates.java │ │ │ └── Sneaky.java │ └── resources │ │ └── LICENSE │ └── test │ └── groovy │ └── org │ └── codejargon │ └── fluentjdbc │ ├── integration │ ├── IntegrationTest.groovy │ ├── IntegrationTestRoutine.groovy │ ├── testdata │ │ ├── Dummies.groovy │ │ ├── Dummy.groovy │ │ └── TestQuery.groovy │ └── vendor │ │ ├── DerbyIntegrationTest.groovy │ │ ├── H2IntegrationTest.groovy │ │ ├── HSQLIntegrationTest.groovy │ │ └── PostgresIntegrationTest.groovy │ └── internal │ ├── mappers │ ├── Dummy.groovy │ └── ObjectMapperTest.groovy │ └── query │ ├── DatabaseInspectionTest.groovy │ ├── FluentJdbcBatchTest.groovy │ ├── FluentJdbcSelectTest.groovy │ ├── FluentJdbcTransactionTest.groovy │ ├── FluentJdbcUpdateTest.groovy │ ├── ParamAssignerTest.groovy │ ├── PlainConnectionTest.groovy │ ├── QueryListenerTest.groovy │ └── UpdateTestBase.groovy └── pom.xml /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Zsolt Herpai 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### About FluentJdbc 2 | [FluentJdbc](http://zsoltherpai.github.io/fluent-jdbc) is a java library for convenient native SQL querying. Blends well with Java 8 / functional code, 3 | supports functionality many jdbc wrappers prevent / abstract away, and is lightweight (~80K, no dependencies). 4 | 5 | FluentJdbc's key features: 6 | * functional, fluent API 7 | * execution of select/insert/update/delete/alter/... statements as one-liners 8 | * parameter mapping (named, positional, supports java.time, enums, Optional, Collections, plugins for custom types) 9 | * transactions 10 | * access to generated keys of insert/update queries 11 | * big data (scalable, streaming style of batch and select) 12 | * automatic result to pojo mapping 13 | * database inspection 14 | * query listener (for logging, auditing, performance measurement, ...) 15 | 16 | ```xml 17 | 18 | org.codejargon 19 | fluentjdbc 20 | 1.8.3 21 | 22 | ``` 23 | Note: requires java 8 24 | 25 | Full documentation on [wiki](https://github.com/zsoltherpai/fluent-jdbc/wiki/Motivation) 26 | 27 | Latest [javadoc](https://github.com/zsoltherpai/fluent-jdbc/wiki/Javadoc) 28 | 29 | ##### Code examples of common use cases 30 | ###### Setting up FluentJdbc 31 | ```java 32 | DataSource dataSource = ... 33 | FluentJdbc fluentJdbc = new FluentJdbcBuilder() 34 | .connectionProvider(dataSource) 35 | .build(); 36 | Query query = fluentJdbc.query(); 37 | // ... use the Query interface for queries (thread-safe, reentrant) 38 | ``` 39 | Note: using a DataSource is the most common scenario, there are other alternatives documented on the [wiki](https://github.com/zsoltherpai/fluent-jdbc/wiki/Motivation) 40 | ###### Update or insert queries 41 | ```java 42 | query 43 | .update("UPDATE CUSTOMER SET NAME = ?, ADDRESS = ?") 44 | .params("John Doe", "Dallas") 45 | .run(); 46 | ``` 47 | ###### Query for a list of results 48 | ```java 49 | List customers = query.select("SELECT * FROM CUSTOMER WHERE NAME = ?") 50 | .params("John Doe") 51 | .listResult(customerMapper); 52 | ``` 53 | ###### Mapping of results 54 | Mapper can be implemented manually 55 | ```java 56 | resultSet -> new Customer(resultSet.getString("NAME"), resultSet.getString("ADDRESS")); 57 | ``` 58 | or mapping can be performed automatically to a java object 59 | ```java 60 | ObjectMappers objectMappers = ObjectMappers.builder().build(); //typically one instance per app 61 | ... 62 | Mapper customerMapper = objectMappers.forClass(Customer.class); 63 | ``` 64 | ###### Query for single result 65 | ```java 66 | Long count = query.select("SELECT COUNT(*) FROM CUSTOMER WHERE NAME = ?") 67 | .params("John Doe") 68 | .singleResult(Mappers.singleLong); 69 | ``` 70 | ###### Query for first result 71 | ```java 72 | Optional customer = query.select("SELECT FROM CUSTOMER WHERE NAME = ?") 73 | .params("John Doe") 74 | .firstResult(customerMapper); 75 | ``` 76 | 77 | ###### Batch insert or update 78 | ```java 79 | Stream> params = ...; // or Iterator/Iterable 80 | query.batch("INSERT INTO CUSTOMER(NAME, ADDRESS) VALUES(?, ?)") 81 | .params(params) 82 | .run(); 83 | ``` 84 | ###### Named parameters 85 | ```java 86 | query.update("UPDATE CUSTOMER SET NAME = :name, ADDRESS = :address") 87 | .namedParam("name", "John Doe") 88 | .namedParam("address", "Dallas") 89 | .run(); 90 | ``` 91 | Note: or .namedParams(mapOfParams) 92 | ###### java.time support for query parameters 93 | ```java 94 | query.update("UPDATE CUSTOMER SET DEADLINE = ?, UPDATED = ?") 95 | .params(LocalDate.of(2015, Month.MARCH, 5), Instant.now()) 96 | .run(); 97 | ``` 98 | Note: support for any type can be implemented 99 | ###### java.util.Optional support 100 | ```java 101 | Optional deadline = ... 102 | query.update("UPDATE CUSTOMER SET DEADLINE = ?") 103 | .params(deadline) 104 | .run(); 105 | ``` 106 | ###### Collection parameter support 107 | ```java 108 | Set ids = ... 109 | List customers = query.select("SELECT * FROM CUSTOMER WHERE ID IN (:ids)") 110 | .namedParam("ids", ids) 111 | .listResult(customerMapper);; 112 | ``` 113 | Note: supported for named parameters 114 | ###### Iterating a large resultset 115 | ```java 116 | query.select("SELECT * FROM CUSTOMER") 117 | .iterateResult(rs -> { 118 | // do something with the row 119 | }); 120 | ``` 121 | ###### Query for a list of limited results 122 | ```java 123 | List customers = query.select("SELECT * FROM CUSTOMER WHERE NAME = ?") 124 | .params("John Doe") 125 | .maxRows(345L) 126 | .listResult(customerMapper); 127 | ``` 128 | ###### Fetching generated key of an insert or updates 129 | ```java 130 | UpdateResultGenKeys result = query 131 | .update("INSERT INTO CUSTOMER(NAME) VALUES(:name)") 132 | .namedParams(namedParams) 133 | .runFetchGenKeys(Mappers.singleLong()); 134 | Long id = result.generatedKeys().get(0); 135 | ``` 136 | ###### Querying using a specific connection object 137 | ```java 138 | Connection connection = ... 139 | Query query = fluentJdbc.queryOn(connection); 140 | // do some querying... 141 | ``` 142 | ###### Transactions 143 | ```java 144 | query.transaction().in( 145 | () -> { 146 | query 147 | .update("UPDATE CUSTOMER SET NAME = ?, ADDRESS = ?") 148 | .params("John Doe", "Dallas") 149 | .run(); 150 | someOtherBusinessOperationAlsoNeedingTransactions(); 151 | } 152 | ) 153 | ``` 154 | All queries executed in the block will be part of the transaction - in the same thread, based on the same FluentJdbc/ConnectionProvider. 155 | Exceptions cause rollback. It is possible to use multiple transactions/datasources simultaneously. 156 | ###### Query listener 157 | A listener provides a callback mechanism called on each FluentJdbc query operation. This allows things like SQL statement logging, 158 | performance measurement. The following example logs all successful SQL operations along with the time taken to execute them: 159 | ```java 160 | AfterQueryListener listener = execution -> { 161 | if(execution.success()) { 162 | log.debug( 163 | String.format( 164 | "Query took %s ms to execute: %s", 165 | execution.executionTimeMs(), 166 | execution.sql() 167 | ) 168 | ) 169 | } 170 | }; 171 | 172 | FluentJdbc fluentJdbc = new FluentJdbcBuilder() 173 | // other configuration 174 | .afterQueryListener(listener) 175 | .build(); 176 | 177 | // run queries 178 | ``` 179 | 180 | Refer to the [full documentation](https://github.com/zsoltherpai/fluent-jdbc/wiki/Motivation) for more details and code examples. -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.codejargon 7 | fluentjdbc-extensions-parent 8 | 1.8.5 9 | 10 | fluentjdbc-guice-persist 11 | FluentJdbc Guice Persist 12 | 13 | 14 | org.codejargon 15 | fluentjdbc 16 | ${project.version} 17 | 18 | 19 | com.google.inject 20 | guice 21 | 4.0 22 | 23 | 24 | com.google.inject.extensions 25 | guice-persist 26 | 4.0 27 | 28 | 29 | org.apache.geronimo.specs 30 | geronimo-jpa_2.0_spec 31 | 1.1 32 | provided 33 | 34 | 35 | org.codejargon 36 | fluentjdbc 37 | ${project.version} 38 | test 39 | test-jar 40 | 41 | 42 | com.h2database 43 | h2 44 | 1.4.184 45 | test 46 | 47 | 48 | org.eclipse.persistence 49 | eclipselink 50 | 2.5.2 51 | test 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/main/java/org/codejargon/fluentjdbc/api/integration/guicepersist/jpa/JpaConnectionExtractor.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.jpa; 2 | 3 | import javax.persistence.EntityManager; 4 | import java.sql.Connection; 5 | 6 | /** 7 | *

Extracts the underlying Connection from an EntityManager. There is no standard way defined in JPA to extract 8 | * the underlying Connection in JPA, but it's possible with vendor-specific code. See examples below

9 | *

EclipseLink and JPA 2.0

10 | *
11 |  *  entityManager.unwrap(java.sql.Connection.class);
12 |  *  
13 | *

Hibernate 4.x and JPA 2.0

14 | *
15 |  *  Session session = entityManager.unwrap(Session.class);
16 |  *  SessionFactoryImplementor sfi = (SessionFactoryImplementor) session.getSessionFactory();
17 |  *  return sfi.getConnectionProvider().getConnection();
18 |  *  
19 | *

Hibernate 3.x and JPA 2.0

20 | *
21 |  *  return entityManager.unwrap(Session.class).connection();
22 |  *  
23 | *

Hibernate 3.x and JPA 1.0

24 | *
25 |  *  Session session = (Session) entityManager.getDelegate();
26 |  *  return session.connection();
27 |  *  
28 | * 29 | */ 30 | public interface JpaConnectionExtractor { 31 | Connection extract(EntityManager entityManager); 32 | } 33 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/main/java/org/codejargon/fluentjdbc/api/integration/guicepersist/jpa/JpaConnectionProvider.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.jpa; 2 | 3 | import com.google.inject.Provider; 4 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 5 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; 6 | import org.codejargon.fluentjdbc.api.integration.QueryConnectionReceiver; 7 | import org.codejargon.fluentjdbc.internal.support.Preconditions; 8 | 9 | import javax.persistence.EntityManager; 10 | import java.sql.Connection; 11 | import java.sql.SQLException; 12 | 13 | class JpaConnectionProvider implements ConnectionProvider { 14 | private final Provider entityManagerProvider; 15 | private final JpaConnectionExtractor jpaConnectionExtractor; 16 | 17 | JpaConnectionProvider(Provider entityManagerProvider, JpaConnectionExtractor jpaConnectionExtractor) { 18 | this.entityManagerProvider = entityManagerProvider; 19 | this.jpaConnectionExtractor = jpaConnectionExtractor; 20 | } 21 | 22 | @Override 23 | public void provide(QueryConnectionReceiver query) throws SQLException { 24 | EntityManager entityManager = entityManagerProvider.get(); 25 | if(entityManager.getTransaction().isActive()) { 26 | query.receive(extractConnection(entityManager)); 27 | } else { 28 | provideInNewTx(query, entityManager); 29 | } 30 | } 31 | private void provideInNewTx(QueryConnectionReceiver query, EntityManager entityManager) throws SQLException { 32 | entityManager.getTransaction().begin(); 33 | try { 34 | query.receive(extractConnection(entityManager)); 35 | entityManager.getTransaction().commit(); 36 | } catch(Exception e) { 37 | entityManager.getTransaction().rollback(); 38 | throw new FluentJdbcException("Error providing connection to FluentJdbc", e); 39 | } 40 | } 41 | 42 | private Connection extractConnection(EntityManager entityManager) { 43 | try { 44 | Connection connection = jpaConnectionExtractor.extract(entityManager); 45 | Preconditions.checkArgument(connection != null, "Connection returned by the provided JpaConnectionExtractor is null"); 46 | return jpaConnectionExtractor.extract(entityManager); 47 | } catch (Exception e) { 48 | throw new FluentJdbcException("Can't extract connection from EntityManager by the provided JpaConnectionExtractor", e); 49 | } 50 | } 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/main/java/org/codejargon/fluentjdbc/api/integration/guicepersist/jpa/JpaFluentJdbcModule.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.jpa; 2 | 3 | import com.google.inject.*; 4 | import org.codejargon.fluentjdbc.api.FluentJdbc; 5 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder; 6 | import org.codejargon.fluentjdbc.api.query.Query; 7 | import org.codejargon.fluentjdbc.internal.support.Preconditions; 8 | import javax.persistence.EntityManager; 9 | 10 | /** 11 | *

12 | * Binds FluentJdbc and Query instances relying on a JPA ConnectionExtractor. 13 | * Requires an existing EntityManager binding. 14 | *

15 | *

Example:

16 | *
17 |  * FluentJdbcBuilder fluentJdbcBuilder = new FluentJdbcBuilder(); // ... configure if needed
18 |  * JpaConnectionExtractor extractor = ... 
19 |  * JpaFluentJdbcModule module = new JpaFluentJdbcModule(fluentJdbcBuilder, extractor);
20 |  * 
21 | * @see org.codejargon.fluentjdbc.api.integration.guicepersist.jpa.JpaConnectionExtractor 22 | */ 23 | public class JpaFluentJdbcModule extends AbstractModule { 24 | private final FluentJdbcBuilder fluentJdbcBuilder; 25 | private final JpaConnectionExtractor jpaConnectionExtractor; 26 | 27 | /** 28 | * Constructs a JpaFluentJdbcModule based on an already configured/customized FluentJdbcBuilder. 29 | * The ConnectionProvider doesn't need to be set, the module will override that. 30 | * 31 | * @param fluentJdbcBuilder an already configured FluentJdbcBuilder 32 | * @param jpaConnectionExtractor implementation for the current JPA vendor 33 | */ 34 | public JpaFluentJdbcModule(FluentJdbcBuilder fluentJdbcBuilder, JpaConnectionExtractor jpaConnectionExtractor) { 35 | Preconditions.checkNotNull(fluentJdbcBuilder, "fluentJdbcBuilder"); 36 | Preconditions.checkNotNull(jpaConnectionExtractor, "jpaConnectionExtractor"); 37 | this.fluentJdbcBuilder = fluentJdbcBuilder; 38 | this.jpaConnectionExtractor = jpaConnectionExtractor; 39 | } 40 | 41 | @Override 42 | protected void configure() { 43 | requireBinding(Key.get(EntityManager.class)); 44 | } 45 | 46 | @Provides @Singleton 47 | FluentJdbc fluentJdbc(Provider entityManagerProvider) { 48 | return fluentJdbcBuilder 49 | .connectionProvider(new JpaConnectionProvider(entityManagerProvider, jpaConnectionExtractor)) 50 | .build(); 51 | } 52 | 53 | @Provides @Singleton 54 | Query query(FluentJdbc fluentJdbc) { 55 | return fluentJdbc.query(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/main/java/org/codejargon/fluentjdbc/api/integration/guicepersist/standalone/StandaloneFluentJdbcModule.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.standalone; 2 | 3 | import com.google.inject.AbstractModule; 4 | import com.google.inject.persist.Transactional; 5 | import org.codejargon.fluentjdbc.api.FluentJdbc; 6 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder; 7 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 8 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; 9 | import org.codejargon.fluentjdbc.api.query.Query; 10 | import org.codejargon.fluentjdbc.internal.FluentJdbcInternal; 11 | import org.codejargon.fluentjdbc.internal.support.Preconditions; 12 | 13 | import javax.sql.DataSource; 14 | import java.util.Optional; 15 | 16 | import static com.google.inject.matcher.Matchers.annotatedWith; 17 | import static com.google.inject.matcher.Matchers.any; 18 | import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkPresent; 19 | 20 | /** 21 | *

Guice module for binding of FluentJdbc and Query interfaces, and @Transactional interceptors. FluentJdbc can be 22 | * customized via FluentJdbcBuilder. The connection provider doesn't need to be set, it will be overridden by the module.

23 | * Example:

24 | *
25 |  * FluentJdbcBuilder fluentJdbcBuilder = new FluentJdbcBuilder(); // ... configure if needed
26 |  * DataSource dataSource = ...
27 |  * StandaloneFluentJdbcModule module = new StandaloneFluentJdbcModule(fluentJdbcBuilder, dataSource);
28 |  * 
29 | */ 30 | public class StandaloneFluentJdbcModule extends AbstractModule { 31 | private final StandaloneTxConnectionProvider connectionProvider; 32 | private final FluentJdbcBuilder fluentJdbcBuilder; 33 | 34 | public StandaloneFluentJdbcModule( 35 | FluentJdbcBuilder fluentJdbcBuilder, DataSource dataSource 36 | ) { 37 | this.fluentJdbcBuilder = Preconditions.checkNotNull(fluentJdbcBuilder, "fluentJdbcBuilder"); 38 | this.connectionProvider = new StandaloneTxConnectionProvider(Preconditions.checkNotNull(dataSource, "dataSource")); 39 | } 40 | 41 | /** 42 | * Constructs StandaloneFluentJdbcModule based on a custom ConnectionProvider. Warning: the ConnectionProvider 43 | * implementation must keep the Connection open after providing it to the query (closing would disrupt transaction management) 44 | * @param fluentJdbcBuilder Fluent-Jdbc configuration 45 | * @param connectionProvider an implementation that must keep the connection open after providing it to the Query 46 | */ 47 | public StandaloneFluentJdbcModule( 48 | FluentJdbcBuilder fluentJdbcBuilder, ConnectionProvider connectionProvider 49 | ) { 50 | this.fluentJdbcBuilder = Preconditions.checkNotNull(fluentJdbcBuilder, "fluentJdbcBuilder"); 51 | this.connectionProvider = new StandaloneTxConnectionProvider(Preconditions.checkNotNull(connectionProvider, "connectionProvider")); 52 | } 53 | 54 | 55 | @Override 56 | protected void configure() { 57 | FluentJdbc fluentJdbc = fluentJdbcBuilder.connectionProvider(connectionProvider).build(); 58 | bindFluentJdbc(fluentJdbc); 59 | bindTransactionInterceptors(connectionProvider); 60 | } 61 | 62 | private void bindFluentJdbc(FluentJdbc fluentJdbc) { 63 | bind(FluentJdbc.class).toInstance(fluentJdbc); 64 | bind(Query.class).toInstance(fluentJdbc.query()); 65 | } 66 | 67 | private void bindTransactionInterceptors(StandaloneTxConnectionProvider cp) { 68 | TransactionInterceptor interceptor = new TransactionInterceptor(cp); 69 | bindInterceptor(any(), annotatedWith(Transactional.class), interceptor); 70 | bindInterceptor(annotatedWith(Transactional.class), any(), interceptor); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/main/java/org/codejargon/fluentjdbc/api/integration/guicepersist/standalone/StandaloneTxConnectionProvider.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.standalone; 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcSqlException; 4 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; 5 | import org.codejargon.fluentjdbc.api.integration.QueryConnectionReceiver; 6 | import org.codejargon.fluentjdbc.internal.support.Preconditions; 7 | 8 | import javax.sql.DataSource; 9 | import java.sql.Connection; 10 | import java.sql.SQLException; 11 | import java.util.Optional; 12 | 13 | class StandaloneTxConnectionProvider implements ConnectionProvider { 14 | private final ThreadLocal> currentTxConnection = new ThreadLocal>() { 15 | @Override 16 | protected Optional initialValue() { 17 | return Optional.empty(); 18 | } 19 | }; 20 | 21 | 22 | private final ConnectionProvider connectionProvider; 23 | 24 | StandaloneTxConnectionProvider(DataSource dataSource) { 25 | Preconditions.checkNotNull(dataSource, "dataSource"); 26 | this.connectionProvider = q -> q.receive(dataSource.getConnection()); 27 | } 28 | 29 | // This connectionProvider will never close a Connection. Handled in provide(). 30 | StandaloneTxConnectionProvider(ConnectionProvider connectionProvider) { 31 | this.connectionProvider = connectionProvider; 32 | } 33 | 34 | @Override 35 | public void provide(QueryConnectionReceiver query) throws SQLException { 36 | Optional current = currentTxConnection.get(); 37 | if (current.isPresent()) { 38 | query.receive(current.get()); 39 | // the interceptor will close the connection 40 | } else { 41 | try (Connection connection = fetchNewConnection()) { 42 | query.receive(connection); 43 | } 44 | } 45 | } 46 | 47 | Boolean hasActiveTransaction() { 48 | return currentTxConnection.get().isPresent(); 49 | } 50 | 51 | void startNewTransaction() { 52 | try { 53 | Connection connection = fetchNewConnection(); 54 | connection.setAutoCommit(false); 55 | currentTxConnection.set(Optional.of(connection)); 56 | } catch (SQLException e) { 57 | throw new FluentJdbcSqlException("Error initializing transaction", e); 58 | } 59 | } 60 | 61 | void commitActiveTransaction() { 62 | try { 63 | currentTxConnection.get().get().commit(); 64 | } catch (SQLException e) { 65 | throw new FluentJdbcSqlException("Error committing transaction", e); 66 | } 67 | } 68 | 69 | void rollbackActiveTransaction() { 70 | try { 71 | currentTxConnection.get().get().rollback(); 72 | } catch (SQLException e) { 73 | throw new FluentJdbcSqlException("Error rolling back transaction", e); 74 | } 75 | } 76 | 77 | void removeActiveTransactionConnection() { 78 | if (currentTxConnection.get().isPresent()) { 79 | try { 80 | currentTxConnection.get().get().close(); 81 | } catch (SQLException e) { 82 | throw new FluentJdbcSqlException("Error closing connection after transaction commit.", e); 83 | } 84 | currentTxConnection.set(Optional.empty()); 85 | } 86 | } 87 | 88 | private Connection fetchNewConnection() throws SQLException { 89 | ConnectionReceiver receiver = new ConnectionReceiver(); 90 | connectionProvider.provide(receiver); 91 | return receiver.connection(); 92 | } 93 | 94 | 95 | private static class ConnectionReceiver implements QueryConnectionReceiver { 96 | private Optional connection = Optional.empty(); 97 | 98 | @Override 99 | public void receive(Connection connection) { 100 | this.connection = Optional.of(connection); 101 | } 102 | 103 | Connection connection() { 104 | return connection.get(); 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/main/java/org/codejargon/fluentjdbc/api/integration/guicepersist/standalone/TransactionInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.standalone; 2 | 3 | import com.google.inject.persist.Transactional; 4 | import org.aopalliance.intercept.MethodInterceptor; 5 | import org.aopalliance.intercept.MethodInvocation; 6 | import org.codejargon.fluentjdbc.internal.support.Lists; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.function.Supplier; 11 | 12 | class TransactionInterceptor implements MethodInterceptor { 13 | private final StandaloneTxConnectionProvider standaloneTxConnectionProvider; 14 | 15 | TransactionInterceptor(StandaloneTxConnectionProvider standaloneTxConnectionProvider) { 16 | this.standaloneTxConnectionProvider = standaloneTxConnectionProvider; 17 | } 18 | 19 | public Object invoke(MethodInvocation methodInvocation) throws Throwable { 20 | Boolean newTransactionStarted = startNewTransactionIfNecessary(); 21 | try { 22 | return invokeMethodAndCommitIfNecessary(methodInvocation, newTransactionStarted); 23 | } catch (Exception e) { 24 | rollbackOrCommit(methodInvocation, e); 25 | throw e; 26 | } finally { 27 | if (newTransactionStarted) { 28 | standaloneTxConnectionProvider.removeActiveTransactionConnection(); 29 | } 30 | } 31 | } 32 | 33 | private Object invokeMethodAndCommitIfNecessary(MethodInvocation methodInvocation, Boolean newTransactionStarted) throws Throwable { 34 | Object result = methodInvocation.proceed(); 35 | if (newTransactionStarted) { 36 | standaloneTxConnectionProvider.commitActiveTransaction(); 37 | } 38 | return result; 39 | } 40 | 41 | private Boolean startNewTransactionIfNecessary() { 42 | final Boolean newTransactionStarted; 43 | if (!standaloneTxConnectionProvider.hasActiveTransaction()) { 44 | standaloneTxConnectionProvider.startNewTransaction(); 45 | newTransactionStarted = true; 46 | } else { 47 | newTransactionStarted = false; 48 | } 49 | return newTransactionStarted; 50 | } 51 | 52 | private void rollbackOrCommit(MethodInvocation methodInvocation, Exception e) { 53 | if (rollbackNecessary(e, transactional(methodInvocation))) { 54 | standaloneTxConnectionProvider.rollbackActiveTransaction(); 55 | } else { 56 | standaloneTxConnectionProvider.commitActiveTransaction(); 57 | } 58 | } 59 | 60 | private Transactional transactional(MethodInvocation methodInvocation) { 61 | return suppliers( 62 | () -> methodInvocation.getMethod().getAnnotation(Transactional.class), 63 | () -> methodInvocation.getThis().getClass().getAnnotation(Transactional.class), 64 | this::defaultTransactional 65 | ).stream() 66 | .map(Supplier::get) 67 | .filter(transactional-> transactional != null) 68 | .findFirst() 69 | .get(); 70 | } 71 | 72 | @SafeVarargs 73 | private static List> suppliers(Supplier... suppliers) { 74 | return Arrays.asList(suppliers); 75 | } 76 | 77 | private boolean rollbackNecessary(Exception cause, Transactional transactional) { 78 | return has(transactional.rollbackOn(), cause) && !has(transactional.ignore(), cause); 79 | } 80 | 81 | private Boolean has(Class[] exceptions, Exception cause) { 82 | return Lists.copyOf(exceptions).stream().filter(e -> e.isInstance(cause)).findAny().isPresent(); 83 | } 84 | 85 | private Transactional defaultTransactional() { 86 | return DefaultTransactionalDummy.class.getAnnotation(Transactional.class); 87 | } 88 | 89 | @Transactional 90 | private static class DefaultTransactionalDummy { 91 | private DefaultTransactionalDummy() { 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/main/resources/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Zsolt Herpai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/groovy/org/codejargon/fluentjdbc/api/integration/guicepersist/TransactionBreaking.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist; 2 | 3 | class TransactionBreaking extends RuntimeException { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/groovy/org/codejargon/fluentjdbc/api/integration/guicepersist/TransactionTestRoutine.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist; 2 | 3 | import com.google.inject.Injector; 4 | import org.codejargon.fluentjdbc.api.FluentJdbcException 5 | import org.codejargon.fluentjdbc.api.mapper.Mappers; 6 | import org.codejargon.fluentjdbc.api.query.Query; 7 | import org.junit.Before 8 | import spock.lang.Specification; 9 | 10 | import javax.inject.Inject 11 | 12 | abstract class TransactionTestRoutine extends Specification { 13 | protected abstract Injector injector() 14 | 15 | @Inject 16 | TransactionTestService testService 17 | @Inject 18 | Query query 19 | 20 | @Before 21 | def injectAndTableCreate() { 22 | injector().injectMembers(this); 23 | try { 24 | query.update("DROP TABLE DUMMY").run(); 25 | } catch (FluentJdbcException e) { 26 | // ignore 27 | } 28 | query.update("CREATE TABLE DUMMY (id VARCHAR(255) PRIMARY KEY)").run(); 29 | } 30 | 31 | def "No transaction"() { 32 | when: 33 | testService.noTransaction() 34 | then: 35 | countIs(1) 36 | } 37 | 38 | 39 | def "Transaction"() { 40 | when: 41 | testService.transaction() 42 | then: 43 | countIs(1) 44 | } 45 | 46 | def "Transaction breaking"() { 47 | when: 48 | try { 49 | testService.transactionBreaking() 50 | } catch (TransactionBreaking e) { 51 | // ignore 52 | } 53 | then: 54 | countIs(0) 55 | } 56 | 57 | def "Propagated transaction"() { 58 | when: 59 | testService.propagatedTransaction() 60 | then: 61 | countIs(2) 62 | } 63 | 64 | def propagatedTransactionBreaking() { 65 | when: 66 | try { 67 | testService.propagatedTransactionBreaking() 68 | } catch (TransactionBreaking e) { 69 | // ignore 70 | } 71 | then: 72 | countIs(0) 73 | } 74 | 75 | def propagatedTransactionOriginalBreaking() { 76 | when: 77 | try { 78 | testService.propagatedTransactionOriginalBreaking() 79 | } catch (TransactionBreaking e) { 80 | // ignore 81 | } 82 | then: 83 | countIs(0) 84 | } 85 | 86 | def rollbackRulesNoException() { 87 | when: 88 | testService.rollbackRulesNoException() 89 | then: 90 | countIs(1) 91 | } 92 | 93 | def rollbackRulesNonRollbackException() { 94 | when: 95 | try { 96 | testService.rollbackRulesNonRollbackException() 97 | } catch (UnsupportedOperationException e) { 98 | // ignore 99 | } 100 | then: 101 | countIs(1) 102 | 103 | } 104 | 105 | def rollbackRulesRollbackException() { 106 | when: 107 | try { 108 | testService.rollbackRulesRollbackException() 109 | } catch (IOException e) { 110 | // ignore 111 | } 112 | then: 113 | countIs(0); 114 | } 115 | 116 | def rollbackRulesIgnoredException() { 117 | when: 118 | try { 119 | testService.rollbackRulesIgnoredException() 120 | } catch (FileNotFoundException e) { 121 | // ignore 122 | } 123 | then: 124 | countIs(1); 125 | } 126 | 127 | void countIs(int i) { 128 | assert inserted() == i 129 | } 130 | 131 | def inserted() { 132 | return query.select("SELECT COUNT(*) FROM DUMMY").singleResult(Mappers.singleInteger()) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/groovy/org/codejargon/fluentjdbc/api/integration/guicepersist/TransactionTestService.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist; 2 | 3 | import com.google.inject.Inject; 4 | import com.google.inject.persist.Transactional 5 | import org.codejargon.fluentjdbc.api.query.Query; 6 | 7 | class TransactionTestService { 8 | final Query query 9 | final TransactionTestService2 testService2 10 | 11 | @Inject 12 | TransactionTestService(Query query, TransactionTestService2 testService2) { 13 | this.query = query 14 | this.testService2 = testService2 15 | } 16 | 17 | def noTransaction() { 18 | insert() 19 | } 20 | 21 | @Transactional 22 | def transaction() { 23 | insert() 24 | } 25 | 26 | @Transactional 27 | def transactionBreaking() { 28 | insert() 29 | throw new TransactionBreaking() 30 | } 31 | 32 | 33 | @Transactional 34 | def propagatedTransaction() { 35 | insert() 36 | testService2.transactional() 37 | } 38 | 39 | @Transactional 40 | def propagatedTransactionOriginalBreaking() { 41 | insert() 42 | testService2.transactional() 43 | throw new TransactionBreaking() 44 | } 45 | 46 | @Transactional 47 | def propagatedTransactionBreaking() { 48 | insert() 49 | testService2.transactionalBreaking() 50 | } 51 | 52 | @Transactional(rollbackOn = IOException.class, ignore = FileNotFoundException.class) 53 | def rollbackRulesNoException() { 54 | insert() 55 | } 56 | 57 | @Transactional(rollbackOn = IOException.class, ignore = FileNotFoundException.class) 58 | def rollbackRulesNonRollbackException() { 59 | insert() 60 | throw new UnsupportedOperationException() 61 | } 62 | 63 | @Transactional(rollbackOn = IOException.class, ignore = FileNotFoundException.class) 64 | def rollbackRulesRollbackException() { 65 | insert() 66 | throw new IOException() 67 | } 68 | 69 | @Transactional(rollbackOn = IOException.class, ignore = FileNotFoundException.class) 70 | def rollbackRulesIgnoredException() { 71 | insert() 72 | throw new FileNotFoundException() 73 | } 74 | 75 | def insert() { 76 | query.update("INSERT INTO DUMMY(id) VALUES(?)").params("1").run() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/groovy/org/codejargon/fluentjdbc/api/integration/guicepersist/TransactionTestService2.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist; 2 | 3 | import com.google.inject.Inject; 4 | import com.google.inject.persist.Transactional 5 | import org.codejargon.fluentjdbc.api.query.Query; 6 | 7 | public class TransactionTestService2 { 8 | private final Query query 9 | 10 | @Inject 11 | TransactionTestService2(Query query) { 12 | this.query = query 13 | } 14 | 15 | @Transactional 16 | def transactional() { 17 | insert() 18 | } 19 | 20 | @Transactional 21 | def transactionalBreaking() { 22 | insert() 23 | throw new TransactionBreaking() 24 | } 25 | 26 | def insert() { 27 | query.update("INSERT INTO DUMMY(id) VALUES(?)").params("2").run() 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/groovy/org/codejargon/fluentjdbc/api/integration/guicepersist/jpa/JpaTestInitialization.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.jpa; 2 | 3 | import com.google.inject.Guice; 4 | import com.google.inject.Injector; 5 | import com.google.inject.persist.PersistService; 6 | import com.google.inject.persist.jpa.JpaPersistModule; 7 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder; 8 | 9 | public class JpaTestInitialization { 10 | final Injector injector 11 | 12 | JpaTestInitialization( 13 | String persistenceUnit, 14 | JpaConnectionExtractor jpaConnectionExtractor 15 | ) { 16 | JpaFluentJdbcModule fluentJdbcModule = new JpaFluentJdbcModule( 17 | new FluentJdbcBuilder(), 18 | jpaConnectionExtractor 19 | ) 20 | injector = Guice.createInjector(jpaPersistModule(persistenceUnit), fluentJdbcModule) 21 | persistService().start() 22 | } 23 | 24 | Injector injector() { 25 | return injector 26 | } 27 | 28 | void stop() { 29 | persistService().stop() 30 | } 31 | 32 | JpaPersistModule jpaPersistModule(String persistenceUnit) { 33 | JpaPersistModule jpaPersistModule = new JpaPersistModule(persistenceUnit) 34 | jpaPersistModule.properties(PersistenceProperties.props) 35 | return jpaPersistModule 36 | } 37 | 38 | PersistService persistService() { 39 | return injector.getInstance(PersistService.class) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/groovy/org/codejargon/fluentjdbc/api/integration/guicepersist/jpa/PersistenceProperties.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.jpa; 2 | 3 | public class PersistenceProperties { 4 | public static final Properties props 5 | 6 | static { 7 | props = new Properties() 8 | props.putAll( 9 | "javax.persistence.jdbc.url": "jdbc:h2:mem:test/test;", 10 | "javax.persistence.jdbc.user": "sa", 11 | "javax.persistence.jdbc.password": "sa", 12 | "javax.persistence.jdbc.driver": "org.h2.Driver", 13 | ); 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/groovy/org/codejargon/fluentjdbc/api/integration/guicepersist/jpa/vendors/EclipseLinkTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.jpa.vendors; 2 | 3 | import com.google.inject.Injector; 4 | import org.codejargon.fluentjdbc.api.integration.guicepersist.TransactionTestRoutine; 5 | import org.codejargon.fluentjdbc.api.integration.guicepersist.jpa.JpaTestInitialization; 6 | import org.codejargon.fluentjdbc.integration.IntegrationTest; 7 | import org.junit.AfterClass; 8 | import org.junit.BeforeClass; 9 | import org.junit.experimental.categories.Category 10 | 11 | import java.sql.Connection; 12 | 13 | @Category(IntegrationTest.class) 14 | public class EclipseLinkTest extends TransactionTestRoutine { 15 | private static JpaTestInitialization jpaTestInitialization 16 | 17 | @BeforeClass 18 | public static void init() { 19 | jpaTestInitialization = new JpaTestInitialization( 20 | "EclipseLink", 21 | { em -> em.unwrap(Connection.class) } 22 | ) 23 | } 24 | 25 | @AfterClass 26 | public static void stopJpa() { 27 | jpaTestInitialization.stop() 28 | } 29 | 30 | @Override 31 | protected Injector injector() { 32 | return jpaTestInitialization.injector() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/groovy/org/codejargon/fluentjdbc/api/integration/guicepersist/standalone/StandaloneTransactionTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.guicepersist.standalone; 2 | 3 | import com.google.inject.Guice; 4 | import com.google.inject.Injector; 5 | import org.codejargon.fluentjdbc.api.integration.guicepersist.TransactionTestRoutine; 6 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder; 7 | import org.codejargon.fluentjdbc.integration.IntegrationTest; 8 | import org.h2.jdbcx.JdbcDataSource; 9 | import org.junit.BeforeClass; 10 | import org.junit.experimental.categories.Category; 11 | 12 | import javax.sql.DataSource; 13 | 14 | @Category(IntegrationTest.class) 15 | public class StandaloneTransactionTest extends TransactionTestRoutine { 16 | private static Injector injector 17 | 18 | @BeforeClass 19 | public static void initFluentJdbcAndTestService() { 20 | FluentJdbcBuilder fluentJdbc = new FluentJdbcBuilder() 21 | injector = Guice.createInjector(new StandaloneFluentJdbcModule(fluentJdbc, h2DataSource())) 22 | } 23 | 24 | private static DataSource h2DataSource() { 25 | Class.forName("org.h2.Driver").newInstance() 26 | JdbcDataSource h2Ds = new JdbcDataSource() 27 | h2Ds.setURL("jdbc:h2:mem:test/test;DB_CLOSE_DELAY=-1") 28 | h2Ds.setUser("sa") 29 | h2Ds.setPassword("sa") 30 | return h2Ds 31 | } 32 | 33 | @Override 34 | protected Injector injector() { 35 | return injector 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /extensions/fluent-jdbc-guice-persist/src/test/resources/META-INF/persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.eclipse.persistence.jpa.PersistenceProvider 5 | false 6 | 7 | -------------------------------------------------------------------------------- /extensions/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.codejargon 7 | fluentjdbc-parent 8 | 1.8.5 9 | 10 | fluentjdbc-extensions-parent 11 | Fluent-Jdbc Extensions Parent 12 | pom 13 | 14 | fluent-jdbc-guice-persist 15 | 16 | -------------------------------------------------------------------------------- /fluent-jdbc/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.codejargon 7 | fluentjdbc-parent 8 | 1.8.5 9 | 10 | fluentjdbc 11 | FluentJdbc Core 12 | 13 | 14 | com.h2database 15 | h2 16 | 1.4.184 17 | test 18 | 19 | 20 | org.apache.derby 21 | derby 22 | 10.11.1.1 23 | test 24 | 25 | 26 | org.hsqldb 27 | hsqldb 28 | 2.3.2 29 | test 30 | 31 | 32 | com.opentable.components 33 | otj-pg-embedded 34 | 0.13.3 35 | test 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-jar-plugin 43 | 44 | 45 | 46 | test-jar 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/FluentJdbc.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api; 2 | 3 | import org.codejargon.fluentjdbc.api.query.Query; 4 | 5 | import java.sql.Connection; 6 | 7 | /** 8 | * Creates fluent Query API based on FluentJdbc's configuration. Immutable, thread-safe. 9 | * 10 | * @see org.codejargon.fluentjdbc.api.FluentJdbcBuilder 11 | */ 12 | public interface FluentJdbc { 13 | /** 14 | * Creates a Query API on a connection provided by the ConnectionProvider. Fails if no ConnectionProvider is set. 15 | * @return Query API on a connection provided by the ConnectionProvider 16 | */ 17 | Query query(); 18 | 19 | /** 20 | * Creates a Query API using a given managed connection 21 | * 22 | * @param connection managed sql Connection 23 | * @return Query API for the given connection 24 | */ 25 | Query queryOn(Connection connection); 26 | } 27 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/FluentJdbcBuilder.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api; 2 | 3 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; 4 | import org.codejargon.fluentjdbc.api.integration.providers.DataSourceConnectionProvider; 5 | import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; 6 | import org.codejargon.fluentjdbc.api.query.Transaction; 7 | import org.codejargon.fluentjdbc.api.query.listen.AfterQueryListener; 8 | import org.codejargon.fluentjdbc.internal.FluentJdbcInternal; 9 | import org.codejargon.fluentjdbc.internal.query.DefaultSqlHandler; 10 | import org.codejargon.fluentjdbc.internal.query.QueryConfig; 11 | import org.codejargon.fluentjdbc.internal.support.Maps; 12 | 13 | import javax.sql.DataSource; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.function.Supplier; 18 | 19 | import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkArgument; 20 | import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkNotNull; 21 | 22 | /** 23 | * Configures and builds a FluentJdbc instance 24 | * 25 | * @see org.codejargon.fluentjdbc.api.FluentJdbc 26 | */ 27 | public class FluentJdbcBuilder { 28 | private Optional defaultFetchSize = Optional.empty(); 29 | private Optional defaultBatchSize = Optional.empty(); 30 | private Optional connectionProvider = Optional.empty(); 31 | private Optional afterQueryListener = Optional.empty(); 32 | private Optional defaultTransactionIsolation = Optional.empty(); 33 | private Supplier defaultSqlErrorHandler = DefaultSqlHandler::new; 34 | private Map paramSetters = Maps.copyOf(new HashMap<>()); 35 | 36 | public FluentJdbcBuilder() { 37 | 38 | } 39 | 40 | /** 41 | * Sets the ConnectionProvider for FluentJdbc. Queries created by fluentJdbc.query() will use 42 | * Connections returned by this provider 43 | * 44 | * @param connectionProvider ConnectionProvider implementation 45 | * @return this 46 | */ 47 | public FluentJdbcBuilder connectionProvider(ConnectionProvider connectionProvider) { 48 | this.connectionProvider = Optional.of(checkNotNull(connectionProvider, "connectionProvider")); 49 | return this; 50 | } 51 | 52 | public FluentJdbcBuilder connectionProvider(DataSource dataSource) { 53 | return connectionProvider(new DataSourceConnectionProvider(dataSource)); 54 | } 55 | 56 | /** 57 | * ParamSetters add support for accepting parameters of custom types in all queries (select/update/insert/batch) 58 | * These setters can also override types supported by FluentJdbc out of the box (JDBC-supported types, 59 | * java.util.Date, java.time) 60 | * @param paramSetters Map of parameter class / ParamSetters pairs. 61 | * @return this 62 | */ 63 | public FluentJdbcBuilder paramSetters(Map paramSetters) { 64 | this.paramSetters = checkNotNull(paramSetters, "paramSetters"); 65 | return this; 66 | } 67 | 68 | /** 69 | * Sets default fetch size of select statements - the number of rows returned with one network roundtrip 70 | * Vendor default is used if not set. Note that vendor defaults may be different, eg MySQL default 71 | * is 0 (no limit) which may lead to memory issues, Oracle DB's default is 10 which may result in poor 72 | * performance with large ResultSets. 73 | * @param rows Number of rows fetched by a select statement by default 74 | * @return this 75 | */ 76 | public FluentJdbcBuilder defaultFetchSize(Integer rows) { 77 | checkArgument(checkNotNull(rows, "rows") >= 0, "Fetch size rows must be >= 0"); 78 | this.defaultFetchSize = Optional.of(rows); 79 | return this; 80 | } 81 | 82 | /** 83 | * Sets default batch size for batch operations. Vendor default is used if not set. 84 | * 85 | * @param size Number of rows updated in a single batch 86 | * @return this 87 | */ 88 | public FluentJdbcBuilder defaultBatchSize(Integer size) { 89 | checkArgument(checkNotNull(size, "size") >= 0, "Batch size rows must be >= 0"); 90 | this.defaultBatchSize = Optional.of(size); 91 | return this; 92 | } 93 | 94 | /** 95 | * Overrides default transaction isolation imposed by the jdbc driver or the database engine 96 | * @param isolation Default isolation 97 | * @return this 98 | */ 99 | public FluentJdbcBuilder defaultTransactionIsolation(Transaction.Isolation isolation) { 100 | this.defaultTransactionIsolation = Optional.of(checkNotNull(isolation, "isolation")); 101 | return this; 102 | } 103 | 104 | /** 105 | * Sets a default / global sql handler. The supplier will be called to fetch an error handler instance for each query execution. 106 | * 107 | * @param sqlErrorHandler supplier of sql error handlers. 108 | * @return this 109 | */ 110 | public FluentJdbcBuilder defaultSqlHandler(Supplier sqlErrorHandler) { 111 | this.defaultSqlErrorHandler = sqlErrorHandler; 112 | return this; 113 | } 114 | 115 | public FluentJdbcBuilder afterQueryListener(AfterQueryListener afterQueryListener) { 116 | this.afterQueryListener = Optional.of(afterQueryListener); 117 | return this; 118 | } 119 | 120 | /** 121 | * Returns a FluentJdbc instance configured by the builder 122 | * @return FluentJdbc instance 123 | */ 124 | public FluentJdbc build() { 125 | return new FluentJdbcInternal( 126 | connectionProvider, 127 | new QueryConfig( 128 | defaultFetchSize, 129 | defaultBatchSize, 130 | Maps.copyOf(paramSetters), 131 | afterQueryListener, 132 | defaultTransactionIsolation, 133 | defaultSqlErrorHandler 134 | ) 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/FluentJdbcException.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api; 2 | 3 | /** 4 | * Base exception for exceptions thrown by FluentJdbc 5 | */ 6 | public class FluentJdbcException extends RuntimeException { 7 | public FluentJdbcException(String message) { 8 | super(message); 9 | } 10 | 11 | public FluentJdbcException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/FluentJdbcSqlException.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api; 2 | 3 | import java.sql.SQLException; 4 | 5 | /** 6 | * A runtime Exception that wraps all SQLExceptions thrown by the underlying JDBC API 7 | */ 8 | public class FluentJdbcSqlException extends FluentJdbcException { 9 | public FluentJdbcSqlException(String message, SQLException sqlException) { 10 | super(message, sqlException); 11 | } 12 | 13 | public SQLException sqlException() { 14 | return (SQLException) getCause(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/ParamSetter.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api; 2 | 3 | import java.sql.PreparedStatement; 4 | import java.sql.SQLException; 5 | 6 | /** 7 | *

Plugin for setting parameters of custom types (eg. Joda time, etc..)

8 | * 9 | * Note: Support for java.time is implemented by Fluent-Jdbc by default 10 | */ 11 | @FunctionalInterface 12 | public interface ParamSetter { 13 | void set(T param, PreparedStatement statement, Integer index) throws SQLException; 14 | } 15 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/integration/ConnectionProvider.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration; 2 | 3 | import java.sql.SQLException; 4 | 5 | /** 6 | *

7 | * Provides Connections to FluentJdbc Queries. It allows both acquiring and releasing a Connection. 8 | * This makes it possible to integrate FluentJdbc to most pooling / transaction management solutions. 9 | *

10 | * 11 | *

Implementation pattern

12 | *
13 |  * query -> {
14 |  *     Connection connection = ... // acquire a connection 
15 |  *     query.receive(connection)   // make the connection available to FluentJdbc queries
16 |  *     connection.close()          // release connection - may not be needed if connection is already managed
17 |  * }         
18 |  * 
19 | * 20 | *

21 | * Example implementations: 22 | *

23 | * 24 | *

25 | * Getting connection from a datasource (provided in the library as DataSourceConnectionProvider): 26 | *

27 | * 28 | *
29 |  * query -> {
30 |  *      try(Connection connection = dataSource.getConnection()) {
31 |  *          query.receive(connection);
32 |  *      }
33 |  *   }
34 |  * 
35 | * 36 | *

37 | * Getting connection from a callback mechanism (eg Spring JdbcOperations/JdbcTemplate): 38 | *

39 | * 40 | *
41 |  * query -> {
42 |  *     jdbcTemplate.execute(connection -> {
43 |  *        query.receive(connection);
44 |  *     });
45 |  * }
46 |  * 
47 | * 48 | */ 49 | @FunctionalInterface 50 | public interface ConnectionProvider { 51 | void provide(QueryConnectionReceiver query) throws SQLException; 52 | } 53 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/integration/QueryConnectionReceiver.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration; 2 | 3 | import java.sql.Connection; 4 | import java.sql.SQLException; 5 | 6 | /** 7 | * Fluent-Jdbc queries receive the connections from this interface. Should be called in ConnectionProvider implementations, 8 | * no need to implement it for integrations. 9 | */ 10 | @FunctionalInterface 11 | public interface QueryConnectionReceiver { 12 | void receive(Connection connection) throws SQLException; 13 | } 14 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/integration/providers/DataSourceConnectionProvider.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.integration.providers; 2 | 3 | import org.codejargon.fluentjdbc.api.integration.QueryConnectionReceiver; 4 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; 5 | 6 | import javax.sql.DataSource; 7 | import java.sql.Connection; 8 | import java.sql.SQLException; 9 | 10 | /** 11 | * ConnectionProvider based on a DataSource. 12 | */ 13 | public class DataSourceConnectionProvider implements ConnectionProvider { 14 | private final DataSource dataSource; 15 | 16 | public DataSourceConnectionProvider(DataSource dataSource) { 17 | this.dataSource = dataSource; 18 | } 19 | 20 | @Override 21 | public void provide(QueryConnectionReceiver query) throws SQLException { 22 | try(Connection connection = dataSource.getConnection()) { 23 | query.receive(connection); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/mapper/Mappers.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.mapper; 2 | 3 | import org.codejargon.fluentjdbc.api.query.Mapper; 4 | 5 | import java.math.BigDecimal; 6 | import java.sql.Blob; 7 | import java.sql.ResultSetMetaData; 8 | import java.util.Collections; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | *

A set of common mappers for convenience.

14 | * @see org.codejargon.fluentjdbc.api.mapper.ObjectMappers 15 | */ 16 | public abstract class Mappers { 17 | private static final Mapper singleInteger = (rs) -> rs.getInt(1); 18 | private static final Mapper singleLong = (rs) -> rs.getLong(1); 19 | private static final Mapper singleString = (rs) -> rs.getString(1); 20 | private static final Mapper singleBigDecimal = (rs) -> rs.getBigDecimal(1); 21 | private static final Mapper singleBoolean = (rs) -> rs.getBoolean(1); 22 | private static final Mapper> map = rs -> { 23 | ResultSetMetaData meta = rs.getMetaData(); 24 | Map result = new LinkedHashMap<>(meta.getColumnCount()); 25 | for(int column = 1; column <= meta.getColumnCount(); ++column) { 26 | result.put(meta.getColumnLabel(column), rs.getObject(column)); 27 | } 28 | return Collections.unmodifiableMap(result); 29 | }; 30 | private static final Mapper singleByteArray = (rs) -> { 31 | Blob blob = rs.getBlob(1); 32 | return blob.getBytes(1, (int) blob.length()); 33 | }; 34 | 35 | 36 | /** 37 | * Maps the first Integer column. 38 | * @return first Integer column 39 | */ 40 | public static Mapper singleInteger() { 41 | return singleInteger; 42 | } 43 | 44 | /** 45 | * Maps the first Long column. 46 | * @return first Long column 47 | */ 48 | public static Mapper singleLong() { 49 | return singleLong; 50 | } 51 | 52 | /** 53 | * Maps the first string column 54 | * @return first string column 55 | */ 56 | public static Mapper singleString() { 57 | return singleString; 58 | } 59 | 60 | /** 61 | * Maps the first BigDecimal column 62 | * @return first BigDecimal column 63 | */ 64 | public static Mapper singleBigDecimal() { 65 | return singleBigDecimal; 66 | } 67 | 68 | /** 69 | * Maps the first Boolean column 70 | * @return first Boolean column 71 | */ 72 | public static Mapper singleBoolean() { 73 | return singleBoolean; 74 | } 75 | 76 | /** 77 | * 78 | * @return 79 | */ 80 | public static Mapper> map() { 81 | return map; 82 | } 83 | 84 | public static Mapper singleByteArray() { 85 | return singleByteArray; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/mapper/ObjectMapperRsExtractor.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.mapper; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | /** 7 | *

Plug-in for supporting custom types in ObjectMappers

8 | * @see org.codejargon.fluentjdbc.api.mapper.ObjectMappers 9 | */ 10 | @FunctionalInterface 11 | public interface ObjectMapperRsExtractor { 12 | /** 13 | * Extracts an object from a ResultSet and converts it to the object of target class. 14 | * @param resultSet ResultSet containing the current row. 15 | * @param index column index in the ResultSet 16 | * @return Object of the required class - nullable 17 | * @throws SQLException 18 | */ 19 | T extract(ResultSet resultSet, Integer index) throws SQLException; 20 | } 21 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/mapper/ObjectMappers.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.mapper; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.function.Function; 8 | 9 | import org.codejargon.fluentjdbc.internal.mappers.ObjectMapper; 10 | import org.codejargon.fluentjdbc.api.query.Mapper; 11 | import org.codejargon.fluentjdbc.internal.mappers.DefaultObjectMapperRsExtractors; 12 | import org.codejargon.fluentjdbc.internal.support.Maps; 13 | 14 | /** 15 | *

Constructs Mappers for mapping a ResultSet row into a plain java object based on object field vs ResultSet column 16 | * match. Default matching is case insensitive and ignores '_' characters in database fields. The target class must have 17 | * a no-arg constructor, fields can be private, no need for accessors.

18 | * 19 | *

In addition to JDBC types, supports java.time types out of the box. Able to support any custom type.

20 | * 21 | * @see org.codejargon.fluentjdbc.api.query.Mapper 22 | */ 23 | public class ObjectMappers { 24 | private final Map extractors; 25 | private final Map> mappers; 26 | private final Function converter; 27 | 28 | private ObjectMappers(Map extractors, Function converter) { 29 | this.extractors = Maps.merge(DefaultObjectMapperRsExtractors.extractors(), extractors); 30 | this.converter = converter; 31 | mappers = new ConcurrentHashMap<>(); 32 | } 33 | 34 | @SuppressWarnings("unchecked") 35 | public Mapper forClass(Class clazz) { 36 | return (Mapper) mappers.computeIfAbsent(clazz, c -> new ObjectMapper<>(c, extractors, converter)); 37 | } 38 | 39 | public static Builder builder() { 40 | return new Builder(); 41 | } 42 | 43 | public static class Builder { 44 | private Map extractors = Collections.emptyMap(); 45 | private Function converter = f -> f.toLowerCase().replace("_", ""); 46 | private Builder() { 47 | 48 | } 49 | 50 | /** 51 | * Sets extractors to support custom (non-JDBC) fields. 52 | * @param extractors Map of field class / ResultSet extractors 53 | * @return this 54 | */ 55 | public Builder extractors(Map extractors) { 56 | this.extractors = Maps.copyOf(extractors); 57 | return this; 58 | } 59 | 60 | /** 61 | * Sets custom converter for object field to ResultSet column matching. 62 | * @param converter function to convert field names 63 | * @return this 64 | */ 65 | public Builder fieldNameConverter(Function converter) { 66 | this.converter = converter; 67 | return this; 68 | } 69 | 70 | public ObjectMappers build() { 71 | return new ObjectMappers(Maps.copyOf(extractors), converter); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/BatchQuery.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.util.Iterator; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.stream.Stream; 7 | 8 | /** 9 | * Batch insert or update query for a SQL statement. Is a mutable object. 10 | */ 11 | public interface BatchQuery { 12 | /** 13 | * 14 | * @param params Parameters used by the batch update 15 | * @return this 16 | */ 17 | BatchQuery params(Iterator> params); 18 | 19 | /** 20 | * 21 | * @param params Parameters used by the batch update 22 | * @return this 23 | */ 24 | BatchQuery params(Iterable> params); 25 | 26 | /** 27 | * 28 | * @param params Parameters used by the batch update 29 | * @return this 30 | */ 31 | BatchQuery params(Stream> params); 32 | 33 | /** 34 | * 35 | * @param params Parameters used by the batch update 36 | * @return this 37 | */ 38 | BatchQuery namedParams(Iterator> params); 39 | 40 | /** 41 | * 42 | * @param params Parameters used by the batch update 43 | * @return this 44 | */ 45 | BatchQuery namedParams(Iterable> params); 46 | 47 | /** 48 | * 49 | * @param params Parameters used by the batch update 50 | * @return this 51 | */ 52 | BatchQuery namedParams(Stream> params); 53 | 54 | /** 55 | * Sets size of a batch (database roundtrip) 56 | * @param batchSize size of a batch 57 | * @return this 58 | */ 59 | BatchQuery batchSize(Integer batchSize); 60 | 61 | /** 62 | * Sets a custom error handler 63 | * 64 | * @param sqlErrorHandler 65 | * @return this 66 | */ 67 | BatchQuery errorHandler(SqlErrorHandler sqlErrorHandler); 68 | 69 | /** 70 | * Runs the batch insert or update and returns the results (eg affected rows) 71 | * 72 | * @return List of update results 73 | */ 74 | List run(); 75 | 76 | /** 77 | * Runs the batch insert or update and returns the results (eg affected rows) 78 | * 79 | * @return List of update results 80 | */ 81 | List> runFetchGenKeys(Mapper generatedKeyMapper); 82 | } 83 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/Mapper.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | /** 7 | * Maps a row of a ResultSet to an object 8 | * @param target class 9 | */ 10 | @FunctionalInterface 11 | public interface Mapper { 12 | /** 13 | * 14 | * @param rs ResultSet containing data of a table row. ResultSet should not be mutated in a Mapper implementation. 15 | * @return result object 16 | * @throws SQLException a Mapper is allowed to throw SQLException for implementation convenience 17 | */ 18 | T map(ResultSet rs) throws SQLException; 19 | } 20 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/PlainConnectionQuery.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.sql.Connection; 4 | import java.sql.SQLException; 5 | 6 | @FunctionalInterface 7 | public interface PlainConnectionQuery { 8 | T operation(Connection con) throws SQLException; 9 | } 10 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/Query.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.inspection.DatabaseInspection; 4 | 5 | import java.sql.Connection; 6 | import java.sql.SQLException; 7 | import java.util.function.Function; 8 | 9 | /** 10 | *

FluentJdbc Query API to create select, update/insert, and batch update/insert queries. Immutable, thread-safe.

11 | * @see org.codejargon.fluentjdbc.api.integration.ConnectionProvider 12 | */ 13 | public interface Query { 14 | /** 15 | * Creates a select query for a SQL statement 16 | * 17 | * @param sql SQL statement 18 | * @return Select query for the SQL statement 19 | */ 20 | SelectQuery select(String sql); 21 | 22 | /** 23 | * Creates an update or insert query for a SQL statement 24 | * 25 | * @param sql SQL statement 26 | * @return Update query for the SQL statement 27 | */ 28 | UpdateQuery update(String sql); 29 | 30 | /** 31 | * Creates a batch update or insert query for a SQL statement 32 | * 33 | * @param sql SQL statement 34 | * @return Batch update or insert query for the SQL statement 35 | */ 36 | BatchQuery batch(String sql); 37 | 38 | /** 39 | * Transaction control 40 | * 41 | * @return Transaction control 42 | */ 43 | Transaction transaction(); 44 | 45 | /** 46 | * Provides access to a JDBC Connection managed by FluentJdbc for low level operations 47 | * 48 | * @param plainConnectionQuery operation on a managed connection 49 | * @param type of return value 50 | * @return value 51 | */ 52 | T plainConnection(PlainConnectionQuery plainConnectionQuery); 53 | 54 | /** 55 | * Inspection of the database (tables, columns, etc..) 56 | * 57 | * @return databaseInspection 58 | */ 59 | DatabaseInspection databaseInspection(); 60 | } 61 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/SelectQuery.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.sql.ResultSet; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.Set; 8 | import java.util.function.Consumer; 9 | import java.util.function.Predicate; 10 | 11 | /** 12 | * Select query for a SQL statement. A SelectQuery is mutable, non-threadsafe. 13 | */ 14 | public interface SelectQuery { 15 | /** 16 | * Adds positional query parameters. 17 | * 18 | * @param params additional query parameters 19 | * @return this 20 | */ 21 | SelectQuery params(List params); 22 | 23 | /** 24 | * Adds positional query parameters. 25 | * 26 | * @param params additional query parameters 27 | * @return this 28 | */ 29 | SelectQuery params(Object... params); 30 | 31 | /** 32 | * Adds named query paramaters. 33 | * 34 | * @param namedParams additional named query parameters 35 | * @return this 36 | */ 37 | SelectQuery namedParams(Map namedParams); 38 | 39 | /** 40 | * Adds a named query parameter 41 | * 42 | * @param name name of parameter 43 | * @param parameter value of parameter 44 | * @return this 45 | */ 46 | SelectQuery namedParam(String name, Object parameter); 47 | 48 | 49 | /** 50 | * Sets a result filter. Only results accepted by this Predicate will be returned by SelectQuery. 51 | * 52 | * @param predicate filter 53 | * @param result type 54 | * @return this 55 | */ 56 | SelectQuery filter(Predicate predicate); 57 | 58 | /** 59 | * Sets fetch size of select statements - the number of rows returned in a single network round-trip. 60 | * FluentJdbc configured default or vendor default is used if not set. Note that vendor defaults 61 | * may be different. Eg MySQL default is 0 (no limit) which may lead to memory issues, Oracle DB's default 62 | * is 10 which may result in poor performance with large ResultSets. 63 | * @param rows Number of rows fetched by a select statement. 64 | * @return this 65 | */ 66 | SelectQuery fetchSize(Integer rows); 67 | 68 | /** 69 | * Limits the number of rows returned by the database for a select statement. 70 | * If rows < Integer.MAX_VALUE, JDBC driver must support setLargeMaxRows() 71 | * @param rows number of rows 72 | * @return this 73 | */ 74 | SelectQuery maxRows(Long rows); 75 | 76 | /** 77 | * Sets a custom error handler 78 | * 79 | * @param sqlErrorHandler 80 | * @return this 81 | */ 82 | SelectQuery errorHandler(SqlErrorHandler sqlErrorHandler); 83 | 84 | /** 85 | * Runs the select query and returns first result - if any 86 | * 87 | * @param mapper ResultSet mapper 88 | * @param result type 89 | * @return Optional of the result (if there is one) 90 | */ 91 | Optional firstResult(Mapper mapper); 92 | 93 | /** 94 | * Runs the select query and returns a single result. 95 | * 96 | * @param mapper ResultSet mapper 97 | * @param result type 98 | * @return exactly one result 99 | * @throws org.codejargon.fluentjdbc.api.FluentJdbcException if no result found 100 | */ 101 | T singleResult(Mapper mapper); 102 | 103 | /** 104 | * Runs the select query and returns results as an immutable list 105 | * 106 | * @param mapper ResultSet mapper 107 | * @param result type 108 | * @return immutable List of results 109 | */ 110 | List listResult(Mapper mapper); 111 | 112 | /** 113 | * Runs the select query and returns results as an immutable set 114 | * 115 | * @param mapper ResultSet mapper 116 | * @param result type 117 | * @return immutable Set of results 118 | */ 119 | Set setResult(Mapper mapper); 120 | 121 | /** 122 | * Runs the select query and provides resultset to the given consumer 123 | * 124 | * @param consumer Consumer accepting the ResultSet 125 | */ 126 | void iterateResult(SqlConsumer consumer); 127 | 128 | /** 129 | * Runs the select query and provides results to the given consumer 130 | * 131 | * @param mapper ResultSet mapper 132 | * @param consumer Consumer accepting the results 133 | * @param result type 134 | */ 135 | void iterateResult(Mapper mapper, Consumer consumer); 136 | 137 | 138 | } 139 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/SqlConsumer.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.sql.SQLException; 4 | 5 | @FunctionalInterface 6 | public interface SqlConsumer { 7 | void accept(T t) throws SQLException; 8 | } 9 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/SqlErrorHandler.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.sql.SQLException; 4 | import java.util.Optional; 5 | 6 | public interface SqlErrorHandler { 7 | enum Action { 8 | RETRY 9 | } 10 | 11 | /** 12 | * Handles SQL errors, may implement logging, etc. The handler should always rethrow an exception in case of 13 | * a critical error. Otherwise retry action can be triggered. The handler is responsible to implement 14 | * delay or limitations for the retry. 15 | * 16 | * @param e the error 17 | * @param sql The sql query. Always present unless the error was thrown by direct plainConnection() usage. 18 | * @return In case no exception is thrown, otherwise action needs to be returned ( eg retry ). 19 | */ 20 | Action handle(SQLException e, Optional sql) throws SQLException; 21 | } 22 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/SqlErrorHandling.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | public interface SqlErrorHandling { 4 | T errorHandler(); 5 | } 6 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/Transaction.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.sql.Connection; 4 | import java.util.function.Supplier; 5 | 6 | public interface Transaction { 7 | /** 8 | * Specifies transaction isolation 9 | * @param isolation transaction isolation level - must be supported by the JDBC driver 10 | * @return this 11 | */ 12 | Transaction isolation(Isolation isolation); 13 | 14 | /** 15 | * Runs the function in a transaction, returns a result. 16 | * @param operation the operation to be executed in a transaction, returns a result 17 | * @param type of result 18 | * @return result 19 | */ 20 | T in(Supplier operation); 21 | 22 | /** 23 | * Runs the specified Runnable in a transaction. No return result. 24 | * @param runnable Will be executed in transaction. 25 | */ 26 | void inNoResult(Runnable runnable); 27 | 28 | enum Isolation { 29 | NONE(Connection.TRANSACTION_NONE), 30 | READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED), 31 | READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED), 32 | REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ), 33 | SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE); 34 | 35 | private final Integer jdbcIsolation; 36 | 37 | Isolation(Integer jdbcIsolation) { 38 | this.jdbcIsolation = jdbcIsolation; 39 | } 40 | 41 | public Integer jdbcIsolation() { 42 | return jdbcIsolation; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/UpdateQuery.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | /** 7 | * Update or insert Query for a SQL statement. An UpdateQuery is mutable. 8 | */ 9 | public interface UpdateQuery { 10 | /** 11 | * Adds positional query parameters 12 | * 13 | * @param params additional query parameters 14 | * @return this 15 | */ 16 | UpdateQuery params(List params); 17 | 18 | /** 19 | * Adds positional query parameters 20 | * 21 | * @param params additional query parameters 22 | * @return this 23 | */ 24 | UpdateQuery params(Object... params); 25 | 26 | /** 27 | * Adds named query paramaters 28 | * 29 | * @param namedParams additional named query parameters 30 | * @return this 31 | */ 32 | UpdateQuery namedParams(Map namedParams); 33 | 34 | /** 35 | * Adds a named query parameter 36 | * 37 | * @param name name of parameter 38 | * @param parameter value of parameter 39 | * @return this 40 | */ 41 | UpdateQuery namedParam(String name, Object parameter); 42 | 43 | /** 44 | * Sets custom error handler 45 | * 46 | * @param sqlErrorHandler 47 | * @return this 48 | */ 49 | UpdateQuery errorHandler(SqlErrorHandler sqlErrorHandler); 50 | 51 | /** 52 | * Runs the update query 53 | * 54 | * @return result of the update (eg affected rows) 55 | */ 56 | UpdateResult run(); 57 | 58 | /** 59 | * Runs the update query and fetches the generated keys 60 | * 61 | * @param generatedKeyMapper maps generated key(s) to an object 62 | * @param type of a single key or an object containing multiple keys 63 | * @return result of the update including generated keys 64 | */ 65 | UpdateResultGenKeys runFetchGenKeys(Mapper generatedKeyMapper); 66 | 67 | /** 68 | * Runs the update query and fetches the generated keys 69 | * 70 | * @param generatedKeyMapper maps generated key(s) to an object 71 | * @param type of a single key or an object containing multiple keys 72 | * @param columns names of columns containing generated values. mandatory using some vendors, like oracle 73 | * @return result of the update including generated keys 74 | */ 75 | UpdateResultGenKeys runFetchGenKeys(Mapper generatedKeyMapper, String[] columns); 76 | 77 | } 78 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/UpdateResult.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | /** 4 | * Results of an update / insert 5 | */ 6 | public interface UpdateResult { 7 | long affectedRows(); 8 | } 9 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/UpdateResultGenKeys.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | /** 7 | * Result of an update / insert including generated keys 8 | * @param Type of generated key(s) for a single row inserted 9 | */ 10 | public interface UpdateResultGenKeys extends UpdateResult { 11 | /** 12 | * @return generated key(s) for each row inserted by the statement 13 | */ 14 | List generatedKeys(); 15 | Optional firstKey(); 16 | } 17 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/inspection/DatabaseInspection.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query.inspection; 2 | 3 | /** 4 | * Inspection of the Database schema 5 | */ 6 | public interface DatabaseInspection { 7 | /** 8 | * Provides access to a DatabaseMetaData instance 9 | * @see java.sql.DatabaseMetaData 10 | * 11 | * @param access Callback with access to DatabaseMetaData 12 | * @param Return object type 13 | * @return Information extracted from DatabaseMetaData 14 | */ 15 | T accessMetaData(MetaDataAccess access); 16 | 17 | /** 18 | * Selects from DatabaseMetaData - for methods returning a ResultSet 19 | * @see java.sql.DatabaseMetaData 20 | * @see java.sql.ResultSet 21 | * 22 | * @param select Function calling a method on DatabaseMetaData that returns ResultSet 23 | * @return MetaData select 24 | */ 25 | MetaDataSelect selectFromMetaData(MetaDataResultSet select); 26 | } 27 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/inspection/MetaDataAccess.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query.inspection; 2 | 3 | import java.sql.DatabaseMetaData; 4 | import java.sql.SQLException; 5 | 6 | @FunctionalInterface 7 | public interface MetaDataAccess { 8 | T access(DatabaseMetaData meta) throws SQLException; 9 | } 10 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/inspection/MetaDataResultSet.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query.inspection; 2 | 3 | 4 | import java.sql.DatabaseMetaData; 5 | import java.sql.ResultSet; 6 | import java.sql.SQLException; 7 | 8 | @FunctionalInterface 9 | public interface MetaDataResultSet { 10 | ResultSet select(DatabaseMetaData metaData) throws SQLException; 11 | } 12 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/inspection/MetaDataSelect.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query.inspection; 2 | 3 | import org.codejargon.fluentjdbc.api.query.Mapper; 4 | 5 | import java.util.List; 6 | 7 | public interface MetaDataSelect { 8 | /** 9 | * Provides results as a list 10 | * 11 | * @param mapper ResultSet mapper 12 | * @param Target object type 13 | * @return List of results 14 | */ 15 | List listResult(Mapper mapper); 16 | } 17 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/AfterQueryListener.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query.listen; 2 | 3 | /** 4 | * This listener callback will be called after each SQL operation made through FluentJdbc. 5 | */ 6 | @FunctionalInterface 7 | public interface AfterQueryListener { 8 | void listen(ExecutionDetails executionDetails); 9 | } 10 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/ExecutionDetails.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.api.query.listen; 2 | 3 | import java.sql.SQLException; 4 | import java.util.Optional; 5 | 6 | public interface ExecutionDetails { 7 | Boolean success(); 8 | String sql(); 9 | Long executionTimeMs(); 10 | Optional sqlException(); 11 | } 12 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/FluentJdbcInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal; 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbc; 4 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 5 | import org.codejargon.fluentjdbc.api.ParamSetter; 6 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; 7 | import org.codejargon.fluentjdbc.api.query.Query; 8 | import org.codejargon.fluentjdbc.api.query.Transaction; 9 | import org.codejargon.fluentjdbc.api.query.listen.AfterQueryListener; 10 | import org.codejargon.fluentjdbc.internal.query.QueryConfig; 11 | import org.codejargon.fluentjdbc.internal.query.QueryInternal; 12 | 13 | import java.sql.Connection; 14 | import java.util.Map; 15 | import java.util.Optional; 16 | 17 | public class FluentJdbcInternal implements FluentJdbc { 18 | 19 | private final Optional connectionProvider; 20 | private final QueryConfig queryConfig; 21 | 22 | public FluentJdbcInternal( 23 | Optional connectionProvider, 24 | QueryConfig queryConfig 25 | ) { 26 | this.connectionProvider = connectionProvider; 27 | this.queryConfig = queryConfig; 28 | } 29 | 30 | @Override 31 | public Query query() { 32 | if (!connectionProvider.isPresent()) { 33 | throw new FluentJdbcException("ConnectionProvider is not set."); 34 | } 35 | return new QueryInternal(connectionProvider.get(), queryConfig); 36 | } 37 | 38 | 39 | 40 | @Override 41 | public Query queryOn(Connection connection) { 42 | return new QueryInternal( 43 | query -> query.receive(connection), 44 | queryConfig 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/integration/QueryConnectionReceiverInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.integration; 2 | 3 | import org.codejargon.fluentjdbc.api.integration.QueryConnectionReceiver; 4 | import org.codejargon.fluentjdbc.internal.query.QueryRunnerConnection; 5 | 6 | import java.sql.Connection; 7 | import java.sql.SQLException; 8 | 9 | public class QueryConnectionReceiverInternal implements QueryConnectionReceiver { 10 | private final QueryRunnerConnection runner; 11 | private T returnValue; 12 | 13 | public QueryConnectionReceiverInternal(QueryRunnerConnection runner) { 14 | this.runner = runner; 15 | } 16 | 17 | @Override 18 | public void receive(Connection connection) throws SQLException { 19 | returnValue = runner.run(connection); 20 | } 21 | 22 | public T returnValue() { 23 | return returnValue; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/mappers/DefaultObjectMapperRsExtractors.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.mappers; 2 | 3 | import java.math.BigDecimal; 4 | import java.nio.ByteBuffer; 5 | import java.sql.*; 6 | import java.time.Instant; 7 | import java.time.LocalDate; 8 | import java.time.LocalDateTime; 9 | import java.time.LocalTime; 10 | import java.time.Year; 11 | import java.time.YearMonth; 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import org.codejargon.fluentjdbc.api.mapper.ObjectMapperRsExtractor; 17 | 18 | public class DefaultObjectMapperRsExtractors { 19 | private static boolean attemptBlobFree = true; 20 | private static final Map extractors; 21 | 22 | static { 23 | Map> exs = new HashMap<>(); 24 | basicTypes(exs); 25 | javaDate(exs); 26 | javaTimeTypes(exs); 27 | binaryTypes(exs); 28 | extractors = Collections.unmodifiableMap(exs); 29 | } 30 | 31 | private static void javaDate(Map> exs) { 32 | reg(exs, java.util.Date.class, ResultSet::getTimestamp); 33 | } 34 | 35 | private static void javaTimeTypes(Map> exs) { 36 | reg(exs, LocalDate.class, (rs, i) -> { 37 | Date date = rs.getDate(i); 38 | return date != null ? date.toLocalDate() : null; 39 | }); 40 | reg(exs, LocalDateTime.class, (rs, i) -> { 41 | Timestamp stamp = rs.getTimestamp(i); 42 | return stamp != null ? stamp.toLocalDateTime() : null; 43 | }); 44 | reg(exs, LocalTime.class, (rs, i) -> { 45 | Time time = rs.getTime(i); 46 | return time != null ? time.toLocalTime() : null; 47 | }); 48 | reg(exs, Year.class, (rs, i) -> { 49 | Date date = rs.getDate(i); 50 | return date != null ? Year.from(date.toLocalDate()) : null; 51 | }); 52 | reg(exs, YearMonth.class, (rs, i) -> { 53 | Date date = rs.getDate(i); 54 | return date != null ? YearMonth.from(date.toLocalDate()) : null; 55 | }); 56 | reg(exs, Instant.class, (rs, i) -> { 57 | Timestamp stamp = rs.getTimestamp(i); 58 | return stamp != null ? stamp.toInstant() : null; 59 | }); 60 | } 61 | 62 | private static void basicTypes(Map> exs) { 63 | reg(exs, Boolean.class, ResultSet::getBoolean); 64 | reg(exs, boolean.class, ResultSet::getBoolean); 65 | reg(exs, Short.class, ResultSet::getShort); 66 | reg(exs, short.class, ResultSet::getShort); 67 | reg(exs, Integer.class, ResultSet::getInt); 68 | reg(exs, int.class, ResultSet::getInt); 69 | reg(exs, Long.class, ResultSet::getLong); 70 | reg(exs, long.class, ResultSet::getLong); 71 | reg(exs, Float.class, ResultSet::getFloat); 72 | reg(exs, float.class, ResultSet::getFloat); 73 | reg(exs, Double.class, ResultSet::getDouble); 74 | reg(exs, double.class, ResultSet::getDouble); 75 | reg(exs, BigDecimal.class, ResultSet::getBigDecimal); 76 | reg(exs, Timestamp.class, ResultSet::getTimestamp); 77 | reg(exs, Time.class, ResultSet::getTime); 78 | reg(exs, Date.class, ResultSet::getDate); 79 | reg(exs, String.class, ResultSet::getString); 80 | } 81 | 82 | private static void binaryTypes(Map> exs) { 83 | reg(exs, byte[].class, (rs, i) -> rs.getBytes(i)); 84 | 85 | reg(exs, ByteBuffer.class, (rs, i) -> { 86 | Blob blob = rs.getBlob(i); 87 | if (blob == null) { 88 | return null; 89 | } 90 | ByteBuffer data = ByteBuffer.wrap(blob.getBytes(1, (int) blob.length())); 91 | freeBlob(blob); 92 | return data; 93 | }); 94 | } 95 | 96 | private static void freeBlob(Blob blob) throws SQLException { 97 | if(attemptBlobFree) { 98 | try { 99 | blob.free(); 100 | } catch (SQLFeatureNotSupportedException e) { 101 | attemptBlobFree = false; 102 | } 103 | } 104 | } 105 | 106 | public static Map extractors() { 107 | return extractors; 108 | } 109 | 110 | private static void reg( 111 | Map> exs, 112 | Class clazz, 113 | ObjectMapperRsExtractor extractor 114 | ) { 115 | exs.put(clazz, extractor); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/mappers/ObjectMapper.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.mappers; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.lang.reflect.ParameterizedType; 7 | import java.sql.ResultSet; 8 | import java.sql.ResultSetMetaData; 9 | import java.sql.SQLException; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.concurrent.ConcurrentMap; 15 | import java.util.function.Function; 16 | 17 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 18 | import org.codejargon.fluentjdbc.api.mapper.ObjectMapperRsExtractor; 19 | import org.codejargon.fluentjdbc.api.query.Mapper; 20 | import org.codejargon.fluentjdbc.internal.support.Arrs; 21 | import org.codejargon.fluentjdbc.internal.support.Maps; 22 | 23 | public class ObjectMapper implements Mapper { 24 | 25 | private final Map extractors; 26 | private final Class type; 27 | private final Map fields; 28 | private final Constructor noargConstructor; 29 | 30 | private final Function converter; 31 | private final ConcurrentMap converterCache = new ConcurrentHashMap<>(); 32 | 33 | public ObjectMapper(Class type, Map extractors, Function converter) { 34 | this.extractors = extractors; 35 | this.type = type; 36 | this.converter = converter; 37 | this.fields = discoverFields(type); 38 | noargConstructor = noargConstructor(); 39 | } 40 | 41 | private String convert(String field){ 42 | return converterCache.computeIfAbsent(field, converter); 43 | } 44 | 45 | private Map discoverFields(Class aType) throws SecurityException { 46 | Map allFields = new HashMap<>(); 47 | Class inspectedClass = aType; 48 | while (inspectedClass != null) { 49 | Arrs.stream(inspectedClass.getDeclaredFields()).forEach( 50 | field -> { 51 | field.setAccessible(true); 52 | allFields.put(convert(field.getName()), field); 53 | } 54 | ); 55 | inspectedClass = inspectedClass.getSuperclass(); 56 | } 57 | return Maps.copyOf(allFields); 58 | } 59 | 60 | @Override 61 | public T map(ResultSet rs) throws SQLException { 62 | T result = newInstance(); 63 | ResultSetMetaData metadata = rs.getMetaData(); 64 | for (int i = 1; i <= metadata.getColumnCount(); ++i) { 65 | mapColumn(convert(metadata.getColumnLabel(i)), i, rs, result); 66 | } 67 | return result; 68 | 69 | } 70 | 71 | private void mapColumn(String fieldName, int i, ResultSet rs, T result) throws IllegalArgumentException, FluentJdbcException, SQLException { 72 | Field field = fields.get(fieldName); 73 | if (field != null) { 74 | Object value = value(typeOfField(field), rs, i); 75 | setField(field, result, value); 76 | } 77 | } 78 | 79 | private void setField(Field field, T result, Object value) { 80 | try { 81 | Object valueToBeSet = (field.getType().equals(Optional.class)) ? 82 | optionalOf(field, value) : 83 | value; 84 | field.set(result, valueToBeSet); 85 | } catch (IllegalAccessException e) { 86 | throw new FluentJdbcException( 87 | String.format( 88 | "Unable to set field %s in %s with value %s", 89 | field.getName(), 90 | field.getDeclaringClass(), 91 | value != null ? value.getClass().getName() : "null" 92 | ), 93 | e 94 | ); 95 | } 96 | } 97 | 98 | private Object optionalOf(Field field, Object value) { 99 | if (value == null) { 100 | return Optional.empty(); 101 | } else { 102 | Class typeOfField = typeOfField(field); 103 | if (!typeOfField.isAssignableFrom(value.getClass())) { 104 | throw new FluentJdbcException(String.format("Can't map value of class %s to Optional<%s>", value.getClass(), field)); 105 | } 106 | return Optional.of(value); 107 | } 108 | } 109 | 110 | private Object value(Class fieldType, ResultSet rs, Integer index) throws SQLException { 111 | ObjectMapperRsExtractor converter = extractors.get(fieldType); 112 | Object value = converter != null ? converter.extract(rs, index) : rs.getObject(index); 113 | return (rs.wasNull() && !fieldType.isPrimitive()) ? null : value; 114 | } 115 | 116 | private T newInstance() throws IllegalArgumentException { 117 | try { 118 | return noargConstructor.newInstance(); 119 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { 120 | throw new FluentJdbcException( 121 | String.format("Cannot instantiate %s with the no-arg constructor", type.getName()), 122 | ex 123 | ); 124 | } 125 | } 126 | 127 | private Constructor noargConstructor() { 128 | try { 129 | Constructor constructor = type.getDeclaredConstructor(); 130 | constructor.setAccessible(true); 131 | return constructor; 132 | } catch (NoSuchMethodException ex) { 133 | throw new FluentJdbcException( 134 | String.format("Cannot find no-arg constructor in %s", type.getName()), 135 | ex 136 | ); 137 | } 138 | 139 | } 140 | 141 | private Class typeOfField(Field field) { 142 | return field.getType().equals(Optional.class) ? 143 | (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0] : 144 | field.getType(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DatabaseInspectionInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.inspection.DatabaseInspection; 4 | import org.codejargon.fluentjdbc.api.query.inspection.MetaDataAccess; 5 | import org.codejargon.fluentjdbc.api.query.inspection.MetaDataResultSet; 6 | import org.codejargon.fluentjdbc.api.query.inspection.MetaDataSelect; 7 | 8 | import java.util.Optional; 9 | 10 | class DatabaseInspectionInternal implements DatabaseInspection { 11 | private final QueryInternal query; 12 | 13 | DatabaseInspectionInternal(QueryInternal query) { 14 | this.query = query; 15 | } 16 | 17 | @Override 18 | public T accessMetaData(MetaDataAccess access) { 19 | return query.query( 20 | connection -> access.access(connection.getMetaData()), 21 | Optional.of("JDBC Database Inspection"), 22 | query.config.defaultSqlErrorHandler.get() 23 | ); 24 | } 25 | 26 | @Override 27 | public MetaDataSelect selectFromMetaData(MetaDataResultSet select) { 28 | return new MetaDataSelectInternal(query, select); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultParamSetters.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.ParamSetter; 4 | 5 | import java.time.*; 6 | import java.util.Collections; 7 | import java.util.Date; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | class DefaultParamSetters { 12 | 13 | private static final Map setters; 14 | 15 | static { 16 | Map ss = new HashMap<>(); 17 | javaDate(ss); 18 | javaTime(ss); 19 | javaBinary(ss); 20 | setters = Collections.unmodifiableMap(ss); 21 | } 22 | 23 | private static void javaTime(Map ss) { 24 | reg(ss, Instant.class, (param, ps, i) -> ps.setTimestamp(i, timestamp(param))); 25 | reg(ss, OffsetDateTime.class, (param, ps, i) -> ps.setTimestamp(i, timestamp(param.toInstant()))); 26 | reg(ss, ZonedDateTime.class, (param, ps, i) -> ps.setTimestamp(i, timestamp(param.toInstant()))); 27 | reg(ss, LocalDate.class, (param, ps, i) -> ps.setDate(i, java.sql.Date.valueOf(param))); 28 | reg(ss, LocalTime.class, (param, ps, i) -> ps.setTime(i, java.sql.Time.valueOf(param))); 29 | reg(ss, LocalDateTime.class, (param, ps, i) -> ps.setTimestamp(i, java.sql.Timestamp.valueOf(param))); 30 | reg(ss, Year.class, (param, ps, i) -> ps.setDate(i, java.sql.Date.valueOf(LocalDate.of(param.getValue(), Month.JANUARY, 1)))); 31 | reg(ss, YearMonth.class, (param, ps, i) -> ps.setDate(i, java.sql.Date.valueOf(LocalDate.of(param.getYear(), param.getMonth(), 1)))); 32 | } 33 | 34 | private static void javaDate(Map ss) { 35 | reg(ss, Date.class, (param, ps, i) -> { 36 | ps.setTimestamp(i, new java.sql.Timestamp(param.getTime())); 37 | }); 38 | } 39 | 40 | private static void javaBinary(Map ss) { 41 | reg(ss, byte[].class, (param, ps, i) -> ps.setBytes(i, param)); 42 | } 43 | 44 | static Map setters() { 45 | return setters; 46 | } 47 | 48 | private static java.sql.Timestamp timestamp(Instant instant) { 49 | return java.sql.Timestamp.from(instant); 50 | } 51 | 52 | private static void reg( 53 | Map setters, 54 | Class clazz, 55 | ParamSetter setter 56 | ) { 57 | setters.put(clazz, setter); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultSqlHandler.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; 4 | 5 | import java.sql.SQLException; 6 | import java.util.Optional; 7 | 8 | public class DefaultSqlHandler implements SqlErrorHandler { 9 | @Override 10 | public Action handle(SQLException e, Optional sql) throws SQLException { 11 | throw e; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/ExecutionDetailsInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.listen.ExecutionDetails; 4 | 5 | import java.sql.SQLException; 6 | import java.util.Optional; 7 | 8 | class ExecutionDetailsInternal implements ExecutionDetails { 9 | private final String sql; 10 | private final Long executionTimeMs; 11 | private final Optional sqlException; 12 | 13 | public ExecutionDetailsInternal( 14 | String sql, 15 | Long executionTimeMs, 16 | Optional sqlException) { 17 | this.sql = sql; 18 | this.executionTimeMs = executionTimeMs; 19 | this.sqlException = sqlException; 20 | } 21 | 22 | @Override 23 | public Boolean success() { 24 | return !sqlException.isPresent(); 25 | } 26 | 27 | @Override 28 | public String sql() { 29 | return sql; 30 | } 31 | 32 | @Override 33 | public Long executionTimeMs() { 34 | return executionTimeMs; 35 | } 36 | 37 | @Override 38 | public Optional sqlException() { 39 | return sqlException; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/FetchGenKey.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 4 | import org.codejargon.fluentjdbc.api.query.Mapper; 5 | import org.codejargon.fluentjdbc.api.query.UpdateResultGenKeys; 6 | 7 | import java.sql.PreparedStatement; 8 | import java.sql.ResultSet; 9 | import java.sql.SQLException; 10 | import java.util.*; 11 | 12 | class FetchGenKey { 13 | final Optional> mapper; 14 | 15 | static FetchGenKey no() { 16 | return new FetchGenKey(Optional.empty()); 17 | } 18 | 19 | static FetchGenKey yes(Mapper mapper) { 20 | return new FetchGenKey(Optional.of(mapper)); 21 | } 22 | 23 | private FetchGenKey(Optional> mapper) { 24 | this.mapper = mapper; 25 | } 26 | 27 | boolean fetch() { 28 | return mapper.isPresent(); 29 | } 30 | 31 | UpdateResultGenKeys genKeys(PreparedStatement ps, Integer affected) throws SQLException { 32 | List keys = generatedKeys(ps); 33 | if(keys.size() != affected) { 34 | throw new FluentJdbcException("Can't fetch generated keys properly, does the jdbc driver support it?"); 35 | } 36 | return new UpdateResultGenKeysInternal<>((long) affected, keys); 37 | } 38 | 39 | private List generatedKeys(PreparedStatement ps) throws SQLException { 40 | try (ResultSet keySet = ps.getGeneratedKeys()) { 41 | List keys = new ArrayList<>(1); 42 | while (keySet.next()) { 43 | keys.add(mapper.get().map(keySet)); 44 | } 45 | return Collections.unmodifiableList(keys); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/MetaDataSelectInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.Mapper; 4 | import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; 5 | import org.codejargon.fluentjdbc.api.query.inspection.MetaDataResultSet; 6 | import org.codejargon.fluentjdbc.api.query.inspection.MetaDataSelect; 7 | 8 | import java.sql.ResultSet; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | public class MetaDataSelectInternal implements MetaDataSelect { 14 | private final QueryInternal query; 15 | private final MetaDataResultSet select; 16 | 17 | public MetaDataSelectInternal(QueryInternal query, MetaDataResultSet select) { 18 | this.query = query; 19 | this.select = select; 20 | } 21 | 22 | @Override 23 | public List listResult(Mapper mapper) { 24 | return query.query( 25 | connection -> { 26 | List results = new ArrayList<>(); 27 | try(ResultSet rs = select.select(connection.getMetaData())) { 28 | while(rs.next()) { 29 | results.add(mapper.map(rs)); 30 | } 31 | } 32 | return results; 33 | }, 34 | Optional.empty(), 35 | query.config.defaultSqlErrorHandler.get() 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/ParamAssigner.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcSqlException; 4 | import org.codejargon.fluentjdbc.api.ParamSetter; 5 | 6 | import java.sql.PreparedStatement; 7 | import java.sql.SQLException; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | class ParamAssigner { 13 | private volatile boolean trySqlTypeForNull = true; 14 | 15 | private static final ParamSetter fallbackParamSetter = 16 | (param, statement, index) -> { 17 | if (!param.getClass().isEnum()) { 18 | statement.setObject(index, param); 19 | } else { 20 | statement.setString(index, param.toString()); 21 | } 22 | }; 23 | 24 | private final Map paramSetters; 25 | 26 | ParamAssigner(Map paramSetters) { 27 | this.paramSetters = paramSetters; 28 | } 29 | 30 | void assignParams(PreparedStatement statement, List params) { 31 | int i = 1; 32 | for (Object param : params) { 33 | assignParam( 34 | statement, 35 | i, 36 | param instanceof Optional ? ((Optional) param).orElse(null) : param 37 | ); 38 | ++i; 39 | } 40 | } 41 | 42 | private void assignParam(PreparedStatement statement, Integer index, Object param) { 43 | try { 44 | if (param != null) { 45 | assignNonNull(param, statement, index); 46 | } else { 47 | assignNull(statement, index); 48 | } 49 | } catch (SQLException e) { 50 | throw new FluentJdbcSqlException(String.format("Error assigning parameter index %s, object %s", index, param != null ? param.getClass().getName() : "null"), e); 51 | } 52 | } 53 | 54 | private void assignNull(PreparedStatement statement, Integer index) throws SQLException { 55 | Optional sqlType = sqlTypeForNull(statement, index); 56 | if(sqlType.isPresent()) { 57 | statement.setNull(index, sqlType.get()); 58 | } else { 59 | statement.setObject(index, null); 60 | } 61 | } 62 | 63 | private Optional sqlTypeForNull(PreparedStatement statement, Integer index) { 64 | if(trySqlTypeForNull) { 65 | try { 66 | return Optional.of(statement.getParameterMetaData().getParameterType(index)); 67 | } catch (SQLException e) { 68 | trySqlTypeForNull = false; 69 | return Optional.empty(); 70 | } 71 | } else { 72 | return Optional.empty(); 73 | } 74 | } 75 | 76 | @SuppressWarnings("unchecked") 77 | private void assignNonNull(Object param, PreparedStatement statement, Integer index) throws SQLException { 78 | paramSetter(param).set(param, statement, index); 79 | } 80 | 81 | private ParamSetter paramSetter(Object param) { 82 | ParamSetter customSetter = paramSetters.get(param.getClass()); 83 | return customSetter != null ? customSetter : fallbackParamSetter; 84 | } 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/PreparedStatementFactory.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import java.sql.Connection; 4 | import java.sql.PreparedStatement; 5 | import java.sql.SQLException; 6 | import java.sql.Statement; 7 | import java.util.List; 8 | 9 | class PreparedStatementFactory { 10 | public static final String[] emptyGenColumns = new String[]{}; 11 | private final QueryConfig config; 12 | 13 | PreparedStatementFactory(QueryConfig config) { 14 | this.config = config; 15 | } 16 | 17 | PreparedStatement createSingle(Connection con, SingleQueryBase singleQueryBase, boolean fetchGenerated, String[] genColumns) throws SQLException { 18 | SqlAndParams sqlAndParams = singleQueryBase.sqlAndParams(config); 19 | PreparedStatement statement = prepareStatement(con, sqlAndParams.sql(), fetchGenerated, genColumns); 20 | singleQueryBase.customizeQuery(statement, config); 21 | assignParams(statement, sqlAndParams.params()); 22 | return statement; 23 | } 24 | 25 | PreparedStatement createBatch(Connection con, Boolean fetchGenerated, String sql) throws SQLException { 26 | return prepareStatement(con, sql, fetchGenerated, emptyGenColumns); 27 | } 28 | 29 | void assignParams(PreparedStatement statement, List params) throws SQLException { 30 | config.paramAssigner.assignParams(statement, params); 31 | } 32 | 33 | private PreparedStatement prepareStatement(Connection con, String sql, Boolean fetchGenerated, String[] genColumns) throws SQLException { 34 | return fetchGenerated ? 35 | (genColumns.length > 0 ? 36 | con.prepareStatement(sql, genColumns) : 37 | con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) 38 | ) : 39 | con.prepareStatement(sql); 40 | } 41 | 42 | 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryConfig.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.ParamSetter; 4 | import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; 5 | import org.codejargon.fluentjdbc.api.query.Transaction; 6 | import org.codejargon.fluentjdbc.api.query.listen.AfterQueryListener; 7 | import org.codejargon.fluentjdbc.internal.query.namedparameter.NamedTransformedSqlFactory; 8 | import org.codejargon.fluentjdbc.internal.support.Maps; 9 | 10 | import java.util.Map; 11 | import java.util.Optional; 12 | import java.util.function.Supplier; 13 | 14 | public class QueryConfig { 15 | final ParamAssigner paramAssigner; 16 | 17 | private final Optional defaultFetchSize; 18 | final Optional defaultBatchSize; 19 | final Optional afterQueryListener; 20 | final NamedTransformedSqlFactory namedTransformedSqlFactory; 21 | final Optional defaultTransactionIsolation; 22 | final Supplier defaultSqlErrorHandler; 23 | 24 | public QueryConfig( 25 | Optional defaultFetchSize, 26 | Optional defaultBatchSize, 27 | Map paramSetters, 28 | Optional afterQueryListener, 29 | Optional defaultTransactionIsolation, 30 | Supplier defaultSqlErrorHandler 31 | ) { 32 | this.paramAssigner = new ParamAssigner( 33 | Maps.merge(DefaultParamSetters.setters(), paramSetters) 34 | ); 35 | this.namedTransformedSqlFactory = new NamedTransformedSqlFactory(); 36 | this.defaultFetchSize = defaultFetchSize; 37 | this.defaultBatchSize = defaultBatchSize; 38 | this.afterQueryListener = afterQueryListener; 39 | this.defaultTransactionIsolation = defaultTransactionIsolation; 40 | this.defaultSqlErrorHandler = defaultSqlErrorHandler; 41 | } 42 | 43 | 44 | Optional fetchSize(Optional selectFetchSize) { 45 | return selectFetchSize.isPresent() ? selectFetchSize : defaultFetchSize; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 4 | import org.codejargon.fluentjdbc.api.FluentJdbcSqlException; 5 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; 6 | import org.codejargon.fluentjdbc.api.query.*; 7 | import org.codejargon.fluentjdbc.api.query.inspection.DatabaseInspection; 8 | import org.codejargon.fluentjdbc.internal.integration.QueryConnectionReceiverInternal; 9 | 10 | import java.sql.Connection; 11 | import java.sql.PreparedStatement; 12 | import java.sql.SQLException; 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.function.Function; 16 | 17 | import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkNotNull; 18 | 19 | public class QueryInternal implements Query { 20 | 21 | final ConnectionProvider connectionProvider; 22 | final QueryConfig config; 23 | final PreparedStatementFactory preparedStatementFactory; 24 | 25 | public QueryInternal( 26 | ConnectionProvider connectionProvider, 27 | QueryConfig config 28 | ) { 29 | this.connectionProvider = connectionProvider; 30 | this.config = config; 31 | preparedStatementFactory = new PreparedStatementFactory(config); 32 | } 33 | 34 | @Override 35 | public SelectQuery select(String sql) { 36 | return new SelectQueryInternal(sql, this); 37 | } 38 | 39 | @Override 40 | public UpdateQuery update(String sql) { 41 | return new UpdateQueryInternal(sql, this); 42 | } 43 | 44 | @Override 45 | public BatchQuery batch(String sql) { 46 | return new BatchQueryInternal(sql, this); 47 | } 48 | 49 | @Override 50 | public Transaction transaction() { 51 | return new TransactionInternal(this); 52 | } 53 | 54 | 55 | @Override 56 | public DatabaseInspection databaseInspection() { 57 | return new DatabaseInspectionInternal(this); 58 | } 59 | 60 | T query(QueryRunnerConnection runner, Optional sql, SqlErrorHandler sqlErrorHandler) { 61 | AttemptResult ret = new AttemptResult<>(null, false); 62 | while(!ret.success) { 63 | ret = attemptQuery(runner, sql, sqlErrorHandler); 64 | } 65 | return ret.result; 66 | } 67 | 68 | @Override 69 | public T plainConnection(PlainConnectionQuery plainConnectionQuery) { 70 | return query(plainConnectionQuery::operation, Optional.empty(), config.defaultSqlErrorHandler.get()); 71 | } 72 | 73 | FluentJdbcException queryException(String sql, Optional reason, Optional e) { 74 | String message = String.format( 75 | "Error running query" + (reason.isPresent() ? ": " + reason.get() : "") + ", %s", sql 76 | ); 77 | return e.isPresent() ? new FluentJdbcSqlException(message, e.get()) : new FluentJdbcException(message); 78 | } 79 | 80 | private AttemptResult attemptQuery(QueryRunnerConnection runner, Optional sql, SqlErrorHandler sqlErrorHandler) { 81 | long start = System.currentTimeMillis(); 82 | try { 83 | T returnValue = doQuery(runner); 84 | listen(sql, start, Optional.empty()); 85 | return new AttemptResult<>(returnValue, true); 86 | } catch (SQLException e) { 87 | handleError(sql, sqlErrorHandler, start, e); 88 | return new AttemptResult<>(null, false); 89 | } 90 | } 91 | 92 | private T doQuery(QueryRunnerConnection runner) throws SQLException { 93 | QueryConnectionReceiverInternal receiver = new QueryConnectionReceiverInternal<>(runner); 94 | Optional transactionedConnection = TransactionInternal.transactionedConnection(connectionProvider); 95 | if (!transactionedConnection.isPresent()) { 96 | connectionProvider.provide(receiver); 97 | } else { 98 | receiver.receive(transactionedConnection.get()); 99 | } 100 | return receiver.returnValue(); 101 | } 102 | 103 | 104 | void assignParams(PreparedStatement statement, List params) throws SQLException { 105 | preparedStatementFactory.assignParams(statement, params); 106 | } 107 | 108 | private void listen(Optional sql, long start, Optional e) { 109 | config.afterQueryListener.ifPresent( 110 | afterQueryListener -> 111 | sql.ifPresent(sqlQuery -> afterQueryListener.listen( 112 | new ExecutionDetailsInternal(sqlQuery, System.currentTimeMillis() - start, e) 113 | )) 114 | ); 115 | } 116 | 117 | private void handleError(Optional sql, SqlErrorHandler sqlErrorHandler, long start, SQLException e) { 118 | try { 119 | checkNotNull(sqlErrorHandler.handle(e, sql), "Action in SqlErrorHandler"); 120 | } catch(SQLException sqle) { 121 | listen(sql, start, Optional.of(e)); 122 | throw queryException(sql.orElse(""), Optional.empty(), Optional.of(e)); 123 | } 124 | } 125 | 126 | private static class AttemptResult { 127 | private final T result; 128 | private final Boolean success; 129 | 130 | public AttemptResult(T result, Boolean success) { 131 | this.result = result; 132 | this.success = success; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryRunnerConnection.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import java.sql.Connection; 4 | import java.sql.SQLException; 5 | 6 | @FunctionalInterface 7 | public interface QueryRunnerConnection { 8 | T run(Connection c) throws SQLException; 9 | } 10 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryRunnerPreparedStatement.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import java.sql.PreparedStatement; 4 | import java.sql.SQLException; 5 | 6 | @FunctionalInterface 7 | public interface QueryRunnerPreparedStatement { 8 | T run(PreparedStatement c) throws SQLException; 9 | } 10 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SelectQueryInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 4 | import org.codejargon.fluentjdbc.api.query.Mapper; 5 | import org.codejargon.fluentjdbc.api.query.SelectQuery; 6 | import org.codejargon.fluentjdbc.api.query.SqlConsumer; 7 | import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; 8 | import org.codejargon.fluentjdbc.internal.support.Predicates; 9 | 10 | import java.sql.PreparedStatement; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | import java.util.*; 14 | import java.util.function.Consumer; 15 | import java.util.function.Predicate; 16 | 17 | import static java.util.Optional.empty; 18 | import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkArgument; 19 | import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkNotNull; 20 | 21 | class SelectQueryInternal extends SingleQueryBase implements SelectQuery { 22 | 23 | private Predicate filter = Predicates.alwaysTrue(); 24 | private Optional fetchSize = empty(); 25 | private Optional maxRows = empty(); 26 | 27 | SelectQueryInternal(String sql, QueryInternal query) { 28 | super(query, sql); 29 | } 30 | 31 | @Override 32 | public SelectQuery filter(Predicate filter) { 33 | this.filter = filter; 34 | return this; 35 | } 36 | 37 | @Override 38 | public SelectQuery fetchSize(Integer rows) { 39 | checkNotNull(rows, "rows"); 40 | checkArgument(rows >= 0, "Fetch size rows must be >= 0"); 41 | this.fetchSize = Optional.of(rows); 42 | return this; 43 | } 44 | 45 | @Override 46 | public SelectQuery maxRows(Long rows) { 47 | checkNotNull(rows, "rows"); 48 | checkArgument(rows >= 0, "Max results rows must be >= 0"); 49 | this.maxRows = Optional.of(rows); 50 | return this; 51 | } 52 | 53 | @Override 54 | public SelectQuery params(List params) { 55 | addParameters(params); 56 | return this; 57 | } 58 | 59 | @Override 60 | public SelectQuery params(Object... params) { 61 | addParameters(params); 62 | return this; 63 | } 64 | 65 | @Override 66 | public SelectQuery namedParams(Map namedParams) { 67 | addNamedParameters(namedParams); 68 | return this; 69 | } 70 | 71 | @Override 72 | public SelectQuery namedParam(String name, Object parameter) { 73 | addNamedParameter(name, parameter); 74 | return this; 75 | } 76 | 77 | @Override 78 | public SelectQuery errorHandler(SqlErrorHandler sqlErrorHandler ) { 79 | this.sqlErrorHandler = () -> sqlErrorHandler; 80 | return this; 81 | } 82 | 83 | 84 | @Override 85 | @SuppressWarnings("unchecked") 86 | public Optional firstResult(Mapper mapper) { 87 | return runQuery( 88 | ps -> { 89 | try (ResultSet rs = ps.executeQuery()) { 90 | Optional result = empty(); 91 | while (rs.next() && !result.isPresent()) { 92 | T candidate = mapper.map(rs); 93 | if (filter.test(candidate)) { 94 | result = Optional.of(candidate); 95 | } 96 | } 97 | return result; 98 | } 99 | }, 100 | sqlErrorHandler.get()); 101 | } 102 | 103 | @Override 104 | @SuppressWarnings("unchecked") 105 | public T singleResult(Mapper mapper) { 106 | Optional firstResult = firstResult(mapper); 107 | if (!firstResult.isPresent()) { 108 | throw query.queryException(sql, Optional.of("At least one result expected"), empty()); 109 | } 110 | return firstResult.get(); 111 | } 112 | 113 | 114 | @Override 115 | public List listResult(Mapper mapper) { 116 | List results = new ArrayList<>(); 117 | iterateResult(mapper, results::add); 118 | return Collections.unmodifiableList(results); 119 | } 120 | 121 | @Override 122 | public Set setResult(Mapper mapper) { 123 | Set results = new HashSet<>(); 124 | iterateResult(mapper, results::add); 125 | return Collections.unmodifiableSet(results); 126 | } 127 | 128 | @Override 129 | @SuppressWarnings("unchecked") 130 | public void iterateResult(Mapper mapper, Consumer consumer) { 131 | runQuery( 132 | ps -> { 133 | try (ResultSet rs = ps.executeQuery()) { 134 | while (rs.next()) { 135 | T candidate = mapper.map(rs); 136 | if (filter.test(candidate)) { 137 | consumer.accept(candidate); 138 | } 139 | } 140 | } 141 | return null; 142 | }, 143 | sqlErrorHandler.get() 144 | ); 145 | } 146 | 147 | @Override 148 | @SuppressWarnings("unchecked") 149 | public void iterateResult(SqlConsumer consumer) { 150 | runQuery( 151 | ps -> { 152 | try (ResultSet rs = ps.executeQuery()) { 153 | while (rs.next()) { 154 | if(filter.test(rs)) { 155 | consumer.accept(rs); 156 | } 157 | } 158 | } 159 | return null; 160 | }, 161 | sqlErrorHandler.get()); 162 | } 163 | 164 | @Override 165 | void customizeQuery(PreparedStatement statement, QueryConfig config) throws SQLException { 166 | selectFetchSize(statement, config); 167 | maxResults(statement); 168 | } 169 | 170 | private void selectFetchSize(PreparedStatement statement, QueryConfig config) throws SQLException { 171 | Optional activeFetchSize = config.fetchSize(fetchSize); 172 | if (activeFetchSize.isPresent()) { 173 | statement.setFetchSize(activeFetchSize.get()); 174 | } 175 | } 176 | 177 | private void maxResults(PreparedStatement statement) throws SQLException { 178 | if (maxRows.isPresent()) { 179 | if (maxRows.get() > Integer.MAX_VALUE) { 180 | setLargeMaxRows(statement); 181 | } else { 182 | statement.setMaxRows((int) maxRows.get().longValue()); 183 | } 184 | } 185 | } 186 | 187 | private void setLargeMaxRows(PreparedStatement statement) throws SQLException { 188 | try { 189 | statement.setLargeMaxRows(maxRows.get()); 190 | } catch (SQLException e) { 191 | throw new FluentJdbcException( 192 | String.format( 193 | "The JDBC driver %s doesn't support setLargeMaxRows(). Set max results <= Integer.MAX_VALUE", 194 | statement.getConnection().getMetaData().getDriverName()) 195 | ); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SingleQueryBase.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; 4 | import org.codejargon.fluentjdbc.internal.query.namedparameter.SqlAndParamsForNamed; 5 | import org.codejargon.fluentjdbc.internal.support.Preconditions; 6 | 7 | import java.sql.PreparedStatement; 8 | import java.sql.SQLException; 9 | import java.util.*; 10 | import java.util.function.Supplier; 11 | 12 | abstract class SingleQueryBase { 13 | protected final String sql; 14 | protected final QueryInternal query; 15 | protected final List params = new ArrayList<>(0); 16 | protected final Map namedParams = new HashMap<>(0); 17 | protected Supplier sqlErrorHandler; 18 | 19 | protected SingleQueryBase(QueryInternal query, String sql) { 20 | this.query = query; 21 | this.sql = sql; 22 | this.sqlErrorHandler = query.config.defaultSqlErrorHandler; 23 | } 24 | 25 | protected void addParameters(List params) { 26 | Preconditions.checkArgument(namedParams.isEmpty(), "Can not add positional parameters if named parameters are set."); 27 | this.params.addAll(params); 28 | } 29 | 30 | protected void addParameters(Object... params) { 31 | addParameters(Arrays.asList(params)); 32 | } 33 | 34 | protected void addNamedParameters(Map namedParams) { 35 | Preconditions.checkArgument(params.isEmpty(), "Can not add named parameters if positional parameters are set."); 36 | this.namedParams.putAll(namedParams); 37 | } 38 | 39 | protected void addNamedParameter(String name, Object parameter) { 40 | Preconditions.checkArgument(params.isEmpty(), "Can not add named parameters if positional parameters are set."); 41 | this.namedParams.put(name, parameter); 42 | } 43 | 44 | protected T runQuery( 45 | QueryRunnerPreparedStatement queryRunnerPreparedStatement, 46 | SqlErrorHandler sqlErrorHandler) { 47 | return runQuery( 48 | queryRunnerPreparedStatement, 49 | false, 50 | PreparedStatementFactory.emptyGenColumns, 51 | sqlErrorHandler 52 | ); 53 | } 54 | 55 | protected T runQueryAndFetch( 56 | QueryRunnerPreparedStatement queryRunnerPreparedStatement, 57 | String[] genColumns, 58 | SqlErrorHandler sqlErrorHandler) { 59 | return runQuery(queryRunnerPreparedStatement, true, genColumns, sqlErrorHandler); 60 | } 61 | 62 | private T runQuery( 63 | QueryRunnerPreparedStatement queryRunnerPreparedStatement, 64 | boolean fetchGenerated, 65 | String[] genColumns, 66 | SqlErrorHandler sqlErrorHandler) { 67 | return query.query(connection -> { 68 | try (PreparedStatement ps = query.preparedStatementFactory.createSingle(connection, this, fetchGenerated, genColumns)) { 69 | return queryRunnerPreparedStatement.run(ps); 70 | } 71 | }, Optional.of(sql), 72 | sqlErrorHandler); 73 | } 74 | 75 | SqlAndParams sqlAndParams(QueryConfig config) { 76 | return namedParams.isEmpty() ? 77 | new SqlAndParams(sql, params) : 78 | SqlAndParamsForNamed.create( 79 | config.namedTransformedSqlFactory.create(sql, namedParams), 80 | namedParams 81 | ); 82 | } 83 | 84 | abstract void customizeQuery(PreparedStatement preparedStatement, QueryConfig config) throws SQLException; 85 | } 86 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SqlAndParams.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import java.util.List; 4 | 5 | public class SqlAndParams { 6 | private final String sql; 7 | private final List params; 8 | 9 | public SqlAndParams(String sql, List params) { 10 | this.sql = sql; 11 | this.params = params; 12 | } 13 | 14 | String sql() { 15 | return sql; 16 | } 17 | 18 | List params() { 19 | return params; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/TransactionInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 4 | import org.codejargon.fluentjdbc.api.FluentJdbcSqlException; 5 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; 6 | import org.codejargon.fluentjdbc.api.query.Transaction; 7 | 8 | import java.sql.Connection; 9 | import java.sql.SQLException; 10 | import java.util.*; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.function.Supplier; 13 | 14 | class TransactionInternal implements Transaction { 15 | private static ThreadLocal> connections = new ThreadLocal<>(); 16 | 17 | private final QueryInternal queryInternal; 18 | private Optional isolation; 19 | 20 | TransactionInternal(QueryInternal queryInternal) { 21 | this.queryInternal = queryInternal; 22 | this.isolation = queryInternal.config.defaultTransactionIsolation; 23 | } 24 | 25 | @Override 26 | public Transaction isolation(Isolation isolation) { 27 | this.isolation = Optional.of(isolation); 28 | return this; 29 | } 30 | 31 | @Override 32 | public T in(Supplier operation) { 33 | Map cons = connections(); 34 | Optional transactionConnection = Optional.ofNullable(cons.get(queryInternal.connectionProvider)); 35 | return !transactionConnection.isPresent() ? 36 | inNewTransaction(operation, cons) : 37 | operation.get(); 38 | } 39 | 40 | @Override 41 | public void inNoResult(Runnable runnable) { 42 | in(() -> { 43 | runnable.run(); 44 | return null; 45 | }); 46 | } 47 | 48 | private T inNewTransaction(Supplier operation, Map cons) { 49 | try { 50 | List result = new ArrayList<>(1); 51 | queryInternal.connectionProvider.provide( 52 | con -> { 53 | Boolean originalAutocommit = null; 54 | try { 55 | isolation(con); 56 | originalAutocommit = con.getAutoCommit(); 57 | cons.put(queryInternal.connectionProvider, con); 58 | try { 59 | result.add(operation.get()); 60 | } catch(RuntimeException e) { 61 | if (!con.getAutoCommit()) { 62 | con.rollback(); 63 | } 64 | throw e; 65 | } 66 | con.commit(); 67 | } catch(SQLException e) { 68 | throw new FluentJdbcSqlException("Error executing transaction", e); 69 | } finally { 70 | restoreOriginalAutocommit(con, originalAutocommit); 71 | removeTransactionedConnection(cons); 72 | } 73 | } 74 | ); 75 | return result.get(0); 76 | } catch(SQLException e) { 77 | // should not occur 78 | throw new FluentJdbcSqlException("Error executing transaction.", e); 79 | } 80 | } 81 | 82 | private void isolation(Connection con) throws SQLException { 83 | if(isolation.isPresent()) { 84 | con.setTransactionIsolation(isolation.get().jdbcIsolation()); 85 | } 86 | } 87 | 88 | private Map connections() { 89 | Map cons = connections.get(); 90 | if(cons == null) { 91 | cons = new ConcurrentHashMap<>(4); 92 | connections.set(cons); 93 | } 94 | return cons; 95 | } 96 | 97 | private void removeTransactionedConnection(Map cons) { 98 | cons.remove(queryInternal.connectionProvider); 99 | if(cons.isEmpty()) { 100 | connections.remove(); 101 | } 102 | } 103 | 104 | private void restoreOriginalAutocommit(Connection con, Boolean originalAutocommit) { 105 | try { 106 | if(originalAutocommit != null && originalAutocommit) { 107 | con.setAutoCommit(true); 108 | } 109 | } catch(SQLException e) { 110 | // 111 | } 112 | } 113 | 114 | static Optional transactionedConnection(ConnectionProvider connectionProvider) throws SQLException { 115 | Optional connection = connections.get() != null ? 116 | Optional.ofNullable(connections.get().get(connectionProvider)) : 117 | Optional.empty(); 118 | if(connection.isPresent() && connection.get().getAutoCommit()) { 119 | connection.get().setAutoCommit(false); 120 | } 121 | return connection; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/UpdateQueryInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.*; 4 | 5 | import java.sql.PreparedStatement; 6 | import java.sql.ResultSet; 7 | import java.sql.SQLException; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | class UpdateQueryInternal extends SingleQueryBase implements UpdateQuery { 14 | 15 | UpdateQueryInternal(String sql, QueryInternal query) { 16 | super(query, sql); 17 | } 18 | 19 | @Override 20 | public UpdateQuery errorHandler(SqlErrorHandler sqlErrorHandler ) { 21 | this.sqlErrorHandler = () -> sqlErrorHandler; 22 | return this; 23 | } 24 | 25 | 26 | @Override 27 | public UpdateResult run() { 28 | return runQuery( 29 | ps -> new UpdateResultInternal((long) ps.executeUpdate()), 30 | sqlErrorHandler.get() 31 | ); 32 | } 33 | 34 | @Override 35 | public UpdateResultGenKeys runFetchGenKeys(Mapper mapper) { 36 | return runFetchGenKeys(mapper, PreparedStatementFactory.emptyGenColumns); 37 | } 38 | 39 | @Override 40 | public UpdateResultGenKeys runFetchGenKeys(Mapper mapper, String[] genColumns) { 41 | return runQueryAndFetch( 42 | ps -> FetchGenKey.yes(mapper).genKeys(ps, ps.executeUpdate()), 43 | genColumns, 44 | sqlErrorHandler.get() 45 | ); 46 | } 47 | 48 | @Override 49 | public UpdateQuery params(List params) { 50 | addParameters(params); 51 | return this; 52 | } 53 | 54 | @Override 55 | public UpdateQuery params(Object... params) { 56 | addParameters(params); 57 | return this; 58 | } 59 | 60 | @Override 61 | public UpdateQuery namedParams(Map namedParams) { 62 | addNamedParameters(namedParams); 63 | return this; 64 | } 65 | 66 | @Override 67 | public UpdateQuery namedParam(String name, Object parameter) { 68 | addNamedParameter(name, parameter); 69 | return this; 70 | } 71 | 72 | 73 | @Override 74 | void customizeQuery(PreparedStatement preparedStatement, QueryConfig config) { 75 | 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/UpdateResultGenKeysInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.UpdateResultGenKeys; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | class UpdateResultGenKeysInternal extends UpdateResultInternal implements UpdateResultGenKeys { 9 | private final List generatedKeys; 10 | 11 | UpdateResultGenKeysInternal(Long affectedRows, List generatedKeys) { 12 | super(affectedRows); 13 | this.generatedKeys = generatedKeys; 14 | } 15 | 16 | @Override 17 | public List generatedKeys() { 18 | return generatedKeys; 19 | } 20 | 21 | @Override 22 | public Optional firstKey() { 23 | return !generatedKeys.isEmpty() ? Optional.of(generatedKeys.get(0)) : Optional.empty(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/UpdateResultInternal.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query; 2 | 3 | import org.codejargon.fluentjdbc.api.query.UpdateResult; 4 | 5 | class UpdateResultInternal implements UpdateResult { 6 | private final long affectedRows; 7 | 8 | UpdateResultInternal(long affectedRows) { 9 | this.affectedRows = affectedRows; 10 | } 11 | 12 | @Override 13 | public long affectedRows() { 14 | return affectedRows; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/namedparameter/NamedTransformedSql.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query.namedparameter; 2 | 3 | import java.util.Map; 4 | 5 | public class NamedTransformedSql { 6 | private final String transformedSql; 7 | private final ParsedSql parsedSql; 8 | 9 | public static NamedTransformedSql forSqlAndParams(String sql, Map namedParams) { 10 | ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); 11 | return new NamedTransformedSql(NamedParameterUtils.substituteNamedParameters(parsedSql, namedParams), parsedSql); 12 | } 13 | 14 | NamedTransformedSql(String transformedSql, ParsedSql parsedSql) { 15 | this.transformedSql = transformedSql; 16 | this.parsedSql = parsedSql; 17 | } 18 | 19 | public String sql() { 20 | return transformedSql; 21 | } 22 | 23 | public ParsedSql parsedSql() { 24 | return parsedSql; 25 | } 26 | 27 | Integer unnamedParameterCount() { 28 | return parsedSql.getUnnamedParameterCount(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/namedparameter/NamedTransformedSqlFactory.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query.namedparameter; 2 | 3 | import java.util.*; 4 | import java.util.stream.Collectors; 5 | 6 | public class NamedTransformedSqlFactory { 7 | private final Map namedTransformedSqls; 8 | 9 | public NamedTransformedSqlFactory() { 10 | this.namedTransformedSqls = Collections.synchronizedMap(new WeakHashMap<>()); 11 | } 12 | 13 | public NamedTransformedSql create(String sql, Map namedParams) { 14 | CacheKey cacheKey = cacheKey(sql, namedParams); 15 | NamedTransformedSql namedTransformedSql = namedTransformedSqls.get(cacheKey); 16 | if (namedTransformedSql == null) { 17 | namedTransformedSql = NamedTransformedSql.forSqlAndParams(sql, namedParams); 18 | namedTransformedSqls.put(cacheKey, namedTransformedSql); 19 | } 20 | return namedTransformedSql; 21 | } 22 | 23 | public static boolean hasCollection(Map namedParams) { 24 | return namedParams.values().stream().filter(value -> value instanceof Collection).findFirst().isPresent(); 25 | } 26 | 27 | private CacheKey cacheKey(String sql, Map namedParams) { 28 | Optional> paramCounts = hasCollection(namedParams) ? 29 | Optional.of( 30 | Collections.unmodifiableMap(new TreeMap<>(namedParams).entrySet().stream() 31 | .collect( 32 | Collectors.toMap( 33 | Map.Entry::getKey, 34 | e -> (e.getValue() instanceof Collection) ? 35 | ((Collection) e.getValue()).size() : 36 | 1 37 | ) 38 | ) 39 | ) 40 | ) : 41 | Optional.empty(); 42 | return new CacheKey(sql, paramCounts); 43 | } 44 | 45 | private static class CacheKey { 46 | private final String sql; 47 | private final Optional> paramCounts; 48 | 49 | private CacheKey(String sql, Optional> paramCounts) { 50 | this.sql = sql; 51 | this.paramCounts = paramCounts; 52 | } 53 | 54 | @Override 55 | public boolean equals(Object o) { 56 | if (this == o) return true; 57 | if (o == null || getClass() != o.getClass()) return false; 58 | 59 | CacheKey that = (CacheKey) o; 60 | 61 | if (!sql.equals(that.sql)) return false; 62 | return paramCounts.equals(that.paramCounts); 63 | 64 | } 65 | 66 | @Override 67 | public int hashCode() { 68 | int result = sql.hashCode(); 69 | result = 31 * result + paramCounts.hashCode(); 70 | return result; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/namedparameter/ParsedSql.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2008 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.codejargon.fluentjdbc.internal.query.namedparameter; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Holds information about a parsed SQL statement. 24 | * 25 | * @author Thomas Risberg 26 | * @author Juergen Hoeller 27 | * @since 2.0 28 | */ 29 | class ParsedSql { 30 | 31 | private String originalSql; 32 | 33 | private List parameterNames = new ArrayList(); 34 | 35 | private List parameterIndexes = new ArrayList<>(); 36 | 37 | private int namedParameterCount; 38 | 39 | private int unnamedParameterCount; 40 | 41 | private int totalParameterCount; 42 | 43 | 44 | /** 45 | * Create a new instance of the {@link ParsedSql} class. 46 | * @param originalSql the SQL statement that is being (or is to be) parsed 47 | */ 48 | ParsedSql(String originalSql) { 49 | this.originalSql = originalSql; 50 | } 51 | 52 | /** 53 | * Return the SQL statement that is being parsed. 54 | */ 55 | String getOriginalSql() { 56 | return this.originalSql; 57 | } 58 | 59 | 60 | /** 61 | * Add a named parameter parsed from this SQL statement. 62 | * @param parameterName the name of the parameter 63 | * @param startIndex the start index in the original SQL String 64 | * @param endIndex the end index in the original SQL String 65 | */ 66 | void addNamedParameter(String parameterName, int startIndex, int endIndex) { 67 | this.parameterNames.add(parameterName); 68 | this.parameterIndexes.add(new int[] {startIndex, endIndex}); 69 | } 70 | 71 | /** 72 | * Return all of the parameters (bind variables) in the parsed SQL statement. 73 | * Repeated occurrences of the same parameter name are included here. 74 | */ 75 | List getParameterNames() { 76 | return this.parameterNames; 77 | } 78 | 79 | /** 80 | * Return the parameter indexes for the specified parameter. 81 | * @param parameterPosition the position of the parameter 82 | * (as index in the parameter names List) 83 | * @return the start index and end index, combined into 84 | * a int array of length 2 85 | */ 86 | int[] getParameterIndexes(int parameterPosition) { 87 | return this.parameterIndexes.get(parameterPosition); 88 | } 89 | 90 | /** 91 | * Set the count of named parameters in the SQL statement. 92 | * Each parameter name counts once; repeated occurrences do not count here. 93 | */ 94 | void setNamedParameterCount(int namedParameterCount) { 95 | this.namedParameterCount = namedParameterCount; 96 | } 97 | 98 | /** 99 | * Return the count of named parameters in the SQL statement. 100 | * Each parameter name counts once; repeated occurrences do not count here. 101 | */ 102 | int getNamedParameterCount() { 103 | return this.namedParameterCount; 104 | } 105 | 106 | /** 107 | * Set the count of all of the unnamed parameters in the SQL statement. 108 | */ 109 | void setUnnamedParameterCount(int unnamedParameterCount) { 110 | this.unnamedParameterCount = unnamedParameterCount; 111 | } 112 | 113 | /** 114 | * Return the count of all of the unnamed parameters in the SQL statement. 115 | */ 116 | int getUnnamedParameterCount() { 117 | return this.unnamedParameterCount; 118 | } 119 | 120 | /** 121 | * Set the total count of all of the parameters in the SQL statement. 122 | * Repeated occurrences of the same parameter name do count here. 123 | */ 124 | void setTotalParameterCount(int totalParameterCount) { 125 | this.totalParameterCount = totalParameterCount; 126 | } 127 | 128 | /** 129 | * Return the total count of all of the parameters in the SQL statement. 130 | * Repeated occurrences of the same parameter name do count here. 131 | */ 132 | int getTotalParameterCount() { 133 | return this.totalParameterCount; 134 | } 135 | 136 | 137 | /** 138 | * Exposes the original SQL String. 139 | */ 140 | @Override 141 | public String toString() { 142 | return this.originalSql; 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/namedparameter/SqlAndParamsForNamed.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query.namedparameter; 2 | 3 | import org.codejargon.fluentjdbc.internal.query.SqlAndParams; 4 | import org.codejargon.fluentjdbc.internal.support.Preconditions; 5 | 6 | import java.util.*; 7 | 8 | public abstract class SqlAndParamsForNamed { 9 | public static SqlAndParams create(NamedTransformedSql namedTransformedSql, Map namedParams) { 10 | Preconditions.checkArgument( 11 | namedTransformedSql.unnamedParameterCount() == 0, 12 | String.format("Querying with named parameters cannot be run with SQL statements containing positional parameters: %s", namedTransformedSql.sql()) 13 | ); 14 | return new SqlAndParams( 15 | namedTransformedSql.sql(), 16 | params(namedTransformedSql.parsedSql(), namedParams) 17 | ); 18 | } 19 | 20 | public static List params(ParsedSql parsedSql, Map namedParams) { 21 | return flatten(NamedParameterUtils.buildValueArray(parsedSql, namedParams)); 22 | } 23 | 24 | @SuppressWarnings("unchecked") 25 | private static List flatten(Object[] params) { 26 | List flattened = new ArrayList<>(); 27 | for (Object param : params) { 28 | if (param instanceof Collection) { 29 | flattened.addAll((Collection) param); 30 | } else { 31 | flattened.add(param); 32 | } 33 | } 34 | return Collections.unmodifiableList(flattened); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/support/Arrs.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.support; 2 | 3 | import java.util.Arrays; 4 | import java.util.stream.Stream; 5 | import java.util.stream.StreamSupport; 6 | 7 | public class Arrs { 8 | public static Stream stream(T[] array) { 9 | return StreamSupport.stream(Arrays.spliterator(array), false); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/support/Ints.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.support; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | public class Ints { 8 | public static List asList(int[] ints) { 9 | List integers = new ArrayList<>(ints.length); 10 | for(int i : ints) { 11 | integers.add(i); 12 | } 13 | return Collections.unmodifiableList(integers); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/support/Iterables.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.support; 2 | 3 | import java.util.Iterator; 4 | import java.util.stream.Stream; 5 | import java.util.stream.StreamSupport; 6 | 7 | public class Iterables { 8 | public static Stream stream(Iterable iterable) { 9 | return StreamSupport.stream(iterable.spliterator(), false); 10 | } 11 | 12 | private static Iterable fromIterator(Iterator iterator) { 13 | return () -> iterator; 14 | } 15 | 16 | public static Stream stream(Iterator iterator) { 17 | return stream(fromIterator(iterator)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/support/Lists.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.support; 2 | 3 | import java.util.*; 4 | 5 | public class Lists { 6 | private static final List emptyList = Collections.unmodifiableList(new ArrayList<>()); 7 | 8 | 9 | public static List copyOf(T[] elements) { 10 | return copyOf(Arrays.asList(elements)); 11 | } 12 | 13 | @SuppressWarnings("unchecked") 14 | public static List copyOf(Collection collection) { 15 | return collection.size() > 0 ? Collections.unmodifiableList(new ArrayList<>(collection)) : (List) emptyList; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/support/Maps.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.support; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.function.Function; 7 | import java.util.stream.Collectors; 8 | 9 | public class Maps { 10 | public static Map copyOf(Map map) { 11 | return Collections.unmodifiableMap(new HashMap<>(map)); 12 | } 13 | 14 | public static Map uniqueIndex(Iterable iterable, Function keyFunction) { 15 | return Collections.unmodifiableMap( 16 | Iterables.stream(iterable).collect( 17 | Collectors.toMap( 18 | keyFunction, 19 | value -> value 20 | ) 21 | ) 22 | ); 23 | } 24 | 25 | public static Map merge(Map map1, Map map2) { 26 | Map merged = new HashMap<>(map1); 27 | merged.putAll(map2); 28 | return Collections.unmodifiableMap(merged); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/support/Preconditions.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.support; 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcException; 4 | 5 | import java.util.Optional; 6 | 7 | public class Preconditions { 8 | public static T checkNotNull(T obj, String description) { 9 | if(obj == null) { 10 | throw new FluentJdbcException(description + " cannot be null."); 11 | } 12 | return obj; 13 | } 14 | 15 | public static void checkPresent(Optional obj, String description) { 16 | if(!obj.isPresent()) { 17 | throw new FluentJdbcException(description + " must be present."); 18 | } 19 | } 20 | 21 | public static void checkArgument(Boolean arg, String description) { 22 | if(!arg) { 23 | throw new FluentJdbcException(description); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/support/Predicates.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.support; 2 | 3 | import java.util.function.Predicate; 4 | 5 | public class Predicates { 6 | private static Predicate alwaysTrue = x -> true; 7 | 8 | public static Predicate alwaysTrue() { 9 | return alwaysTrue; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/support/Sneaky.java: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.support; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public class Sneaky { 6 | @FunctionalInterface 7 | public static interface SneakyConsumer{ 8 | void accept(T elem) throws Exception; 9 | } 10 | 11 | public static Consumer consumer(SneakyConsumer c) { 12 | return elem -> { 13 | try { 14 | c.accept(elem); 15 | } catch (Exception ex) { 16 | Sneaky.sneakyException(ex); 17 | } 18 | }; 19 | } 20 | 21 | @SuppressWarnings("unchecked") 22 | private static void sneakyException(Throwable t) throws T { 23 | throw (T) t; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /fluent-jdbc/src/main/resources/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Zsolt Herpai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/IntegrationTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration; 2 | 3 | interface IntegrationTest { 4 | } 5 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/IntegrationTestRoutine.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration 2 | import org.codejargon.fluentjdbc.api.FluentJdbc 3 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder 4 | import org.codejargon.fluentjdbc.api.integration.providers.DataSourceConnectionProvider 5 | import org.codejargon.fluentjdbc.api.mapper.Mappers 6 | import org.codejargon.fluentjdbc.api.mapper.ObjectMappers 7 | import org.codejargon.fluentjdbc.api.query.Query 8 | import org.codejargon.fluentjdbc.api.query.Transaction 9 | import org.codejargon.fluentjdbc.integration.testdata.Dummies 10 | import org.codejargon.fluentjdbc.integration.testdata.Dummy 11 | import org.codejargon.fluentjdbc.internal.support.Maps 12 | import org.junit.After 13 | import org.junit.Before 14 | import spock.lang.Specification 15 | 16 | import javax.sql.DataSource 17 | import java.sql.Connection 18 | import java.sql.ResultSet 19 | 20 | import static org.codejargon.fluentjdbc.integration.testdata.Dummies.* 21 | import static org.codejargon.fluentjdbc.integration.testdata.TestQuery.* 22 | 23 | abstract class IntegrationTestRoutine extends Specification { 24 | private static final def objectMappers = ObjectMappers.builder().build(); 25 | private static final def dummyMapper = objectMappers.forClass(Dummy.class); 26 | private static final def dummyAliasMapper = objectMappers.forClass(DummyAlias.class); 27 | 28 | protected FluentJdbc fluentJdbc 29 | protected Query query 30 | 31 | protected abstract DataSource dataSource() 32 | 33 | @Before 34 | void initializeFluentJdbcAndCleanUpDb() { 35 | fluentJdbc = new FluentJdbcBuilder() 36 | .connectionProvider(new DataSourceConnectionProvider(dataSource())) 37 | .build() 38 | query = fluentJdbc.query() 39 | removeContentAndVerify() 40 | } 41 | 42 | @After 43 | void cleanUpDb() { 44 | removeContentAndVerify() 45 | } 46 | 47 | def "Insert with positional parameters"() { 48 | when: 49 | query.update(insertSqlPositional).params(dummy1.params()).run() 50 | Dummy dummy = fluentJdbc.query().select(selectAllSql).singleResult(dummyMapper) 51 | then: 52 | assertDummy(dummy, dummy1) 53 | } 54 | 55 | def "Insert with named parameters"() { 56 | when: 57 | query.update(insertSqlNamed).namedParams(dummy1.namedParams()).run() 58 | then: 59 | Dummy dummy = fluentJdbc.query().select(selectAllSql).singleResult(dummyMapper) 60 | assertDummy(dummy, dummy1); 61 | } 62 | 63 | def "Select with named parameters including collections"() { 64 | when: 65 | query.update(insertSqlNamed).namedParams(dummy1.namedParams()).run() 66 | query.update(insertSqlNamed).namedParams(dummy2.namedParams()).run() 67 | then: 68 | fluentJdbc.query().select(selectIds).namedParams(["ids": [dummy1.id(), dummy2.id()] as List ]).listResult(dummyMapper).size() == 2 69 | fluentJdbc.query().select(selectIds).namedParams(["ids": [dummy1.id()] as List ]).listResult(dummyMapper).size() == 1 70 | } 71 | 72 | def "Batch insert with positional parameters"() { 73 | when: 74 | query 75 | .batch(insertSqlPositional) 76 | .params(Dummies.batchParams(dummy1, dummy2)) 77 | .run() 78 | then: 79 | List dummies = fluentJdbc.query().select(selectAllSql).listResult(dummyMapper) 80 | verifyBatchResults(dummies) 81 | } 82 | 83 | 84 | def "ObjectMappers supports SQL as"() { 85 | when: 86 | query 87 | .batch(insertSqlPositional) 88 | .params(Dummies.batchParams(dummy1)) 89 | .run() 90 | List dummies = fluentJdbc.query().select("SELECT ID AS ID_ALIAS FROM DUMMY").listResult(dummyAliasMapper) 91 | then: 92 | dummies.get(0).idAlias == dummy1.id 93 | } 94 | 95 | def "Mappers.map"() { 96 | when: 97 | query 98 | .batch(insertSqlPositional) 99 | .params(batchParams(dummy1)) 100 | .run() 101 | List> dummies = fluentJdbc.query().select("SELECT ID AS ID_ALIAS, string FROM DUMMY").listResult(Mappers.map()) 102 | then: 103 | ["ID_ALIAS", "id_alias"].collect { dummies[0].get(it) }.any { it == dummy1.id } 104 | ["STRING", "string"].collect { dummies[0].get(it) }.any { it == dummy1.string} 105 | } 106 | 107 | def "Batch insert with named parameters"() { 108 | when: 109 | query 110 | .batch(insertSqlNamed) 111 | .namedParams(Dummies.namedBatchParams(dummy1, dummy2)) 112 | .run() 113 | then: 114 | List dummies = fluentJdbc.query().select(selectAllSql).listResult(dummyMapper) 115 | verifyBatchResults(dummies) 116 | } 117 | 118 | def "Select with MaxRows specified"() { 119 | when: 120 | query 121 | .batch(insertSqlNamed) 122 | .namedParams(Dummies.namedBatchParams(dummy1, dummy2)) 123 | .run() 124 | then: 125 | List dummies = fluentJdbc.query().select(selectAllSql).listResult(dummyMapper) 126 | dummies.size() == 2 127 | List partialDummies = fluentJdbc.query().select(selectAllSql).maxRows(1L).listResult(dummyMapper) 128 | partialDummies.size() == 1 129 | } 130 | 131 | def "Transaction committed"() { 132 | when: 133 | query.transaction().in({ -> 134 | query.update(insertSqlPositional).params(dummy1.params()).run() 135 | query.update(insertSqlPositional).params(dummy2.params()).run() 136 | }); 137 | then: 138 | List dummies = fluentJdbc.query().select(selectAllSql).listResult(dummyMapper) 139 | dummies.size() == 2 140 | } 141 | 142 | def "Transaction rolled back"() { 143 | when: 144 | query.transaction().in({ -> 145 | query.update(insertSqlPositional).params(dummy1.params()).run() 146 | throwException() 147 | query.update(insertSqlPositional).params(dummy2.params()).run() 148 | }); 149 | then: 150 | thrown(RollbackException) 151 | List dummies = fluentJdbc.query().select(selectAllSql).listResult(dummyMapper) 152 | dummies.size() == 0 153 | } 154 | 155 | def "Transaction isolation"() { 156 | when: 157 | query.transaction().isolation(Transaction.Isolation.READ_COMMITTED).in({ -> 158 | query.update(insertSqlPositional).params(dummy1.params()).run() 159 | query.update(insertSqlPositional).params(dummy2.params()).run() 160 | }); 161 | then: 162 | List dummies = fluentJdbc.query().select(selectAllSql).listResult(dummyMapper) 163 | dummies.size() == 2 164 | } 165 | 166 | protected def "Database inspection with access"() { 167 | when: 168 | boolean foundTable = query.databaseInspection().accessMetaData({ 169 | meta -> 170 | ResultSet rs = meta.getTables(null, null, null, null) 171 | while (rs.next()) { 172 | if (["DUMMY", "dummy"].any { it.equals(rs.getString(3)) }) { 173 | return true; 174 | } 175 | } 176 | return false; 177 | } 178 | ) 179 | then: 180 | foundTable 181 | } 182 | 183 | protected def "Database inspection with select"() { 184 | when: 185 | List tables = query.databaseInspection().selectFromMetaData({ 186 | meta -> 187 | meta.getTables(null, null, null, null) 188 | }).listResult({ rs -> return rs.getString(3) }) 189 | then: 190 | ["DUMMY", "dummy"].any { tables.contains(it) } 191 | } 192 | 193 | protected static void createTestTable(Connection connection, String binaryColumn) { 194 | try { 195 | new FluentJdbcBuilder().build().queryOn(connection).update(dropDummyTable).run() 196 | } catch(Exception e) { 197 | // ignorable 198 | } 199 | new FluentJdbcBuilder().build().queryOn(connection).update(createDummyTable.replace("%BINCOLUMN%", binaryColumn)).run() 200 | } 201 | 202 | void removeContentAndVerify() { 203 | query.update("DELETE FROM DUMMY").run() 204 | def id = fluentJdbc.query().select("SELECT id FROM DUMMY").firstResult(Mappers.singleString()) 205 | assert !id.isPresent() 206 | } 207 | 208 | void verifyBatchResults(List dummies) { 209 | assert dummies.size() == 2 210 | Map dummyIndex = Maps.uniqueIndex(dummies, { d -> d.id }) 211 | assert dummyIndex.containsKey(dummy1.id) 212 | assertDummy(dummyIndex.get(dummy1.id), dummy1) 213 | assert dummyIndex.containsKey(dummy2.id) 214 | assertDummy(dummyIndex.get(dummy2.id), dummy2) 215 | } 216 | 217 | def throwException() { 218 | throw new RollbackException() 219 | } 220 | 221 | static class DummyAlias { 222 | String idAlias 223 | } 224 | } 225 | 226 | class RollbackException extends RuntimeException { 227 | 228 | } 229 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/testdata/Dummies.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration.testdata; 2 | 3 | import java.time.LocalDate; 4 | import java.time.Month; 5 | 6 | class Dummies { 7 | static final def dummy1 = new Dummy("idValue1", "stringValue1", LocalDate.of(2014, Month.MARCH, 12), new java.sql.Date(System.currentTimeMillis()), "foo".getBytes()); 8 | static final def dummy2 = new Dummy("idValue2", "stringValue2", LocalDate.of(2014, Month.JANUARY, 2), new java.sql.Date(System.currentTimeMillis()), "foo".getBytes()); 9 | 10 | static Iterator> namedBatchParams(Dummy... dummies) { 11 | List> allParams = [] 12 | dummies.each { 13 | dummy -> allParams.add(dummy.namedParams()) 14 | } 15 | return allParams.iterator() 16 | } 17 | 18 | static Iterator> batchParams(Dummy... dummies) { 19 | List> allParams = [] 20 | dummies.each { 21 | dummy -> 22 | allParams.add(dummy.params()) 23 | 24 | } 25 | allParams.iterator() 26 | } 27 | 28 | static void assertDummy(Dummy actual, Dummy expected) { 29 | assert actual.id.equals(expected.id) 30 | assert actual.string.equals(expected.string) 31 | assert actual.dateLocalDate.equals(expected.dateLocalDate) 32 | assert actual.dateSqlDate.toLocalDate().equals(expected.dateSqlDate.toLocalDate()) 33 | assert actual.nullString == null && expected.nullString == null 34 | assert Arrays.equals(actual.bytearray, expected.bytearray) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/testdata/Dummy.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration.testdata 2 | 3 | import java.time.LocalDate 4 | 5 | class Dummy { 6 | String id 7 | String string 8 | LocalDate dateLocalDate 9 | java.sql.Date dateSqlDate 10 | String nullString 11 | byte[] bytearray 12 | 13 | Dummy(String id, String string, LocalDate dateLocalDate, java.sql.Date dateSqlDate, byte[] bytearray) { 14 | this.id = id 15 | this.string = string 16 | this.dateLocalDate = dateLocalDate 17 | this.dateSqlDate = dateSqlDate 18 | this.nullString = null 19 | this.bytearray = bytearray 20 | } 21 | 22 | private Dummy() { 23 | } 24 | 25 | List params() { 26 | return [ 27 | id, 28 | string, 29 | dateLocalDate, 30 | dateSqlDate, 31 | nullString, 32 | bytearray 33 | ] 34 | } 35 | 36 | Map namedParams() { 37 | return [ 38 | "id" : id, 39 | "string" : string, 40 | "dateLocalDate": dateLocalDate, 41 | "dateSqlDate" : dateSqlDate, 42 | "nullString" : nullString, 43 | "bytearray" : bytearray 44 | ] 45 | } 46 | 47 | String id() { 48 | return id; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/testdata/TestQuery.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration.testdata; 2 | 3 | class TestQuery { 4 | public static final String insertSqlPositional = "INSERT INTO DUMMY(id, string, dateLocalDate, dateSqlDate, nullString, bytearray) VALUES(?, ?, ?, ?, ?, ?)"; 5 | public static final String insertSqlNamed = "INSERT INTO DUMMY(id, string, dateLocalDate, dateSqlDate, nullString, bytearray) VALUES(:id, :string, :dateLocalDate, :dateSqlDate, :nullString, :bytearray)"; 6 | public static final String selectAllSql = "SELECT * FROM DUMMY"; 7 | public static final String selectIds = "SELECT * FROM DUMMY where id in (:ids)"; 8 | public static final String createDummyTable = "CREATE TABLE DUMMY (id VARCHAR(255) PRIMARY KEY, string VARCHAR(255), dateLocalDate DATE, dateSqlDate DATE, nullString VARCHAR(255), bytearray %BINCOLUMN%)"; 9 | public static final String dropDummyTable = "DROP TABLE DUMMY" 10 | } 11 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/vendor/DerbyIntegrationTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration.vendor 2 | import org.apache.derby.jdbc.EmbeddedDataSource 3 | import org.codejargon.fluentjdbc.integration.IntegrationTest 4 | import org.codejargon.fluentjdbc.integration.IntegrationTestRoutine 5 | import org.junit.AfterClass 6 | import org.junit.BeforeClass 7 | import org.junit.experimental.categories.Category 8 | 9 | import javax.sql.DataSource 10 | import java.sql.Connection 11 | 12 | @Category(IntegrationTest.class) 13 | class DerbyIntegrationTest extends IntegrationTestRoutine { 14 | static Connection sentry 15 | static DataSource derbyDataSource 16 | 17 | @BeforeClass 18 | static void initH2() { 19 | initDerbyDataSource() 20 | createTestTable(sentry, "BLOB") 21 | } 22 | 23 | @AfterClass 24 | static void closeH2() { 25 | sentry.close() 26 | } 27 | 28 | private static void initDerbyDataSource() { 29 | Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() 30 | EmbeddedDataSource ds = new EmbeddedDataSource() 31 | ds.setDatabaseName("memory:test") 32 | ds.setCreateDatabase("create") 33 | derbyDataSource = ds 34 | sentry = ds.getConnection() 35 | } 36 | 37 | @Override 38 | protected DataSource dataSource() { 39 | return derbyDataSource; 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/vendor/H2IntegrationTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration.vendor; 2 | 3 | import org.codejargon.fluentjdbc.api.mapper.Mappers; 4 | import org.codejargon.fluentjdbc.api.query.UpdateResultGenKeys; 5 | import org.codejargon.fluentjdbc.integration.IntegrationTest; 6 | import org.codejargon.fluentjdbc.integration.IntegrationTestRoutine; 7 | import org.h2.jdbcx.JdbcDataSource; 8 | import org.junit.*; 9 | import org.junit.experimental.categories.Category; 10 | 11 | import javax.sql.DataSource; 12 | import java.sql.Connection; 13 | import java.sql.SQLException; 14 | 15 | import static org.hamcrest.CoreMatchers.is; 16 | import static org.junit.Assert.assertThat; 17 | 18 | @Category(IntegrationTest.class) 19 | class H2IntegrationTest extends IntegrationTestRoutine { 20 | 21 | static Connection sentry 22 | static DataSource h2DataSource 23 | 24 | @BeforeClass 25 | static void initH2() { 26 | initH2DataSource() 27 | createTestTable(sentry, "VARBINARY") 28 | } 29 | 30 | @AfterClass 31 | static void closeH2() { 32 | sentry.close() 33 | } 34 | 35 | def "Auto-generated keys fetched"() { 36 | given: 37 | query.update("CREATE TABLE DUMMY_AUTO (id INTEGER PRIMARY KEY AUTO_INCREMENT, data VARCHAR(255));").run() 38 | when: 39 | def result = query.update("INSERT INTO DUMMY_AUTO(DATA) VALUES('bla')").runFetchGenKeys( 40 | Mappers.singleLong() 41 | ) 42 | then: 43 | result.generatedKeys().size() == 1 44 | result.generatedKeys().get(0) == 1L 45 | } 46 | 47 | def "Auto-generated keys for specified colums fetched"() { 48 | given: 49 | query.update("CREATE TABLE DUMMY_AUTO2 (id INTEGER PRIMARY KEY AUTO_INCREMENT, data VARCHAR(255));").run() 50 | when: 51 | String[] genColumns = ["id"] 52 | def result = query.update("INSERT INTO DUMMY_AUTO2(DATA) VALUES('bla')").runFetchGenKeys( 53 | Mappers.singleLong(), genColumns 54 | ) 55 | then: 56 | result.generatedKeys().size() == 1 57 | result.generatedKeys().get(0) == 1L 58 | } 59 | 60 | def "Blob support"() { 61 | given: 62 | query.update("CREATE TABLE DUMMY_BYTE (id INTEGER PRIMARY KEY AUTO_INCREMENT, data BINARY(1000));").run() 63 | byte[] expected = "hah".getBytes() 64 | when: 65 | query.update("INSERT INTO DUMMY_BYTE(data) VALUES(?)").params(Collections.singletonList(expected)).run() 66 | byte[] result = query.select("SELECT data FROM DUMMY_BYTE").singleResult(Mappers.singleByteArray()) 67 | then: 68 | expected == result 69 | } 70 | 71 | private static def initH2DataSource() { 72 | Class.forName("org.h2.Driver").newInstance() 73 | JdbcDataSource ds = new JdbcDataSource() 74 | ds.setURL("jdbc:h2:mem:test/test") 75 | ds.setUser("sa") 76 | ds.setPassword("sa") 77 | h2DataSource = ds 78 | // keep a connection open for the duration of the test 79 | sentry = ds.getConnection() 80 | } 81 | 82 | @Override 83 | protected DataSource dataSource() { 84 | return h2DataSource; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/vendor/HSQLIntegrationTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration.vendor 2 | 3 | import org.codejargon.fluentjdbc.api.mapper.Mappers 4 | import org.codejargon.fluentjdbc.integration.IntegrationTest 5 | import org.codejargon.fluentjdbc.integration.IntegrationTestRoutine 6 | import org.junit.AfterClass 7 | import org.junit.BeforeClass 8 | import org.junit.experimental.categories.Category 9 | 10 | import javax.sql.DataSource 11 | import java.sql.Connection 12 | import java.sql.DriverManager 13 | import java.util.logging.Logger 14 | 15 | @Category(IntegrationTest.class) 16 | class HSQLIntegrationTest extends IntegrationTestRoutine { 17 | static def connectionString = "jdbc:hsqldb:mem:testdb" 18 | static Connection sentry 19 | static DataSource hsqlDataSource 20 | 21 | @BeforeClass 22 | static void initHsql() { 23 | initH2DataSource() 24 | createTestTable(sentry, "VARBINARY(100)") 25 | } 26 | 27 | @AfterClass 28 | static void closeHsql() { 29 | sentry.close() 30 | } 31 | 32 | def "Batch insert auto-generated keys fetch"() { 33 | given: 34 | query.update("CREATE TABLE DUMMY_AUTO (id INTEGER IDENTITY PRIMARY KEY, data VARCHAR(255));").run() 35 | when: 36 | def result = query.batch("INSERT INTO DUMMY_AUTO(DATA) VALUES(?)").params([["a"], ["b"]]).runFetchGenKeys(Mappers.singleLong()) 37 | then: 38 | result.size() == 2 39 | result.get(0).generatedKeys().size() == 1 40 | result.get(0).generatedKeys().get(0) == 0 41 | result.get(1).generatedKeys().size() == 1 42 | result.get(1).generatedKeys().get(0) == 1 43 | } 44 | 45 | private static void initH2DataSource() { 46 | Class.forName("org.hsqldb.jdbcDriver") 47 | hsqlDataSource = new HsqlDatasource() 48 | // keep a connection open for the duration of the tests 49 | sentry = hsqlDataSource.getConnection() 50 | } 51 | 52 | 53 | @Override 54 | protected DataSource dataSource() { 55 | return hsqlDataSource 56 | } 57 | 58 | private static class HsqlDatasource implements DataSource { 59 | @Override 60 | public Connection getConnection() { 61 | return DriverManager.getConnection(connectionString, "SA", "") 62 | } 63 | 64 | @Override 65 | public Connection getConnection(String username, String password) { 66 | throw new UnsupportedOperationException() 67 | } 68 | 69 | @Override 70 | public PrintWriter getLogWriter() { 71 | throw new UnsupportedOperationException() 72 | } 73 | 74 | @Override 75 | public void setLogWriter(PrintWriter out) { 76 | throw new UnsupportedOperationException() 77 | } 78 | 79 | @Override 80 | public void setLoginTimeout(int seconds) { 81 | throw new UnsupportedOperationException() 82 | } 83 | 84 | @Override 85 | public int getLoginTimeout() { 86 | throw new UnsupportedOperationException() 87 | } 88 | 89 | @Override 90 | public Logger getParentLogger() { 91 | throw new UnsupportedOperationException() 92 | } 93 | 94 | @Override 95 | public T unwrap(Class iface) { 96 | throw new UnsupportedOperationException() 97 | } 98 | 99 | @Override 100 | public boolean isWrapperFor(Class iface) { 101 | throw new UnsupportedOperationException() 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/integration/vendor/PostgresIntegrationTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.integration.vendor 2 | 3 | import com.opentable.db.postgres.embedded.EmbeddedPostgres 4 | import org.codejargon.fluentjdbc.integration.IntegrationTestRoutine 5 | import org.junit.AfterClass 6 | import org.junit.BeforeClass 7 | 8 | import javax.sql.DataSource 9 | 10 | class PostgresIntegrationTest extends IntegrationTestRoutine { 11 | static EmbeddedPostgres pg 12 | static DataSource ds 13 | 14 | @BeforeClass 15 | static void initPostgres() { 16 | pg = EmbeddedPostgres.start(); 17 | ds = pg.getDatabase("postgres", "postgres") 18 | 19 | def con = null 20 | try { 21 | con = ds.getConnection() 22 | createTestTable(con, "bytea") 23 | } finally { 24 | con.close() 25 | } 26 | } 27 | 28 | @AfterClass 29 | static void closePostgres() { 30 | pg.close() 31 | } 32 | 33 | @Override 34 | protected DataSource dataSource() { 35 | return ds 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/mappers/Dummy.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.mappers 2 | 3 | import java.time.Instant 4 | import java.time.LocalDate 5 | import java.time.LocalDateTime 6 | import java.time.Year 7 | import java.time.YearMonth 8 | 9 | class Dummy { 10 | Long longColumn 11 | Integer intColumn 12 | String stringColumn 13 | String stringNullColumn 14 | BigDecimal bigDecimalColumn 15 | Year yearColumn 16 | YearMonth yearMonthColumn 17 | LocalDate localDateColumn 18 | LocalDateTime localDateTimeColumn 19 | Instant instantColumn 20 | Instant instantNullColumn 21 | Optional optionalNonEmptyColumn 22 | Optional optionalEmptyColumn 23 | byte[] byteArrayColumn 24 | 25 | boolean equals(o) { 26 | if (this.is(o)) return true 27 | if (getClass() != o.class) return false 28 | 29 | Dummy dummy = (Dummy) o 30 | 31 | if (bigDecimalColumn != dummy.bigDecimalColumn) return false 32 | if (!Arrays.equals(byteArrayColumn, dummy.byteArrayColumn)) return false 33 | if (instantColumn != dummy.instantColumn) return false 34 | if (instantNullColumn != dummy.instantNullColumn) return false 35 | if (intColumn != dummy.intColumn) return false 36 | if (localDateColumn != dummy.localDateColumn) return false 37 | if (localDateTimeColumn != dummy.localDateTimeColumn) return false 38 | if (longColumn != dummy.longColumn) return false 39 | if (optionalEmptyColumn != dummy.optionalEmptyColumn) return false 40 | if (optionalNonEmptyColumn != dummy.optionalNonEmptyColumn) return false 41 | if (stringColumn != dummy.stringColumn) return false 42 | if (stringNullColumn != dummy.stringNullColumn) return false 43 | if (yearColumn != dummy.yearColumn) return false 44 | if (yearMonthColumn != dummy.yearMonthColumn) return false 45 | 46 | return true 47 | } 48 | 49 | int hashCode() { 50 | int result 51 | result = (longColumn != null ? longColumn.hashCode() : 0) 52 | result = 31 * result + (intColumn != null ? intColumn.hashCode() : 0) 53 | result = 31 * result + (stringColumn != null ? stringColumn.hashCode() : 0) 54 | result = 31 * result + (stringNullColumn != null ? stringNullColumn.hashCode() : 0) 55 | result = 31 * result + (bigDecimalColumn != null ? bigDecimalColumn.hashCode() : 0) 56 | result = 31 * result + (yearColumn != null ? yearColumn.hashCode() : 0) 57 | result = 31 * result + (yearMonthColumn != null ? yearMonthColumn.hashCode() : 0) 58 | result = 31 * result + (localDateColumn != null ? localDateColumn.hashCode() : 0) 59 | result = 31 * result + (localDateTimeColumn != null ? localDateTimeColumn.hashCode() : 0) 60 | result = 31 * result + (instantColumn != null ? instantColumn.hashCode() : 0) 61 | result = 31 * result + (instantNullColumn != null ? instantNullColumn.hashCode() : 0) 62 | result = 31 * result + (optionalNonEmptyColumn != null ? optionalNonEmptyColumn.hashCode() : 0) 63 | result = 31 * result + (optionalEmptyColumn != null ? optionalEmptyColumn.hashCode() : 0) 64 | result = 31 * result + (byteArrayColumn != null ? Arrays.hashCode(byteArrayColumn) : 0) 65 | return result 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/mappers/ObjectMapperTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.mappers; 2 | 3 | import org.codejargon.fluentjdbc.api.mapper.ObjectMappers 4 | import spock.lang.Specification 5 | 6 | import javax.sql.rowset.serial.SerialBlob; 7 | import java.sql.*; 8 | import java.time.*; 9 | 10 | class ObjectMapperTest extends Specification { 11 | 12 | static final expectedDummy = new Dummy( 13 | longColumn: 5L, 14 | intColumn: 25, 15 | stringColumn: "foo", 16 | stringNullColumn: null, 17 | bigDecimalColumn: BigDecimal.TEN, 18 | yearColumn: Year.of(2015), 19 | yearMonthColumn: YearMonth.of(2015, Month.FEBRUARY), 20 | localDateColumn: LocalDate.of(2015, Month.FEBRUARY, 15), 21 | localDateTimeColumn: LocalDateTime.of(2015, Month.FEBRUARY, 15, 11, 2), 22 | instantColumn: Instant.ofEpochMilli(543435L), 23 | instantNullColumn: null, 24 | optionalNonEmptyColumn: Optional.of(new java.util.Date()), 25 | optionalEmptyColumn: Optional.empty(), 26 | byteArrayColumn: "hah".getBytes() 27 | ) 28 | 29 | ResultSet resultSet = Mock(ResultSet) 30 | def factory = ObjectMappers.builder().build(); 31 | 32 | def setup() throws SQLException { 33 | def meta = Mock(ResultSetMetaData) 34 | 35 | resultSet.getLong(1) >> expectedDummy.longColumn 36 | meta.getColumnLabel(1) >> "LONG_COLUMN" 37 | resultSet.getInt(2) >> expectedDummy.intColumn 38 | meta.getColumnLabel(2) >> "INT_COLUMN" 39 | resultSet.getString(3) >> expectedDummy.stringColumn 40 | meta.getColumnLabel(3) >> "STRING_COLUMN" 41 | resultSet.getString(4) >> null 42 | meta.getColumnLabel(4) >> "STRING_NULL_COLUMN" 43 | resultSet.getBigDecimal(5) >> expectedDummy.bigDecimalColumn 44 | meta.getColumnLabel(5) >> "BIGDECIMAL_COLUMN" 45 | resultSet.getDate(6) >> Date.valueOf(LocalDate.of(expectedDummy.yearColumn.getValue(), Month.JANUARY, 1)) 46 | meta.getColumnLabel(6) >> "YEAR_COLUMN" 47 | resultSet.getDate(7) >> Date.valueOf(LocalDate.of(expectedDummy.yearMonthColumn.getYear(), expectedDummy.yearMonthColumn.getMonth(), 1)) 48 | meta.getColumnLabel(7) >> "YEARMONTH_COLUMN" 49 | resultSet.getDate(8) >> Date.valueOf(expectedDummy.localDateColumn) 50 | meta.getColumnLabel(8) >> "LOCALDATE_COLUMN" 51 | resultSet.getTimestamp(9) >> Timestamp.valueOf(expectedDummy.localDateTimeColumn) 52 | meta.getColumnLabel(9) >> "LOCALDATETIME_COLUMN" 53 | resultSet.getTimestamp(10) >> Timestamp.from(expectedDummy.instantColumn) 54 | meta.getColumnLabel(10) >> "INSTANT_COLUMN" 55 | resultSet.getTimestamp(11) >> null 56 | meta.getColumnLabel(11) >> "INSTANT_NULL_COLUMN" 57 | resultSet.getTimestamp(12) >> new Timestamp(expectedDummy.optionalNonEmptyColumn.get().getTime()) 58 | meta.getColumnLabel(12) >> "OPTIONAL_NON_EMPTY_COLUMN" 59 | resultSet.getTimestamp(13) >> null 60 | meta.getColumnLabel(13) >> "OPTIONAL_EMPTY_COLUMN" 61 | resultSet.getBytes(14) >> expectedDummy.byteArrayColumn 62 | meta.getColumnLabel(14) >> "BYTE_ARRAY_COLUMN" 63 | resultSet.getMetaData() >> meta 64 | meta.getColumnCount() >> 14 65 | } 66 | 67 | def map() { 68 | def mapper = factory.forClass(Dummy.class) 69 | when: 70 | def mappedDummy = mapper.map(resultSet) 71 | // For concise comparison in the expectations 72 | mappedDummy.optionalNonEmptyColumn = Optional.of(new java.util.Date(mappedDummy.optionalNonEmptyColumn.get().getTime())) 73 | then: 74 | mappedDummy == expectedDummy 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/DatabaseInspectionTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder 4 | import org.codejargon.fluentjdbc.api.query.Query; 5 | import spock.lang.Specification; 6 | 7 | import java.sql.Connection 8 | import java.sql.DatabaseMetaData 9 | import java.sql.ResultSet; 10 | 11 | class DatabaseInspectionTest extends Specification { 12 | 13 | def connection = Mock(Connection) 14 | def metaData = Mock(DatabaseMetaData) 15 | def resultset = Mock(ResultSet) 16 | 17 | Query query 18 | 19 | def setup() { 20 | connection.getMetaData() >> metaData 21 | query = new FluentJdbcBuilder().connectionProvider( 22 | { q -> q.receive(connection)} 23 | ).build().query(); 24 | } 25 | 26 | def "Access to metaData"() { 27 | def expectedVersion = 3 28 | given: 29 | metaData.getJDBCMajorVersion() >> expectedVersion 30 | when: 31 | int resultVersion = query.databaseInspection().accessMetaData( 32 | {meta -> return meta.getJDBCMajorVersion()} 33 | ); 34 | then: 35 | resultVersion == expectedVersion 36 | } 37 | 38 | def "Select from metaData"() { 39 | given: 40 | def rsIndex = 1 41 | def tableNames = ["1", "2"] 42 | resultset.next() >> true >> true >> false 43 | resultset.getString(rsIndex) >> tableNames.get(0) >> tableNames.get(1) 44 | metaData.getTables(null, null, null, null) >> resultset 45 | when: 46 | def results = query.databaseInspection() 47 | .selectFromMetaData({meta -> return meta.getTables(null, null, null, null)}) 48 | .listResult({rs -> rs.getString(rsIndex)}) 49 | then: 50 | results.size() == tableNames.size() 51 | results.get(0) == tableNames.get(0) 52 | results.get(1) == tableNames.get(1) 53 | 1 * resultset.close() 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/FluentJdbcBatchTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query 2 | import org.codejargon.fluentjdbc.api.query.UpdateResult 3 | 4 | import java.sql.ResultSet 5 | import java.sql.Statement 6 | 7 | import static org.codejargon.fluentjdbc.api.mapper.Mappers.singleLong 8 | 9 | class FluentJdbcBatchTest extends UpdateTestBase { 10 | static def param3 = "param3" 11 | static def param4 = "param4" 12 | static def param5 = "param5" 13 | static def param6 = "param6" 14 | 15 | def "Batch update"() { 16 | given: 17 | int[] expectedUpdated = [1, 1] 18 | Iterable> params = [ 19 | [param1, param2], 20 | [param3, param4] 21 | ] 22 | preparedStatement.executeBatch() >> expectedUpdated 23 | when: 24 | def updated = query.batch(sql).params(params).run() 25 | then: 26 | assertUpdateResults(expectedUpdated, updated) 27 | interaction { 28 | verify4Params() 29 | verifyBatches(2) 30 | } 31 | } 32 | 33 | def "Batch update with batch size specified"() { 34 | given: 35 | int[] expectedUpdated = [1, 1, 1] 36 | Iterable> params = Arrays.>asList( 37 | Arrays.asList(param1, param2), 38 | Arrays.asList(param3, param4), 39 | Arrays.asList(param5, param6) 40 | ) 41 | preparedStatement.executeBatch() >> [1, 1] >> [1] 42 | when: 43 | def updated = query.batch(sql).batchSize(2).params(params).run() 44 | then: 45 | assertUpdateResults(expectedUpdated, updated) 46 | interaction { 47 | verify6Params() 48 | verifyBatches(3) 49 | } 50 | } 51 | 52 | def "Batch update with named parameters"() { 53 | given: 54 | int[] expectedUpdated = [1]; 55 | Iterable> namedParams = Arrays.>asList( 56 | namedParams() 57 | ) 58 | connection.prepareStatement(sql) >> preparedStatement 59 | preparedStatement.executeBatch() >> expectedUpdated 60 | when: 61 | query.batch(sqlWithNamedParams).namedParams(namedParams).run(); 62 | then: 63 | interaction { 64 | verify2Params(); 65 | verifyBatches(1); 66 | } 67 | } 68 | 69 | def "Batch update fetch generated keys"() { 70 | given: 71 | Long generatedKey1 = 5L 72 | Long generatedKey2 = 6L 73 | int[] expectedUpdated = [1, 1] 74 | Iterable> params = [ 75 | [param1, param2], 76 | [param3, param4] 77 | ] 78 | preparedStatement.executeBatch() >> expectedUpdated 79 | ResultSet genKeyRs = Mock(ResultSet) 80 | genKeyRs.next() >> true >> true >> false 81 | genKeyRs.getLong(1) >> generatedKey1 >> generatedKey2 82 | preparedStatement.getGeneratedKeys() >> genKeyRs 83 | connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) >> preparedStatement 84 | when: 85 | def updated = query.batch(sql).params(params).runFetchGenKeys(singleLong()) 86 | then: 87 | assertUpdateResults(expectedUpdated, updated) 88 | updated.size() == 2 89 | updated.get(0).generatedKeys() == [generatedKey1] 90 | updated.get(1).generatedKeys() == [generatedKey2] 91 | interaction { 92 | verify4Params() 93 | verifyBatches(2) 94 | } 95 | } 96 | 97 | 98 | 99 | def verifyBatches(Integer addBatch) { 100 | addBatch * preparedStatement.addBatch() 101 | 1 * preparedStatement.close() 102 | } 103 | 104 | def assertUpdateResults(int[] expectedUpdated, List updated) { 105 | assert updated.size() == expectedUpdated.length 106 | updated.eachWithIndex { UpdateResult result, int i -> 107 | result.affectedRows() == (long) expectedUpdated[i] 108 | } 109 | } 110 | 111 | def verify2Params() { 112 | 1 * preparedStatement.setObject(1, param1) 113 | 1 * preparedStatement.setObject(2, param2) 114 | } 115 | 116 | def verify4Params() { 117 | verify2Params(); 118 | 1 * preparedStatement.setObject(1, param3) 119 | 1 * preparedStatement.setObject(2, param4) 120 | } 121 | 122 | def verify6Params() { 123 | verify4Params(); 124 | 1 * preparedStatement.setObject(1, param5) 125 | 1 * preparedStatement.setObject(2, param6) 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/FluentJdbcTransactionTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder 4 | import org.codejargon.fluentjdbc.api.FluentJdbcException 5 | import org.codejargon.fluentjdbc.api.query.Transaction 6 | 7 | import java.sql.Connection 8 | 9 | class FluentJdbcTransactionTest extends UpdateTestBase { 10 | def expectedUpdatedRows = 5L 11 | 12 | def "No transaction uses different connections"() { 13 | given: 14 | preparedStatement.executeUpdate() >> expectedUpdatedRows.intValue() 15 | when: 16 | query.update(sql).params(param1, param2).run() 17 | query.update(sql).params(param1, param2).run() 18 | query.update(sql).params(param1, param2).run() 19 | then: 20 | connectionProvided == 3 21 | 0 * connection.setAutoCommit(false) 22 | 3 * preparedStatement.setObject(1, param1) 23 | 3 * preparedStatement.setObject(2, param2) 24 | 3 * preparedStatement.close() 25 | 0 * connection.commit() 26 | 0 * connection.rollback() 27 | } 28 | 29 | def "Successful transaction committed, same connection used"() { 30 | given: 31 | preparedStatement.executeUpdate() >> expectedUpdatedRows.intValue() 32 | when: 33 | def updateResult = query.transaction().in( 34 | { -> 35 | query.update(sql).params(param1, param2).run() 36 | query.update(sql).params(param1, param2).run() 37 | query.update(sql).params(param1, param2).run() 38 | } 39 | ); 40 | then: 41 | // checking original state, then check on first query 42 | connection.getAutoCommit() >> true >> true >> false 43 | connectionProvided == 1 44 | _ * connection.getAutoCommit(); 45 | 1 * connection.setAutoCommit(false) 46 | 3 * preparedStatement.setObject(1, param1) 47 | 3 * preparedStatement.setObject(2, param2) 48 | 3 * preparedStatement.close() 49 | 1 * connection.commit() 50 | 0 * connection.rollback() 51 | 1 * connection.setAutoCommit(true) 52 | updateResult.affectedRows() == expectedUpdatedRows 53 | } 54 | 55 | def "Failed operation rolls back transaction"() { 56 | given: 57 | preparedStatement.executeUpdate() >> expectedUpdatedRows.intValue() 58 | when: 59 | query.transaction().in( 60 | { -> 61 | query.update(sql).params(param1, param2).run() 62 | query.update(sql).params(param1, param2).run() 63 | throwException() 64 | query.update(sql).params(param1, param2).run() 65 | } 66 | ); 67 | then: 68 | // checking original state, then check on first query 69 | connection.getAutoCommit() >> true >> true >> false 70 | 71 | thrown(MyRuntimeException) 72 | _ * connection.getAutoCommit(); 73 | 1 * connection.setAutoCommit(false) 74 | 2 * preparedStatement.setObject(1, param1) 75 | 2 * preparedStatement.setObject(2, param2) 76 | 2 * preparedStatement.close() 77 | 1 * connection.rollback() 78 | 0 * connection.commit() 79 | 1 * connection.setAutoCommit(true); 80 | } 81 | 82 | def "Transactions are lazy"() { 83 | given: 84 | preparedStatement.executeUpdate() >> expectedUpdatedRows.intValue() 85 | when: 86 | query.transaction().inNoResult({ -> }); 87 | then: 88 | 0 * connection.setAutoCommit(false) 89 | } 90 | 91 | def "Transaction isolation"() { 92 | given: 93 | preparedStatement.executeUpdate() >> expectedUpdatedRows.intValue() 94 | when: 95 | def updateResult = query.transaction().isolation(Transaction.Isolation.REPEATABLE_READ).in( 96 | { -> 97 | query.update(sql).params(param1, param2).run() 98 | query.update(sql).params(param1, param2).run() 99 | query.update(sql).params(param1, param2).run() 100 | } 101 | ); 102 | then: 103 | // checking original state, then check on first query 104 | connection.getAutoCommit() >> true >> true >> false 105 | connectionProvided == 1 106 | _ * connection.getAutoCommit() 107 | 1 * connection.setAutoCommit(false) 108 | 1 * connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ) 109 | 1 * connection.commit() 110 | updateResult.affectedRows() == expectedUpdatedRows 111 | } 112 | 113 | def "Default transaction isolation"() { 114 | given: 115 | def fluentJdbc = new FluentJdbcBuilder() 116 | .connectionProvider(connectionProvider) 117 | .defaultTransactionIsolation(Transaction.Isolation.SERIALIZABLE) 118 | .build() 119 | 120 | when: 121 | fluentJdbc.query().transaction().in({}) 122 | 123 | then: 124 | 1 * connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) 125 | } 126 | 127 | def throwException() { 128 | throw new MyRuntimeException() 129 | } 130 | } 131 | 132 | class MyRuntimeException extends RuntimeException { 133 | 134 | } 135 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/FluentJdbcUpdateTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query 2 | 3 | import org.codejargon.fluentjdbc.api.mapper.Mappers 4 | import org.codejargon.fluentjdbc.api.query.UpdateResultGenKeys 5 | 6 | import java.sql.ResultSet 7 | import java.sql.Statement 8 | 9 | class FluentJdbcUpdateTest extends UpdateTestBase { 10 | static def expectedUpdatedRows = 5L 11 | 12 | def "Update with positional parameters"() { 13 | given: 14 | preparedStatement.executeUpdate() >> expectedUpdatedRows.intValue() 15 | when: 16 | def updateResult = query.update(sql).params(param1, param2).run() 17 | then: 18 | updateResult.affectedRows() == expectedUpdatedRows 19 | interaction { 20 | verifyQuerying() 21 | } 22 | } 23 | 24 | def "Update with named parameters"() { 25 | given: 26 | connection.prepareStatement(sql) >> preparedStatement 27 | preparedStatement.executeUpdate() >> expectedUpdatedRows.intValue() 28 | when: 29 | def updateResult = query.update(sqlWithNamedParams).namedParams(namedParams()).run() 30 | then: 31 | updateResult.affectedRows() == expectedUpdatedRows 32 | interaction { 33 | verifyQuerying() 34 | } 35 | } 36 | 37 | 38 | def "Update and fetch generated keys"() { 39 | given: 40 | Long generatedKey1 = 5L 41 | Long generatedKey2 = 6L 42 | ResultSet genKeyRs = Mock(ResultSet) 43 | genKeyRs.next() >> true >> true >> false 44 | genKeyRs.getLong(1) >> generatedKey1 >> generatedKey2 45 | preparedStatement.getGeneratedKeys() >> genKeyRs 46 | 0 * connection.prepareStatement(sql) 47 | 1 * connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) >> preparedStatement 48 | 1 * preparedStatement.executeUpdate() >> 2 49 | when: 50 | UpdateResultGenKeys result = query.update(sql).params(param1, param2).runFetchGenKeys(Mappers.singleLong()); 51 | then: 52 | result.generatedKeys().size() == 2 53 | result.generatedKeys().get(0) == generatedKey1 54 | result.generatedKeys().get(1) == generatedKey2 55 | interaction { 56 | verifyQuerying() 57 | } 58 | } 59 | 60 | def "Update and fetch generated keys with generated column names specified (mandatory in Oracle)"() { 61 | given: 62 | String[] genColumns = ["id"]; 63 | Long generatedKey = 5L 64 | ResultSet genKeyRs = Mock(ResultSet) 65 | genKeyRs.next() >> true >> false 66 | genKeyRs.getLong(1) >> generatedKey 67 | preparedStatement.getGeneratedKeys() >> genKeyRs 68 | 0 * connection.prepareStatement(sql) 69 | 1 * connection.prepareStatement(sql, genColumns) >> preparedStatement 70 | 1 * preparedStatement.executeUpdate() >> 1 71 | when: 72 | UpdateResultGenKeys result = query.update(sql).params(param1, param2).runFetchGenKeys(Mappers.singleLong(), genColumns); 73 | then: 74 | result.generatedKeys().size() == 1 75 | result.generatedKeys().get(0) == generatedKey 76 | interaction { 77 | verifyQuerying() 78 | } 79 | } 80 | 81 | def verifyQuerying() { 82 | 1 * preparedStatement.setObject(1, param1) 83 | 1 * preparedStatement.setObject(2, param2) 84 | 1 * preparedStatement.close() 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/ParamAssignerTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query 2 | 3 | import spock.lang.Specification 4 | 5 | import java.sql.* 6 | import java.time.* 7 | 8 | class ParamAssignerTest extends Specification { 9 | static final def localDateTime = LocalDateTime.of(2015, Month.MARCH, 5, 12, 5) 10 | static final def localDate = LocalDate.of(2015, Month.MARCH, 5) 11 | static final def localTime = LocalTime.of(12, 5) 12 | static final def instant = localDateTime.toInstant(ZoneOffset.UTC) 13 | static final def javaDate = new java.util.Date(instant.toEpochMilli()) 14 | 15 | static final def string = "a" 16 | static final def longParam = 5L 17 | static final def intParam = 123; 18 | static final def bigDecimal = BigDecimal.TEN 19 | static final def sqlDate = java.sql.Date.valueOf(localDate) 20 | static final def time = Time.valueOf(localTime) 21 | static final def byteArray = "hah".getBytes() 22 | static final Timestamp timestamp = Timestamp.from(instant) 23 | 24 | 25 | def statement = Mock(PreparedStatement) 26 | def paramAssigner = new ParamAssigner(DefaultParamSetters.setters()) 27 | 28 | def "JDBC types"() { 29 | InputStream blobContent 30 | when: 31 | paramAssigner.assignParams( 32 | statement, 33 | [string, longParam, intParam, bigDecimal, sqlDate, time, timestamp, byteArray] 34 | ) 35 | then: 36 | 1 * statement.setObject(1, string) 37 | 1 * statement.setObject(2, longParam) 38 | 1 * statement.setObject(3, intParam) 39 | 1 * statement.setObject(4, bigDecimal) 40 | 1 * statement.setObject(5, sqlDate) 41 | 1 * statement.setObject(6, time) 42 | 1 * statement.setObject(7, timestamp) 43 | 1 * statement.setBytes(8, byteArray) 44 | } 45 | 46 | def "Local java.time types"() { 47 | when: 48 | paramAssigner.assignParams( 49 | statement, 50 | [localDateTime, localDate, localTime] 51 | ) 52 | then: 53 | 1 * statement.setTimestamp(1, Timestamp.valueOf(localDateTime)) 54 | 1 * statement.setDate(2, sqlDate) 55 | 1 * statement.setTime(3, time) 56 | } 57 | 58 | def "java.time Instant"() throws SQLException { 59 | when: 60 | paramAssigner.assignParams( 61 | statement, 62 | [instant] 63 | ) 64 | then: 65 | 1 * statement.setTimestamp(1, Timestamp.from(instant)); 66 | } 67 | 68 | def "java.util Date"() throws SQLException { 69 | when: 70 | paramAssigner.assignParams( 71 | statement, 72 | [javaDate] 73 | ) 74 | then: 75 | 1 * statement.setTimestamp(1, timestamp) 76 | } 77 | 78 | def "enum set as string"() throws SQLException { 79 | when: 80 | paramAssigner.assignParams( 81 | statement, 82 | [Foo.BAR] 83 | ) 84 | then: 85 | 1 * statement.setString(1, "BAR") 86 | } 87 | 88 | def "Optional with value set"() throws SQLException { 89 | given: 90 | def value = Optional.of("foo"); 91 | 92 | when: 93 | paramAssigner.assignParams( 94 | statement, 95 | [value] 96 | ) 97 | then: 98 | 1 * statement.setObject(1, value.get()) 99 | } 100 | 101 | def "Empty Optional set as null"() throws SQLException { 102 | given: 103 | def parameterMetadata = Mock(ParameterMetaData) 104 | parameterMetadata.getParameterType(1) >> 1 105 | statement.getParameterMetaData() >> parameterMetadata 106 | 107 | def value = Optional.empty(); 108 | 109 | when: 110 | paramAssigner.assignParams( 111 | statement, 112 | [value] 113 | ) 114 | then: 115 | 116 | 1 * statement.setNull(1, 1) 117 | } 118 | 119 | enum Foo { 120 | BAR 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/PlainConnectionTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query 2 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder 3 | import org.codejargon.fluentjdbc.api.query.Query 4 | import org.junit.Test 5 | import spock.lang.Specification 6 | 7 | import java.sql.Connection 8 | import java.sql.PreparedStatement 9 | import java.sql.ResultSet 10 | 11 | public class PlainConnectionTest extends Specification { 12 | static final String sql = "SELECT * FROM BAR" 13 | static final String column = "FOO" 14 | 15 | 16 | static String param1 = "lille" 17 | static String param2 = "lamb" 18 | static String result1 = "1" 19 | static String result2 = "2" 20 | static String result3 = "3" 21 | 22 | def connection = Mock(Connection) 23 | def preparedStatement = Mock(PreparedStatement) 24 | 25 | Query query; 26 | 27 | def setup() { 28 | connection.prepareStatement(sql) >> preparedStatement 29 | query = new FluentJdbcBuilder().connectionProvider( 30 | { q -> q.receive(connection)} 31 | ).build().query(); 32 | } 33 | 34 | @Test 35 | def "Access to connection"() { 36 | when: 37 | query.plainConnection({connection -> 38 | def statement = connection.prepareStatement(sql) 39 | statement.execute() 40 | }) 41 | then: 42 | 1 * preparedStatement.execute() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/QueryListenerTest.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query 2 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder 3 | import org.codejargon.fluentjdbc.api.FluentJdbcSqlException 4 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider 5 | import org.codejargon.fluentjdbc.api.query.Query 6 | import org.codejargon.fluentjdbc.api.query.listen.ExecutionDetails 7 | import spock.lang.Specification 8 | 9 | import java.sql.Connection 10 | import java.sql.PreparedStatement 11 | import java.sql.SQLException 12 | 13 | class QueryListenerTest extends Specification { 14 | protected static final def sql = "UPDATE FOO SET BAR = 'x' WHERE COL1 = ? AND COL2 = ?" 15 | Query query 16 | def preparedStatement = Mock(PreparedStatement) 17 | Connection connection = Mock(Connection) 18 | ExecutionDetails executionDetails = null 19 | 20 | def setup() { 21 | connection.prepareStatement(sql) >> preparedStatement 22 | ConnectionProvider connectionProvider = { q -> 23 | q.receive(connection) 24 | } 25 | query = new FluentJdbcBuilder() 26 | .connectionProvider(connectionProvider) 27 | .afterQueryListener({ details -> this.executionDetails = details } ) 28 | .build() 29 | .query() 30 | } 31 | 32 | def "Listener invoked"() { 33 | when: 34 | query.update(sql).run() 35 | then: 36 | executionDetails != null 37 | executionDetails.success() 38 | executionDetails.sql() == sql 39 | executionDetails.executionTimeMs() >= 0 40 | !executionDetails.sqlException().isPresent() 41 | } 42 | 43 | def "SQLException propagated by the listener"() { 44 | given: 45 | preparedStatement.executeUpdate() >> { throw new SQLException() } 46 | when: 47 | query.update(sql).run() 48 | then: 49 | thrown(FluentJdbcSqlException) 50 | executionDetails != null 51 | !executionDetails.success() 52 | executionDetails.sql() == sql 53 | executionDetails.executionTimeMs() >= 0 54 | executionDetails.sqlException().isPresent() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/UpdateTestBase.groovy: -------------------------------------------------------------------------------- 1 | package org.codejargon.fluentjdbc.internal.query 2 | 3 | import org.codejargon.fluentjdbc.api.FluentJdbc 4 | import org.codejargon.fluentjdbc.api.FluentJdbcBuilder 5 | import org.codejargon.fluentjdbc.api.integration.ConnectionProvider 6 | import org.codejargon.fluentjdbc.api.query.Query 7 | import org.junit.Before 8 | import spock.lang.Specification 9 | 10 | import java.sql.Connection 11 | import java.sql.PreparedStatement 12 | import java.sql.SQLException 13 | 14 | public abstract class UpdateTestBase extends Specification { 15 | protected static final def sql = "UPDATE FOO SET BAR = 'x' WHERE COL1 = ? AND COL2 = ?" 16 | protected static final def sqlWithNamedParams = "UPDATE FOO SET BAR = 'x' WHERE COL1 = :param1 AND COL2 = :param2" 17 | protected static final def param1 = "lille" 18 | protected static final def param2 = "lamb" 19 | 20 | protected def connection = Mock(Connection) 21 | protected def preparedStatement = Mock(PreparedStatement) 22 | protected ConnectionProvider connectionProvider 23 | protected def connectionProvided = 0; 24 | 25 | protected FluentJdbc fluentJdbc 26 | protected Query query 27 | 28 | @Before 29 | def setupBase() throws SQLException { 30 | connection.prepareStatement(sql) >> preparedStatement 31 | connectionProvider = { q -> 32 | connectionProvided++ 33 | q.receive(connection) 34 | } 35 | fluentJdbc = new FluentJdbcBuilder().connectionProvider(connectionProvider).build() 36 | query = fluentJdbc.query() 37 | } 38 | 39 | protected Map namedParams() { 40 | return ["param1": param1, "param2": param2] 41 | } 42 | } 43 | --------------------------------------------------------------------------------