├── .gitignore ├── CNAME ├── LICENSE ├── NOTICE ├── README.md ├── _config.yml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── maven_push.gradle ├── settings.gradle └── src ├── main └── java │ └── com │ └── codurance │ └── lightaccess │ ├── LightAccess.java │ ├── connection │ ├── CallableStatementBuilder.java │ ├── LAConnection.java │ ├── PreparedStatementBuilder.java │ └── StatementBuilder.java │ ├── executables │ ├── Command.java │ ├── DDLCommand.java │ ├── SQLCommand.java │ ├── SQLQuery.java │ └── Throwables.java │ └── mapping │ ├── KeyValue.java │ ├── LAResultSet.java │ └── OneToMany.java └── test └── java ├── com └── codurance │ └── lightaccess │ ├── executables │ └── ThrowablesShould.java │ └── mapping │ ├── KeyValueShould.java │ ├── LAResultSetShould.java │ └── OneToManyShould.java └── integration ├── JoinsIntegrationTest.java ├── LightAccessIntegrationTest.java └── dtos ├── Product.java ├── ProductID.java ├── User.java ├── UserWithWishList.java ├── WishList.java └── WishListProduct.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # Mongo Explorer plugin: 25 | .idea/**/mongoSettings.xml 26 | 27 | ## File-based project format: 28 | *.iws 29 | 30 | ## Plugin-specific files: 31 | 32 | # IntelliJ 33 | /out/ 34 | *.iml 35 | 36 | # mpeltonen/sbt-idea plugin 37 | .idea_modules/ 38 | 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | fabric.properties 47 | ### Vim template 48 | # swap 49 | [._]*.s[a-v][a-z] 50 | [._]*.sw[a-p] 51 | [._]s[a-v][a-z] 52 | [._]sw[a-p] 53 | # session 54 | Session.vim 55 | # temporary 56 | .netrwhist 57 | *~ 58 | # auto-generated tag files 59 | tags 60 | ### NetBeans template 61 | nbproject/private/ 62 | build/ 63 | nbbuild/ 64 | dist/ 65 | nbdist/ 66 | .nb-gradle/ 67 | ### Eclipse template 68 | 69 | .metadata 70 | bin/ 71 | tmp/ 72 | *.tmp 73 | *.bak 74 | *.swp 75 | *~.nib 76 | local.properties 77 | .settings/ 78 | .loadpath 79 | .recommenders 80 | 81 | # Eclipse Core 82 | .project 83 | 84 | # External tool builders 85 | .externalToolBuilders/ 86 | 87 | # Locally stored "Eclipse launch configurations" 88 | *.launch 89 | 90 | # PyDev specific (Python IDE for Eclipse) 91 | *.pydevproject 92 | 93 | # CDT-specific (C/C++ Development Tooling) 94 | .cproject 95 | 96 | # JDT-specific (Eclipse Java Development Tools) 97 | .classpath 98 | 99 | # Java annotation processor (APT) 100 | .factorypath 101 | 102 | # PDT-specific (PHP Development Tools) 103 | .buildpath 104 | 105 | # sbteclipse plugin 106 | .target 107 | 108 | # Tern plugin 109 | .tern-project 110 | 111 | # TeXlipse plugin 112 | .texlipse 113 | 114 | # STS (Spring Tool Suite) 115 | .springBeans 116 | 117 | # Code Recommenders 118 | .recommenders/ 119 | 120 | # Scala IDE specific (Scala & Java development for Eclipse) 121 | .cache-main 122 | .scala_dependencies 123 | .worksheet 124 | ### Maven template 125 | target/ 126 | pom.xml.tag 127 | pom.xml.releaseBackup 128 | pom.xml.versionsBackup 129 | pom.xml.next 130 | release.properties 131 | dependency-reduced-pom.xml 132 | buildNumber.properties 133 | .mvn/timing.properties 134 | 135 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 136 | !/.mvn/wrapper/maven-wrapper.jar 137 | ### Gradle template 138 | .gradle 139 | /build/ 140 | 141 | # Ignore Gradle GUI config 142 | gradle-app.setting 143 | 144 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 145 | !gradle-wrapper.jar 146 | 147 | # Cache of project 148 | .gradletasknamecache 149 | 150 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 151 | # gradle/wrapper/gradle-wrapper.properties 152 | ### Emacs template 153 | # -*- mode: gitignore; -*- 154 | *~ 155 | \#*\# 156 | /.emacs.desktop 157 | /.emacs.desktop.lock 158 | *.elc 159 | auto-save-list 160 | tramp 161 | .\#* 162 | 163 | # Org-mode 164 | .org-id-locations 165 | *_archive 166 | 167 | # flymake-mode 168 | *_flymake.* 169 | 170 | # eshell files 171 | /eshell/history 172 | /eshell/lastdir 173 | 174 | # elpa packages 175 | /elpa/ 176 | 177 | # reftex files 178 | *.rel 179 | 180 | # AUCTeX auto folder 181 | /auto/ 182 | 183 | # cask packages 184 | .cask/ 185 | dist/ 186 | 187 | # Flycheck 188 | flycheck_*.el 189 | 190 | # server auth directory 191 | /server/ 192 | 193 | # projectiles files 194 | .projectile 195 | 196 | # directory configuration 197 | .dir-locals.el 198 | 199 | .idea/ 200 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | light-access.codurance.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Codurance 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. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/codurance/light-access/tree/master.svg?style=shield)](https://circleci.com/gh/codurance/light-access/tree/master) 2 | 3 | Light Access 4 | ============ 5 | 6 | Light Access is a small library on top of [JDBC][1], under the [Apache 2](https://www.apache.org/licenses/LICENSE-2.0) license. 7 | 8 | # Who should use this library? 9 | 10 | This library is for Java developers who: 11 | 12 | * Want full control of their code. 13 | * Want a nice and fluid API on top of JDBC, using lambdas. 14 | * Prefer to use non-intrusive small libraries instead of intrusive ORM frameworks. 15 | * Want to reduce boiler plate code from their repositories. 16 | * Don't want to deal with JDBC's complexities and annoying exception handling. 17 | * Don't like to use any sort of automatic binding between their data structures and database. 18 | 19 | 20 | # Table of Contents 21 | 22 | 1. [Installing LightAccess](#installation) 23 | 2. [Getting started](#start) 24 | 3. [DDL statements](#ddlstatements) 25 | 1. [DDLCommand](#ddlcommand) 26 | 2. [Executing multiple DDL commands](#multipleddlstatements) 27 | 4. [DML statements](#dmlstatements) 28 | 1. [Select - single result](#selectsingleresult) 29 | 2. [Select - multiple results](#selectmultipleresults) 30 | 3. [Normalising one to many joins](#onetomanyjoins) 31 | 4. [Insert](#insert) 32 | 5. [Update](#update) 33 | 6. [Delete](#delete) 34 | 7. [Statement, PreparedStatement and CallableStatement](#jdbcstatements) 35 | 5. [Further documentation](#furtherdocumentation) 36 | 1. [Databases tested](#databases) 37 | 6. [History](#history) 38 | 39 | 40 | 41 | ## Installing Light Access 42 | 43 | **Maven** 44 | 45 | 46 | com.codurance 47 | light-access 48 | 0.1.0 49 | 50 | 51 | **Gradle** 52 | 53 | compile group: 'com.codurance', name: 'light-access', version: '0.1.0' 54 | 55 | 56 | ## Getting started 57 | 58 | The main class to look at is [LightAccess][2]. We recommend to have this class injected into your [repositories][3]. 59 | 60 | LightAccess receives a Datasource in its constructor and you can pass a connection pool to it. Let's do it using [h2][4]. 61 | 62 | ```Java 63 | import com.codurance.lightaccess.LightAccess; 64 | import org.h2.jdbcx.JdbcConnectionPool; 65 | ``` 66 | 67 | ```Java 68 | JdbcConnectionPool jdbcConnectionPool = JdbcConnectionPool.create("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "user", "password"); 69 | LightAccess lightAccess = new LightAccess(jdbcConnectionPool); 70 | ``` 71 | 72 | 73 | ## Executing DDL statements 74 | 75 | First let's define a DDL statement which create a table called 'products' with 3 fields. 76 | 77 | ```java 78 | private static final String CREATE_PRODUCTS_TABLE = 79 | "CREATE TABLE products (id integer PRIMARY KEY, name VARCHAR(255), date TIMESTAMP)"; 80 | ``` 81 | 82 | So now, the only thing we need to do is to use the LightAccess to execute this DDL command. 83 | 84 | ```java 85 | lightAccess.executeDDLCommand((conn) -> conn.statement(CREATE_PRODUCTS_TABLE).execute()); 86 | ``` 87 | 88 | And that's it. No exception handling or dealings with database connections. It is all handled for you. 89 | 90 | Alternatively, you can extract the lambda to a method. 91 | 92 | ```java 93 | private DDLCommand createProductsTable() { 94 | return (conn) -> conn.statement(CREATE_PRODUCTS_TABLE).execute(); 95 | } 96 | ``` 97 | 98 | And use it like this. 99 | 100 | ```java 101 | lightAccess.executeDDLCommand(createProductsTable()); 102 | ``` 103 | 104 | 105 | ### DDLCommand 106 | 107 | The `LightAccess.executeDDLCommand(DDLCommand command)` receives a `DDLCommand` as parameter. 108 | 109 | ```java 110 | public interface DDLCommand { 111 | void execute(LAConnection connection) throws SQLException; 112 | } 113 | ``` 114 | 115 | With that, you can pass in any lambda that satisfy the `execute(LAConnection connection)` method signature. 116 | 117 | 118 | ### Executing multiple DDL statements 119 | 120 | It is possible to execute multiple commands in one go: 121 | 122 | ```java 123 | private static final String CREATE_USERS_TABLE = "CREATE TABLE users (userId integer PRIMARY KEY, name VARCHAR(255))"; 124 | private static final String CREATE_WISHLISTS_TABLE = "CREATE TABLE wishlists (wishListId integer PRIMARY KEY, userId integer, name VARCHAR(255), creationDate TIMESTAMP)"; 125 | private static final String CREATE_PRODUCTS_TABLE = "CREATE TABLE products (productId integer PRIMARY KEY, name VARCHAR(255), date TIMESTAMP)"; 126 | private static final String CREATE_WISHLIST_PRODUCT_TABLE = "CREATE TABLE wishlist_product (id integer PRIMARY KEY, wishListId integer, productId integer)"; 127 | ``` 128 | 129 | ```java 130 | public void create_all_tables() { 131 | lightAccess.executeDDLCommand(createTables()); 132 | } 133 | 134 | private DDLCommand createTables() { 135 | return (conn) -> { 136 | conn.statement(CREATE_USERS_TABLE).execute(); 137 | conn.statement(CREATE_WISHLISTS_TABLE).execute(); 138 | conn.statement(CREATE_PRODUCTS_TABLE).execute(); 139 | conn.statement(CREATE_WISHLIST_PRODUCT_TABLE).execute(); 140 | }; 141 | } 142 | ``` 143 | 144 | 145 | ## Executing DML statements 146 | 147 | Let's assume we have an object `Product` that we want to map to the `products` table. 148 | 149 | ```java 150 | public class Product { 151 | private int id; 152 | private String name; 153 | private LocalDate date; 154 | 155 | Product(int id, String name, LocalDate date) { 156 | this.id = id; 157 | this.name = name; 158 | this.date = date; 159 | } 160 | 161 | // getters 162 | 163 | // equals and hashcode 164 | } 165 | ``` 166 | 167 | 168 | ### Select - single result 169 | 170 | Let's take the following select statement. 171 | 172 | ```java 173 | private static final String SELECT_PRODUCT_BY_ID_SQL = "select * from products where id = ?"; 174 | ``` 175 | 176 | Now let's create a method that returns a lambda for this select statement. As we are looking for a single entity and 177 | we may not find it, it would be good if our query (`SQLQuery`) returned an `Optional`. 178 | 179 | ```java 180 | private SQLQuery> retrieveProductWithId(int id) { 181 | return conn -> conn.prepareStatement(SELECT_PRODUCT_BY_ID_SQL) 182 | .withParam(id) 183 | .executeQuery() 184 | .onlyResult(this::toProduct); 185 | } 186 | ``` 187 | 188 | In case we find a product with this ID, we need to map the result set to the Product object. This is done in the 189 | `toProduct` method passed to `.onlyResult()` above. 190 | 191 | ```java 192 | private Product toProduct(LAResultSet laResultSet) { 193 | return new Product(laResultSet.getInt(1), 194 | laResultSet.getString(2), 195 | laResultSet.getLocalDate(3)); 196 | } 197 | ``` 198 | 199 | Now we only need to execute the select statement. 200 | 201 | ```java 202 | Optional product = lightAccess.executeQuery(retrieveProductWithId(10)); 203 | ``` 204 | 205 | In case you prefer an inline version, you can use: 206 | 207 | ```java 208 | Optional product = lightAccess.executeQuery(conn -> conn.prepareStatement(SELECT_PRODUCT_BY_ID_SQL) 209 | .withParam(PRODUCT_TWO.id) 210 | .executeQuery() 211 | .onlyResult(this::toProduct)); 212 | ``` 213 | 214 | 215 | ### Select - multiple results 216 | 217 | Let's take the following select statement: 218 | 219 | ```java 220 | private static final String SELECT_ALL_PRODUCTS_SQL = "select * from products"; 221 | ``` 222 | 223 | Now let's create a method that returns a lambda: 224 | 225 | ```java 226 | private SQLQuery> retrieveAllProducts() { 227 | return conn -> conn.prepareStatement(SELECT_ALL_PRODUCTS_SQL) 228 | .executeQuery() 229 | .mapResults(this::toProduct); 230 | } 231 | ``` 232 | 233 | Note that now we are calling `mapResults(this::toProduct)` instead of `onlyResult(this::toProduct)`, and the `SQLQuery` 234 | is parameterised to return `List`. 235 | 236 | Now we just need to invoke the query like before. 237 | 238 | ```java 239 | List products = lightAccess.executeQuery(retrieveAllProducts()); 240 | ``` 241 | 242 | And in case you prefer the inlined version: 243 | 244 | ```java 245 | List products = lightAccess.executeQuery(conn -> conn.prepareStatement(SELECT_ALL_PRODUCTS_SQL) 246 | .executeQuery() 247 | .mapResults(this::toProduct)); 248 | ``` 249 | 250 | 251 | ### Normalising one to many joins 252 | 253 | Let's say we have a table with users and a table with wish lists: 254 | 255 | ```sql 256 | CREATE TABLE users (userId integer PRIMARY KEY, name VARCHAR(255)); 257 | CREATE TABLE wishlists (wishListId integer PRIMARY KEY, userId integer, name VARCHAR(255), creationDate TIMESTAMP); 258 | ``` 259 | 260 | Now let's assume we want to have all users and their respective wish lists, including the users without wish list. 261 | 262 | ```sql 263 | select u.userId, u.name, w.wishListId, w.userId, w.name, w.creationDate 264 | from users u 265 | left join wishlists w on u.userId = w.userId 266 | ``` 267 | 268 | We want the result to be stored in a list containing the following DTO: 269 | 270 | ```java 271 | public class UserWithWishList { 272 | 273 | private final User user; 274 | private final List wishLists; 275 | 276 | public UserWithWishList(User user, List wishLists) { 277 | this.user = user; 278 | this.wishLists = unmodifiableList(wishLists); 279 | } 280 | 281 | // getters, equals, hashcode. 282 | } 283 | ``` 284 | 285 | For this to work we need to have a DTO for user and a DTO for the wish list: 286 | 287 | ```java 288 | public class User { 289 | 290 | private final Integer id; 291 | private final String name; 292 | 293 | public User(Integer id, String name) { 294 | this.id = id; 295 | this.name = name; 296 | } 297 | 298 | // getters, equals, hashcode. 299 | } 300 | ``` 301 | 302 | ```java 303 | public class WishList { 304 | 305 | private final Integer id; 306 | private final Integer userId; 307 | private final String name; 308 | private final LocalDate creationDate; 309 | 310 | public WishList(Integer id, Integer userId, String name, LocalDate creationDate) { 311 | this.id = id; 312 | this.userId = userId; 313 | this.name = name; 314 | this.creationDate = creationDate; 315 | } 316 | } 317 | ``` 318 | 319 | So now we are ready to get a list of `UserWithWishList` objects: 320 | 321 | ```java 322 | 323 | public List usersWithWishLists() { 324 | OneToMany wishListsPerUser = lightAccess.executeQuery((conn -> 325 | conn.prepareStatement(SELECT_WISHLISTS_PER_USER_SQL) 326 | .executeQuery() 327 | .normaliseOneToMany(this::mapToUserWishList))) 328 | 329 | return wishListsPerUser.collect((user, wishLists) -> new UserWithWishList(user, wishLists)); 330 | } 331 | 332 | private KeyValue> mapToUserWishList(LAResultSet laResultSet) { 333 | User user = new User(laResultSet.getInt(1), laResultSet.getString(2)); 334 | 335 | Optional wishList = Optional.ofNullable((laResultSet.getInt(3) > 0) 336 | ? new WishList(laResultSet.getInt(3), 337 | laResultSet.getInt(4), 338 | laResultSet.getString(5), 339 | laResultSet.getLocalDate(6)) 340 | : null); 341 | return new KeyValue<>(user, wishList); 342 | } 343 | ``` 344 | 345 | For more details, please check the [integration tests for joins][5] 346 | 347 | 348 | ### Insert 349 | 350 | Let's assume we have the following product table: 351 | 352 | ```sql 353 | CREATE TABLE products (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), date TIMESTAMP) 354 | ``` 355 | 356 | And we have the following Product DTO. 357 | 358 | ```java 359 | public class Product { 360 | private int id; 361 | private String name; 362 | private LocalDate date; 363 | 364 | public Product(int id, String name, LocalDate date) { 365 | this.id = id; 366 | this.name = name; 367 | this.date = date; 368 | } 369 | 370 | public int id() { 371 | return id; 372 | } 373 | 374 | public String name() { 375 | return name; 376 | } 377 | 378 | public LocalDate date() { 379 | return date; 380 | } 381 | 382 | // equals, hashcode 383 | } 384 | ``` 385 | 386 | For inserting a product, we just need to do the following: 387 | 388 | ```java 389 | INSERT_PRODUCT_SQL = "insert into products (id, name, date) values (?, ?, ?)"; 390 | 391 | Product product = new Product(1, "Product 1", LocalDate.of(2017, 07, 26)); 392 | 393 | lightAccess.executeCommand(conn -> conn.prepareStatement(INSERT_PRODUCT_SQL) 394 | .withParam(product.id()) 395 | .withParam(product.name()) 396 | .withParam(product.date()) 397 | .executeUpdate()); 398 | ``` 399 | 400 | And as always, can extract the lambda to a method: 401 | 402 | ```java 403 | private SQLCommand insert(Product product) { 404 | return conn -> conn.prepareStatement(INSERT_PRODUCT_SQL) 405 | .withParam(product.id()) 406 | .withParam(product.name()) 407 | .withParam(product.date()) 408 | .executeUpdate(); 409 | } 410 | ``` 411 | 412 | And call it like that: 413 | 414 | ```java 415 | lightAccess.executeCommand(insert(produt)); 416 | ``` 417 | 418 | 419 | ### Update 420 | 421 | Let's say that we wan to update the name of the given product. 422 | 423 | ```java 424 | private static final String UPDATE_PRODUCT_NAME_SQL = "update products set name = ? where id = ?"; 425 | ``` 426 | 427 | Now we can execute the update: 428 | 429 | ```java 430 | lightAccess.executeCommand(updateProductName(1, "Another name")); 431 | ``` 432 | 433 | ```java 434 | private SQLCommand updateProductName(int id, String name) { 435 | return conn -> conn.prepareStatement(UPDATE_PRODUCT_NAME_SQL) 436 | .withParam(name) 437 | .withParam(id) 438 | .executeUpdate(); 439 | } 440 | ``` 441 | 442 | 443 | ### Delete 444 | 445 | Delete is exactly the same as inserts and updates. 446 | 447 | ## Calling sequences (PostgreSQL / H2) 448 | 449 | Let's first create a sequence: 450 | 451 | ```java 452 | private static final String ID_SEQUENCE = "id_sequence"; 453 | private static final String CREATE_SEQUENCE_DDL = "CREATE SEQUENCE " + ID_SEQUENCE + " START WITH 1"; 454 | ``` 455 | 456 | ```java 457 | lightAccess.executeDDLCommand((conn) -> conn.statement(CREATE_SEQUENCE_DDL).execute()); 458 | ``` 459 | 460 | Now we can read the next ID from it. 461 | 462 | ```java 463 | int id = lightAccess.nextId(ID_SEQUENCE); 464 | ``` 465 | 466 | In case we don't want an int ID, we can also map the ID to something else: 467 | 468 | ```java 469 | ProductID secondId = lightAccess.nextId(ID_SEQUENCE, ProductID::new); 470 | ``` 471 | 472 | Where `ProductID` is: 473 | 474 | ```java 475 | public class ProductID { 476 | private int id; 477 | 478 | public ProductID(int id) { 479 | this.id = id; 480 | } 481 | 482 | // getter, equals, hashcode 483 | } 484 | ``` 485 | 486 | We can also map that to String or any other object: 487 | 488 | ```java 489 | String stringID = lightAccess.nextId(ID_SEQUENCE, Object::toString); 490 | ``` 491 | 492 | 493 | ### Creating Statement, PreparedStatement and CallableStatement 494 | 495 | An instance of `LAConnection` will be received in all queries and commands represented by `DDLCommand`, `SQLCommand` 496 | and `SQLQuery`. With this instance you can create a [Statement][6], [PreparedStatement][7] and [CallableStatement][8], 497 | according to your need. 498 | 499 | As a guideline, we normally use a `Statement` for DDL, a `PreparedStatement` for DML and `CallableStatement` for calling 500 | stored procedures or sequences. 501 | 502 | 503 | # Further documentation 504 | 505 | Please check the [tests][9] for more details in how to use this library. 506 | 507 | 508 | ### Databases tested 509 | 510 | We have only tested this library with [Amazon RDS][10] for [PostgreSQL][11]. 511 | 512 | 513 | ## History 514 | 515 | This library was first created by [Sandro Mancuso][12] while refactoring and removing duplication from multiple 516 | repositories in one of the [Codurance][13]'s internal projects. 517 | 518 | [1]: https://docs.oracle.com/javase/tutorial/jdbc/basics/ 519 | [2]: https://github.com/codurance/light-access/blob/master/src/main/java/com/codurance/lightaccess/LightAccess.java 520 | [3]: https://martinfowler.com/eaaCatalog/repository.html 521 | [4]: http://www.h2database.com/html/main.html 522 | [5]: https://github.com/codurance/light-access/blob/master/src/test/java/integration/JoinsIntegrationTest.java 523 | [6]: https://docs.oracle.com/javase/8/docs/api/java/sql/Statement.html 524 | [7]: https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html 525 | [8]: https://docs.oracle.com/javase/8/docs/api/java/sql/CallableStatement.html 526 | [9]: https://github.com/codurance/light-access/tree/master/src/test/java 527 | [10]: https://aws.amazon.com/rds/ 528 | [11]: https://www.postgresql.org/ 529 | [12]: http://twitter.com/sandromancuso 530 | [13]: http://codurance.com -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.codurance' 2 | version '0.1.0' 3 | 4 | apply plugin: 'java' 5 | 6 | sourceCompatibility = 1.8 7 | targetCompatibility = 1.8 8 | 9 | dependencies { 10 | compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.5' 11 | 12 | testCompile group: 'junit', name: 'junit', version: '4.12' 13 | testCompile group: 'org.mockito', name: 'mockito-core', version: '2.8.9' 14 | testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3' 15 | testCompile group: 'org.assertj', name: 'assertj-core', version: '3.6.2' 16 | 17 | testCompile 'com.h2database:h2:1.4.196' 18 | } 19 | 20 | def isReleaseBuild() { 21 | return version.contains("SNAPSHOT") == false 22 | } 23 | 24 | repositories { 25 | mavenCentral() 26 | jcenter() 27 | } 28 | 29 | // Used to push in maven 30 | apply from: './maven_push.gradle' -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=0.1.0 2 | VERSION_CODE=2 3 | GROUP=com.codurance 4 | 5 | POM_ARTIFACT_ID=light-access 6 | POM_NAME=light-access.pom 7 | POM_PACKAGING=jar 8 | 9 | POM_DESCRIPTION=Non-intrusive JDBC library supporting lambdas 10 | POM_URL=https://github.com/codurance/light-access 11 | POM_SCM_URL=https://github.com/codurance/light-access 12 | POM_SCM_CONNECTION=scm:git:git@github.com:codurance/light-access.git 13 | POM_SCM_DEV_CONNECTION=scm:git:git@github.com:codurance/light-access.git 14 | POM_LICENCE_NAME=AGPL-3.0 15 | POM_LICENCE_URL=https://opensource.org/licenses/AGPL-3.0 16 | POM_LICENCE_DIST=repo 17 | POM_DEVELOPER_ID=sandromancuso 18 | POM_DEVELOPER_NAME=Sandro Mancuso 19 | 20 | SNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots 21 | RELEASE_REPOSITORY_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codurance/light-access/d5443ca42838c7a131672dd90f2e3f23ae0d4c64/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 25 23:04:31 BST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /maven_push.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | 4 | def sonatypeRepositoryUrl 5 | if (isReleaseBuild()) { 6 | println 'RELEASE BUILD' 7 | sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 8 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 9 | } else { 10 | println 'DEBUG BUILD' 11 | sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 12 | : "https://oss.sonatype.org/content/repositories/snapshots/" 13 | } 14 | 15 | def getRepositoryUsername() { 16 | return hasProperty('ossrhUser') ? ossrhUser : "" 17 | } 18 | 19 | def getRepositoryPassword() { 20 | return hasProperty('ossrhPassword') ? ossrhPassword : "" 21 | } 22 | 23 | afterEvaluate { project -> 24 | uploadArchives { 25 | repositories { 26 | mavenDeployer { 27 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 28 | 29 | pom.artifactId = POM_ARTIFACT_ID 30 | 31 | repository(url: sonatypeRepositoryUrl) { 32 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 33 | } 34 | 35 | pom.project { 36 | name POM_NAME 37 | packaging POM_PACKAGING 38 | description POM_DESCRIPTION 39 | url POM_URL 40 | 41 | scm { 42 | url POM_SCM_URL 43 | connection POM_SCM_CONNECTION 44 | developerConnection POM_SCM_DEV_CONNECTION 45 | } 46 | 47 | licenses { 48 | license { 49 | name POM_LICENCE_NAME 50 | url POM_LICENCE_URL 51 | distribution POM_LICENCE_DIST 52 | } 53 | } 54 | 55 | developers { 56 | developer { 57 | id POM_DEVELOPER_ID 58 | name POM_DEVELOPER_NAME 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | signing { 67 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 68 | sign configurations.archives 69 | } 70 | 71 | 72 | task sourcesJar(type: Jar) { 73 | from sourceSets.main.java.srcDirs 74 | classifier = 'sources' 75 | } 76 | 77 | task javadocJar(type: Jar, dependsOn: javadoc) { 78 | classifier = 'javadoc' 79 | from javadoc.destinationDir 80 | } 81 | 82 | artifacts { 83 | archives javadocJar 84 | archives sourcesJar 85 | } 86 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'light-access' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/LightAccess.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess; 2 | 3 | import com.codurance.lightaccess.connection.LAConnection; 4 | import com.codurance.lightaccess.executables.*; 5 | 6 | import javax.sql.DataSource; 7 | import java.sql.SQLException; 8 | import java.util.function.Function; 9 | 10 | import static com.codurance.lightaccess.executables.Throwables.executeWithResource; 11 | import static java.lang.String.format; 12 | 13 | public class LightAccess { 14 | 15 | private static final String SEQUENCE_CALL_SQL = "select nextval('%s')"; 16 | 17 | private DataSource ds; 18 | 19 | public LightAccess(DataSource connection) { 20 | this.ds = connection; 21 | } 22 | 23 | public T executeQuery(SQLQuery sqlQuery) { 24 | LAConnection conn = pgConnection(); 25 | return executeWithResource(conn, () -> sqlQuery.execute(conn)); 26 | } 27 | 28 | public void executeCommand(SQLCommand sqlCommand) { 29 | execute(sqlCommand); 30 | } 31 | 32 | public void executeDDLCommand(DDLCommand ddlCommand) { 33 | execute(ddlCommand); 34 | } 35 | 36 | public int nextId(String sequenceName) { 37 | return nextId(sequenceName, x -> x); 38 | } 39 | 40 | public T nextId(String sequenceName, Function nextId) { 41 | LAConnection conn = pgConnection(); 42 | return executeWithResource(conn, () -> nextId.apply(sequenceNextId(sequenceName, conn))); 43 | } 44 | 45 | private int sequenceNextId(String sequenceName, LAConnection conn) throws SQLException { 46 | String sql = format(SEQUENCE_CALL_SQL, sequenceName); 47 | return conn.callableStatement(sql) 48 | .executeQuery() 49 | .nextRecord() 50 | .getInt(1); 51 | } 52 | 53 | private void execute(Command command) { 54 | LAConnection conn = pgConnection(); 55 | executeWithResource(conn, () -> command.execute(conn)); 56 | } 57 | 58 | private LAConnection pgConnection() { 59 | return Throwables.executeQuery(() -> new LAConnection(ds.getConnection())); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/connection/CallableStatementBuilder.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.connection; 2 | 3 | import com.codurance.lightaccess.executables.Throwables; 4 | import com.codurance.lightaccess.mapping.LAResultSet; 5 | 6 | import java.sql.CallableStatement; 7 | import java.sql.Connection; 8 | 9 | public class CallableStatementBuilder { 10 | 11 | private CallableStatement callableStatement; 12 | 13 | public CallableStatementBuilder(Connection connection, String sql) { 14 | this.callableStatement = Throwables.executeQuery(() -> connection.prepareCall(sql)); 15 | } 16 | 17 | public LAResultSet executeQuery() { 18 | return Throwables.executeQuery(() -> new LAResultSet(callableStatement.executeQuery())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/connection/LAConnection.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.connection; 2 | 3 | import java.sql.Connection; 4 | 5 | public class LAConnection implements AutoCloseable { 6 | 7 | private Connection connection; 8 | 9 | public LAConnection(Connection connection) { 10 | this.connection = connection; 11 | } 12 | 13 | /** 14 | * Used for SQL statements with parameters and that can be executed 15 | * multiple times. 16 | * 17 | * @param sql SQL query or command. 18 | * @return 19 | */ 20 | public PreparedStatementBuilder prepareStatement(String sql) { 21 | return new PreparedStatementBuilder(connection, sql); 22 | } 23 | 24 | /** 25 | * Used for DDL commands. 26 | * 27 | * @param ddl DDL statement. 28 | * @return 29 | */ 30 | public StatementBuilder statement(String ddl) { 31 | return new StatementBuilder(connection, ddl); 32 | } 33 | 34 | /** 35 | * Used for invoking stored procedures and sequences. 36 | * 37 | * @param sql SQL statement for calling stored procedures or sequences. 38 | * @return 39 | */ 40 | public CallableStatementBuilder callableStatement(String sql) { 41 | return new CallableStatementBuilder(connection, sql); 42 | } 43 | 44 | /** 45 | * Closes the connection. 46 | * 47 | * @throws Exception 48 | */ 49 | @Override 50 | public void close() throws Exception { 51 | connection.close(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/connection/PreparedStatementBuilder.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.connection; 2 | 3 | import com.codurance.lightaccess.executables.Throwables; 4 | import com.codurance.lightaccess.mapping.LAResultSet; 5 | 6 | import java.sql.Connection; 7 | import java.sql.Date; 8 | import java.sql.PreparedStatement; 9 | import java.time.LocalDate; 10 | 11 | import static com.codurance.lightaccess.executables.Throwables.execute; 12 | 13 | public class PreparedStatementBuilder { 14 | 15 | private PreparedStatement preparedStatement; 16 | private int paramIndex = 0; 17 | 18 | PreparedStatementBuilder(Connection connection, String sql) { 19 | execute(() -> this.preparedStatement = connection.prepareStatement(sql)); 20 | } 21 | 22 | public PreparedStatementBuilder withParam(String param) { 23 | return withParam((paramIndex) -> execute(() -> preparedStatement.setString(paramIndex, param))); 24 | } 25 | 26 | public PreparedStatementBuilder withParam(int param) { 27 | return withParam((paramIndex) -> execute(() -> preparedStatement.setInt(paramIndex, param))); 28 | } 29 | 30 | public PreparedStatementBuilder withParam(LocalDate param) { 31 | return withParam((paramIndex) -> execute(() -> preparedStatement.setDate(paramIndex, Date.valueOf(param)))); 32 | } 33 | 34 | public void executeUpdate() { 35 | execute(() -> { 36 | preparedStatement.executeUpdate(); 37 | preparedStatement.close(); 38 | }); 39 | 40 | } 41 | 42 | public LAResultSet executeQuery() { 43 | return Throwables.executeQuery(() -> new LAResultSet(preparedStatement.executeQuery())); 44 | } 45 | 46 | private interface SetParam { 47 | void execute(int paramCount); 48 | } 49 | 50 | private PreparedStatementBuilder withParam(SetParam setParam) { 51 | paramIndex += 1; 52 | execute(() -> setParam.execute(paramIndex)); 53 | return this; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/connection/StatementBuilder.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.connection; 2 | 3 | import com.codurance.lightaccess.executables.Throwables; 4 | 5 | import java.sql.Connection; 6 | import java.sql.Statement; 7 | 8 | import static com.codurance.lightaccess.executables.Throwables.executeWithResource; 9 | 10 | public class StatementBuilder { 11 | 12 | private Statement statement; 13 | private String sql; 14 | 15 | StatementBuilder(Connection connection, String sql) { 16 | Throwables.execute(() -> this.statement = connection.createStatement()); 17 | this.sql = sql; 18 | } 19 | 20 | public void execute() { 21 | executeWithResource(statement, () -> statement.execute(sql)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/executables/Command.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.executables; 2 | 3 | import com.codurance.lightaccess.connection.LAConnection; 4 | 5 | import java.sql.SQLException; 6 | 7 | @FunctionalInterface 8 | public interface Command { 9 | void execute(LAConnection connection) throws SQLException; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/executables/DDLCommand.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.executables; 2 | 3 | public interface DDLCommand extends Command {} 4 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/executables/SQLCommand.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.executables; 2 | 3 | public interface SQLCommand extends Command {} 4 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/executables/SQLQuery.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.executables; 2 | 3 | import com.codurance.lightaccess.connection.LAConnection; 4 | 5 | import java.sql.SQLException; 6 | 7 | @FunctionalInterface 8 | public interface SQLQuery { 9 | T execute(LAConnection connection) throws SQLException; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/executables/Throwables.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.executables; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | public class Throwables { 6 | 7 | @FunctionalInterface 8 | public interface Command { 9 | void execute() throws Exception; 10 | } 11 | 12 | @FunctionalInterface 13 | public interface Query extends Callable { 14 | } 15 | 16 | @FunctionalInterface 17 | public interface ExceptionWrapper { 18 | E wrap(Exception e); 19 | } 20 | 21 | public static void execute(Command command) throws RuntimeException { 22 | execute(command, RuntimeException::new); 23 | } 24 | 25 | public static void execute(Command command, ExceptionWrapper wrapper) throws E { 26 | try { 27 | command.execute(); 28 | } catch (RuntimeException e) { 29 | throw e; 30 | } catch (Exception e) { 31 | throw wrapper.wrap(e); 32 | } 33 | } 34 | 35 | public static T executeQuery(Query callable) throws RuntimeException { 36 | return executeQuery(callable, RuntimeException::new); 37 | } 38 | 39 | public static T executeQuery(Callable callable, ExceptionWrapper wrapper) throws E { 40 | try { 41 | return callable.call(); 42 | } catch (RuntimeException e) { 43 | throw e; 44 | } catch (Exception e) { 45 | throw wrapper.wrap(e); 46 | } 47 | } 48 | 49 | public static void executeWithResource(T closeableResource, Command command) { 50 | try(T ignored = closeableResource) { 51 | command.execute(); 52 | } catch (Exception e) { 53 | throw new RuntimeException(e); 54 | } 55 | } 56 | 57 | public static R executeWithResource(T closeableResource, Query callable) { 58 | try(T ignored = closeableResource) { 59 | return callable.call(); 60 | } catch (Exception e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/mapping/KeyValue.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.mapping; 2 | 3 | import java.util.Map; 4 | 5 | public class KeyValue implements Map.Entry { 6 | 7 | private final K key; 8 | private V value; 9 | 10 | public KeyValue(K key, V value) { 11 | this.key = key; 12 | this.value = value; 13 | } 14 | 15 | @Override 16 | public K getKey() { 17 | return key; 18 | } 19 | 20 | @Override 21 | public V getValue() { 22 | return value; 23 | } 24 | 25 | /** 26 | * This class should be immutable but this method has to be 27 | * implemented because of the Map.Entry interface. 28 | * 29 | * @see java.util.Map.Entry#setValue(Object) 30 | */ 31 | @Override 32 | public Object setValue(Object value) { 33 | throw new UnsupportedOperationException(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/mapping/LAResultSet.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.mapping; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | import java.text.SimpleDateFormat; 6 | import java.time.LocalDate; 7 | import java.util.*; 8 | import java.util.function.Function; 9 | 10 | import static com.codurance.lightaccess.executables.Throwables.executeQuery; 11 | 12 | public class LAResultSet { 13 | private final SimpleDateFormat YYYY_MM_DD_date_format = new SimpleDateFormat("yyyy-MM-dd"); 14 | private ResultSet resultSet; 15 | 16 | public LAResultSet(ResultSet resultSet) { 17 | this.resultSet = resultSet; 18 | } 19 | 20 | public int getInt(int columnIndex) { 21 | return executeQuery(() -> resultSet.getInt(columnIndex)); 22 | } 23 | 24 | public String getString(int columnIndex) { 25 | String stringValue = executeQuery(() -> resultSet.getString(columnIndex)); 26 | return (stringValue != null) ? stringValue : ""; 27 | } 28 | 29 | public Optional getOptionalString(int columnIndex) { 30 | return Optional.ofNullable(executeQuery(() -> resultSet.getString(columnIndex))); 31 | } 32 | 33 | public LocalDate getLocalDate(int columnIndex) { 34 | return utilDateToLocalDate(getDate(columnIndex)); 35 | } 36 | 37 | public Date getDate(int columnIndex) { 38 | return executeQuery(() -> sqlDateToUtilDate(columnIndex)); 39 | } 40 | 41 | public Optional getOptionalLocalDate(int columnIndex) { 42 | return Optional.ofNullable(getLocalDate(columnIndex)); 43 | } 44 | 45 | public Optional onlyResult(Function mapOne) throws SQLException { 46 | if (resultSet.next()) { 47 | return Optional.of(mapOne.apply(this)); 48 | } 49 | return Optional.empty(); 50 | } 51 | 52 | public List mapResults(Function mapResults) { 53 | List list = new ArrayList<>(); 54 | while (this.next()) { 55 | list.add(mapResults.apply(this)); 56 | } 57 | return list; 58 | } 59 | 60 | public OneToMany normaliseOneToMany(Function>> normalise) { 61 | OneToMany oneToMany = new OneToMany<>(); 62 | while (this.next()) { 63 | KeyValue> kv = normalise.apply(this); 64 | oneToMany.put(kv); 65 | } 66 | return oneToMany; 67 | } 68 | 69 | public LAResultSet nextRecord() { 70 | next(); 71 | return this; 72 | } 73 | 74 | private boolean next() { 75 | return executeQuery(() -> resultSet.next()); 76 | } 77 | 78 | private Date sqlDateToUtilDate(int columnIndex) throws SQLException { 79 | java.sql.Date date = resultSet.getDate(columnIndex); 80 | return (date != null) ? new Date(date.getTime()) : null; 81 | } 82 | 83 | private LocalDate utilDateToLocalDate(Date date) { 84 | return (date != null) ? LocalDate.parse(YYYY_MM_DD_date_format.format(date)) : null; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/codurance/lightaccess/mapping/OneToMany.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.mapping; 2 | 3 | import java.util.*; 4 | import java.util.function.BiFunction; 5 | 6 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 7 | 8 | public class OneToMany { 9 | 10 | private Map> data = new HashMap<>(); 11 | 12 | public void put(KeyValue> keyValue) { 13 | if (keyValue.getValue().isPresent()) { 14 | this.put(keyValue.getKey(), keyValue.getValue()); 15 | } else { 16 | this.put(keyValue.getKey(), Optional.empty()); 17 | } 18 | } 19 | 20 | public void put(K key, Optional value) { 21 | List children = data.getOrDefault(key, new ArrayList<>()); 22 | value.ifPresent(children::add); 23 | data.put(key, children); 24 | } 25 | 26 | public List collect(BiFunction, T> collect) { 27 | List list = new ArrayList<>(); 28 | data.forEach((key, value) -> list.add(collect.apply(key, value))); 29 | return list; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object other) { 34 | return reflectionEquals(this, other); 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return "OneToMany{" + 40 | "data=" + data + 41 | '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/codurance/lightaccess/executables/ThrowablesShould.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.executables; 2 | 3 | import com.codurance.lightaccess.executables.Throwables.Command; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.junit.rules.ExpectedException; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.MockitoJUnit; 9 | import org.mockito.junit.MockitoRule; 10 | 11 | import static org.hamcrest.core.Is.is; 12 | import static org.mockito.Mockito.doThrow; 13 | import static org.mockito.Mockito.verify; 14 | 15 | public class ThrowablesShould { 16 | 17 | private final Throwables throwables = new Throwables(); 18 | 19 | @Rule public MockitoRule mockito = MockitoJUnit.rule(); 20 | 21 | @Rule public ExpectedException expectedException = ExpectedException.none(); 22 | 23 | @Mock Command command; 24 | @Mock Throwables.Query query; 25 | @Mock AutoCloseable resource; 26 | 27 | @Test public void 28 | execute_command() throws Exception { 29 | Throwables.execute(command); 30 | 31 | verify(command).execute(); 32 | } 33 | 34 | @Test public void 35 | throws_command_runtime_exception() throws Exception { 36 | doThrow(new DummyRuntimeException("exception message")).when(command).execute(); 37 | 38 | expectedException.expect(DummyRuntimeException.class); 39 | expectedException.expectMessage(is("exception message")); 40 | 41 | Throwables.execute(command); 42 | } 43 | 44 | @Test public void 45 | wraps_checked_exception_thrown_by_command_with_runtime_wrapper() throws Exception { 46 | DummyCheckedException dummyCheckedException = new DummyCheckedException(); 47 | doThrow(dummyCheckedException).when(command).execute(); 48 | 49 | expectedException.expect(RuntimeException.class); 50 | expectedException.expectCause(is(dummyCheckedException)); 51 | 52 | Throwables.execute(command); 53 | } 54 | 55 | @Test public void 56 | wraps_runtime_checked_exception_thrown_by_command_with_another_checked_exception() throws Exception { 57 | Exception commandException = new Exception(); 58 | doThrow(commandException).when(command).execute(); 59 | 60 | expectedException.expect(DummyCheckedException.class); 61 | expectedException.expectCause(is(commandException)); 62 | 63 | Throwables.execute(command, DummyCheckedException::new); 64 | } 65 | 66 | @Test public void 67 | execute_query() throws Exception { 68 | Throwables.executeQuery(query); 69 | 70 | verify(query).call(); 71 | } 72 | 73 | @Test public void 74 | throws_query_runtime_exception() throws Exception { 75 | doThrow(new DummyRuntimeException("exception message")).when(query).call(); 76 | 77 | expectedException.expect(DummyRuntimeException.class); 78 | expectedException.expectMessage(is("exception message")); 79 | 80 | Throwables.executeQuery(query); 81 | } 82 | 83 | @Test public void 84 | wraps_checked_exception_thrown_by_query_with_runtime_wrapper() throws Exception { 85 | DummyCheckedException dummyCheckedException = new DummyCheckedException(); 86 | doThrow(dummyCheckedException).when(query).call(); 87 | 88 | expectedException.expect(RuntimeException.class); 89 | expectedException.expectCause(is(dummyCheckedException)); 90 | 91 | Throwables.executeQuery(query); 92 | } 93 | 94 | @Test public void 95 | wraps_runtime_checked_exception_thrown_by_query_with_another_checked_exception() throws Exception { 96 | Exception queryException = new Exception(); 97 | doThrow(queryException).when(query).call(); 98 | 99 | expectedException.expect(DummyCheckedException.class); 100 | expectedException.expectCause(is(queryException)); 101 | 102 | Throwables.executeQuery(query, DummyCheckedException::new); 103 | } 104 | 105 | @Test public void 106 | close_resource_when_executing_a_command() throws Exception { 107 | Throwables.executeWithResource(resource, command); 108 | 109 | verify(resource).close(); 110 | } 111 | 112 | @Test public void 113 | close_resource_even_when_a_command_throws_exception() throws Exception { 114 | DummyCheckedException commandException = new DummyCheckedException(); 115 | doThrow(commandException).when(command).execute(); 116 | 117 | expectedException.expect(RuntimeException.class); 118 | expectedException.expectCause(is(commandException)); 119 | 120 | Throwables.executeWithResource(resource, command); 121 | 122 | verify(resource).close(); 123 | } 124 | 125 | @Test public void 126 | close_resource_when_executing_a_query() throws Exception { 127 | Throwables.executeWithResource(resource, query); 128 | 129 | verify(resource).close(); 130 | } 131 | 132 | @Test public void 133 | close_resource_even_when_a_query_throws_exception() throws Exception { 134 | DummyCheckedException queryException = new DummyCheckedException(); 135 | doThrow(queryException).when(query).call(); 136 | 137 | expectedException.expect(RuntimeException.class); 138 | expectedException.expectCause(is(queryException)); 139 | 140 | Throwables.executeWithResource(resource, query); 141 | 142 | verify(resource).close(); 143 | } 144 | 145 | private class DummyRuntimeException extends RuntimeException { 146 | DummyRuntimeException(String message) { 147 | super(message); 148 | } 149 | } 150 | 151 | private class DummyCheckedException extends Exception { 152 | DummyCheckedException(Exception e) { 153 | super(e); 154 | } 155 | 156 | DummyCheckedException() { 157 | super(); 158 | } 159 | } 160 | 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/test/java/com/codurance/lightaccess/mapping/KeyValueShould.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.mapping; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class KeyValueShould { 8 | 9 | @Test public void 10 | return_key_and_value() { 11 | KeyValue keyValue = new KeyValue<>(1, "Value"); 12 | 13 | assertThat(keyValue.getKey()).isEqualTo(1); 14 | assertThat(keyValue.getValue()).isEqualTo("Value"); 15 | } 16 | 17 | @Test(expected = UnsupportedOperationException.class) public void 18 | throw_unsupported_operation_exception_if_mutation_is_attempted() { 19 | KeyValue keyValue = new KeyValue<>(1, "Value"); 20 | 21 | keyValue.setValue(""); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/test/java/com/codurance/lightaccess/mapping/LAResultSetShould.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.mapping; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.MockitoJUnitRunner; 8 | 9 | import java.sql.Date; 10 | import java.sql.ResultSet; 11 | import java.sql.SQLException; 12 | import java.text.SimpleDateFormat; 13 | import java.time.LocalDate; 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | import static java.util.Optional.ofNullable; 18 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 19 | import static org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode; 20 | import static org.apache.commons.lang3.builder.ReflectionToStringBuilder.reflectionToString; 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.mockito.BDDMockito.given; 23 | import static org.mockito.Mockito.verify; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class LAResultSetShould { 27 | 28 | private static final LocalDate TODAY_LOCAL_DATE = LocalDate.of(2017, 07, 29); 29 | private static final java.sql.Date TODAY_SQL_DATE = localDateToSqlDate(TODAY_LOCAL_DATE); 30 | private static final java.util.Date TODAY_UTIL_DATE = sqlDateToUtilDate(TODAY_SQL_DATE); 31 | 32 | @Mock ResultSet resultSet; 33 | 34 | private LAResultSet laResultSet; 35 | 36 | @Before 37 | public void initialise() { 38 | laResultSet = new LAResultSet(resultSet); 39 | } 40 | 41 | @Test public void 42 | return_zero_when_int_field_is_null() throws SQLException { 43 | // JDBC java.sql.ResultSet.getInt(int columnindex) returns 0 when field is null. 44 | given(resultSet.getInt(1)).willReturn(0); 45 | 46 | assertThat(laResultSet.getInt(1)).isEqualTo(0); 47 | } 48 | 49 | @Test public void 50 | return_int_when_int_field_has_value() throws SQLException { 51 | given(resultSet.getInt(1)).willReturn(10); 52 | 53 | assertThat(laResultSet.getInt(1)).isEqualTo(10); 54 | } 55 | 56 | @Test public void 57 | return_empty_string_when_string_field_is_null() throws SQLException { 58 | given(resultSet.getString(1)).willReturn(null); 59 | 60 | assertThat(laResultSet.getString(1)).isEqualTo(""); 61 | } 62 | 63 | @Test public void 64 | return_string_when_string_field_has_value() throws SQLException { 65 | given(resultSet.getString(1)).willReturn("value"); 66 | 67 | assertThat(laResultSet.getString(1)).isEqualTo("value"); 68 | } 69 | 70 | @Test public void 71 | return_optional_empty_string_when_string_field_is_null() throws SQLException { 72 | given(resultSet.getString(1)).willReturn(null); 73 | 74 | assertThat(laResultSet.getOptionalString(1)).isEmpty(); 75 | } 76 | 77 | @Test public void 78 | return_optional_string_when_string_field_has_value() throws SQLException { 79 | given(resultSet.getString(1)).willReturn("value"); 80 | 81 | assertThat(laResultSet.getOptionalString(1)).contains("value"); 82 | } 83 | 84 | @Test public void 85 | return_null_local_date_when_date_field_is_null() throws SQLException { 86 | given(resultSet.getDate(1)).willReturn(null); 87 | 88 | assertThat(laResultSet.getLocalDate(1)).isNull(); 89 | } 90 | 91 | @Test public void 92 | return_local_date_when_date_field_has_value() throws SQLException { 93 | given(resultSet.getDate(1)).willReturn(TODAY_SQL_DATE); 94 | 95 | assertThat(laResultSet.getLocalDate(1)).isEqualTo(TODAY_LOCAL_DATE); 96 | } 97 | 98 | @Test public void 99 | return_null_util_date_when_date_field_is_null() throws SQLException { 100 | given(resultSet.getDate(1)).willReturn(null); 101 | 102 | assertThat(laResultSet.getDate(1)).isNull(); 103 | } 104 | 105 | @Test public void 106 | return_date_when_date_field_has_value() throws SQLException { 107 | given(resultSet.getDate(1)).willReturn(TODAY_SQL_DATE); 108 | 109 | assertThat(laResultSet.getDate(1)).isEqualTo(TODAY_UTIL_DATE); 110 | } 111 | 112 | @Test public void 113 | return_optional_empty_local_date_when_date_field_is_null() throws SQLException { 114 | given(resultSet.getDate(1)).willReturn(null); 115 | 116 | Optional date = laResultSet.getOptionalLocalDate(1); 117 | 118 | assertThat(date.isPresent()).isFalse(); 119 | } 120 | 121 | @Test public void 122 | return_optional_local_date_when_date_field_has_value() throws SQLException { 123 | given(resultSet.getDate(1)).willReturn(TODAY_SQL_DATE); 124 | 125 | Optional date = laResultSet.getOptionalLocalDate(1); 126 | 127 | assertThat(date).contains(TODAY_LOCAL_DATE); 128 | } 129 | 130 | @Test public void 131 | return_optional_empty_when_result_set_has_no_records() throws SQLException { 132 | given(resultSet.next()).willReturn(false); 133 | 134 | Optional person = laResultSet.onlyResult(this::toPerson); 135 | 136 | assertThat(person).isEmpty(); 137 | } 138 | 139 | @Test public void 140 | return_optional_person_when_result_set_one_value() throws SQLException { 141 | Person aPerson = new Person(1, "Person"); 142 | given(resultSet.next()).willReturn(true); 143 | given(resultSet.getInt(1)).willReturn(aPerson.id); 144 | given(resultSet.getString(2)).willReturn(aPerson.name); 145 | 146 | Optional personOptional = laResultSet.onlyResult(this::toPerson); 147 | 148 | assertThat(personOptional).contains(aPerson); 149 | } 150 | 151 | @Test public void 152 | return_empty_list_when_mapping_empty_result_set() throws SQLException { 153 | given(resultSet.next()).willReturn(false); 154 | 155 | List persons = laResultSet.mapResults(this::toPerson); 156 | 157 | assertThat(persons).isEmpty(); 158 | } 159 | 160 | @Test public void 161 | return_list_of_entities_when_mapping_result_set() throws SQLException { 162 | Person person_1 = new Person(1, "Person 1"); 163 | Person person_2 = new Person(2, "Person 2"); 164 | given(resultSet.next()).willReturn(true, true, false); 165 | given(resultSet.getInt(1)).willReturn(person_1.id, person_2.id); 166 | given(resultSet.getString(2)).willReturn(person_1.name, person_2.name); 167 | 168 | List persons = laResultSet.mapResults(this::toPerson); 169 | 170 | assertThat(persons).containsExactlyInAnyOrder(person_1, person_2); 171 | } 172 | 173 | @Test public void 174 | return_a_normalised_one_to_many_structure_after_a_join_statement() throws SQLException { 175 | Person person_1 = new Person(1, "Person 1"); 176 | Person person_2 = new Person(2, "Person 2"); 177 | Person person_3 = new Person(3, "Person 3"); 178 | Product product_1 = new Product(10, "product 10"); 179 | Product product_2 = new Product(20, "product 20"); 180 | 181 | given(resultSet.next()).willReturn(true, true, true, true, false); 182 | given(resultSet.getInt(1)).willReturn(person_1.id, person_1.id, person_2.id, person_3.id); 183 | given(resultSet.getString(2)).willReturn(person_1.name, person_1.name, person_2.name, person_3.name); 184 | given(resultSet.getInt(3)).willReturn(product_1.id, product_2.id, product_1.id, 0); 185 | given(resultSet.getString(4)).willReturn(product_1.description, product_2.description, product_1.description, null); 186 | 187 | OneToMany persons = laResultSet.normaliseOneToMany(this::toPersonWithProducts); 188 | 189 | OneToMany expected = new OneToMany<>(); 190 | expected.put(keyValue(person_1, product_1)); 191 | expected.put(keyValue(person_1, product_2)); 192 | expected.put(keyValue(person_2, product_1)); 193 | expected.put(keyValue(person_3, null)); 194 | 195 | assertThat(persons).isEqualTo(expected); 196 | } 197 | 198 | @Test public void 199 | move_to_next_record() throws SQLException { 200 | laResultSet.nextRecord(); 201 | 202 | verify(resultSet).next(); 203 | } 204 | 205 | private KeyValue> toPersonWithProducts(LAResultSet laResultSet) { 206 | int personId = laResultSet.getInt(1); 207 | String personName = laResultSet.getString(2); 208 | Person person = new Person(personId, personName); 209 | 210 | int productId = laResultSet.getInt(3); 211 | String productDescription = laResultSet.getString(4); 212 | Optional product = ofNullable((productId > 0) ? new Product(productId, productDescription) : null); 213 | 214 | return new KeyValue<>(person, product); 215 | } 216 | 217 | private KeyValue> keyValue(Person person, Product product) { 218 | return new KeyValue<>(person, Optional.ofNullable(product)); 219 | } 220 | 221 | private Person toPerson(LAResultSet laResultSet) { 222 | return new Person(laResultSet.getInt(1), laResultSet.getString(2)); 223 | } 224 | 225 | private static java.util.Date sqlDateToUtilDate(java.sql.Date sqlDate) { 226 | return new java.util.Date(sqlDate.getTime()); 227 | } 228 | 229 | private static java.sql.Date localDateToSqlDate(LocalDate localDate) { 230 | return Date.valueOf(localDate); 231 | } 232 | 233 | class Person { 234 | private final Integer id; 235 | private final String name; 236 | 237 | Person(Integer id, String name) { 238 | this.id = id; 239 | this.name = name; 240 | } 241 | 242 | @Override 243 | public boolean equals(Object other) { 244 | return reflectionEquals(this, other); 245 | } 246 | 247 | @Override 248 | public int hashCode() { 249 | return reflectionHashCode(this); 250 | } 251 | 252 | @Override 253 | public String toString() { 254 | return reflectionToString(this); 255 | } 256 | } 257 | 258 | class Product { 259 | private final Integer id; 260 | private final String description; 261 | 262 | Product(Integer id, String description) { 263 | this.id = id; 264 | this.description = description; 265 | } 266 | 267 | @Override 268 | public boolean equals(Object other) { 269 | return reflectionEquals(this, other); 270 | } 271 | 272 | @Override 273 | public int hashCode() { 274 | return reflectionHashCode(this); 275 | } 276 | 277 | @Override 278 | public String toString() { 279 | return "Product{" + 280 | "id=" + id + 281 | ", description='" + description + '\'' + 282 | '}'; 283 | } 284 | } 285 | 286 | } -------------------------------------------------------------------------------- /src/test/java/com/codurance/lightaccess/mapping/OneToManyShould.java: -------------------------------------------------------------------------------- 1 | package com.codurance.lightaccess.mapping; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Optional; 8 | 9 | import static java.util.Arrays.asList; 10 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 11 | import static org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode; 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | public class OneToManyShould { 15 | 16 | private static final Company COMPANY_1 = new Company(1, "Company 1"); 17 | private static final Company COMPANY_2 = new Company(2, "Company 2"); 18 | private static final Company COMPANY_3 = new Company(2, "Company 3"); 19 | 20 | private static final Contact JOHN = new Contact(10, "John"); 21 | private static final Contact BRIAN = new Contact(10, "Brian"); 22 | private static final Contact SALLY = new Contact(10, "Sally"); 23 | 24 | 25 | @Test public void 26 | collect_data_into_a_list_of_objects_containing_key_and_values() { 27 | OneToMany oneToMany = new OneToMany<>(); 28 | oneToMany.put(COMPANY_1, Optional.of(JOHN)); 29 | oneToMany.put(COMPANY_1, Optional.of(BRIAN)); 30 | oneToMany.put(COMPANY_2, Optional.of(SALLY)); 31 | oneToMany.put(COMPANY_3, Optional.empty()); 32 | 33 | List companyWithContacts = oneToMany.collect(CompanyWithContacts::new); 34 | 35 | assertThat(companyWithContacts).containsExactlyInAnyOrder( 36 | new CompanyWithContacts(COMPANY_1, JOHN, BRIAN), 37 | new CompanyWithContacts(COMPANY_2, SALLY), 38 | new CompanyWithContacts(COMPANY_3)); 39 | } 40 | 41 | static class Company { 42 | private final Integer id; 43 | private final String name; 44 | 45 | Company(Integer id, String name) { 46 | this.id = id; 47 | this.name = name; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object other) { 52 | return reflectionEquals(this, other); 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return reflectionHashCode(this); 58 | } 59 | } 60 | 61 | static class Contact { 62 | private final Integer id; 63 | private final String name; 64 | 65 | Contact(Integer id, String name) { 66 | this.id = id; 67 | this.name = name; 68 | } 69 | 70 | @Override 71 | public boolean equals(Object other) { 72 | return reflectionEquals(this, other); 73 | } 74 | 75 | @Override 76 | public int hashCode() { 77 | return reflectionHashCode(this); 78 | } 79 | } 80 | 81 | static class CompanyWithContacts { 82 | private Company company; 83 | private List contacts = new ArrayList<>(); 84 | 85 | CompanyWithContacts(Company company, Contact... contacts) { 86 | this(company, asList(contacts)); 87 | } 88 | 89 | CompanyWithContacts(Company company, List contacts) { 90 | this.company = company; 91 | this.contacts = contacts; 92 | } 93 | 94 | @Override 95 | public boolean equals(Object other) { 96 | return reflectionEquals(this, other); 97 | } 98 | 99 | @Override 100 | public int hashCode() { 101 | return reflectionHashCode(this); 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/integration/JoinsIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package integration; 2 | 3 | import com.codurance.lightaccess.LightAccess; 4 | import com.codurance.lightaccess.executables.DDLCommand; 5 | import com.codurance.lightaccess.mapping.KeyValue; 6 | import com.codurance.lightaccess.mapping.LAResultSet; 7 | import com.codurance.lightaccess.mapping.OneToMany; 8 | import integration.dtos.*; 9 | import org.h2.jdbcx.JdbcConnectionPool; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.BeforeClass; 13 | import org.junit.Test; 14 | 15 | import java.sql.SQLException; 16 | import java.time.LocalDate; 17 | import java.util.List; 18 | import java.util.Optional; 19 | 20 | import static java.util.Arrays.asList; 21 | import static java.util.Collections.emptyList; 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | public class JoinsIntegrationTest { 25 | 26 | private static final String CREATE_USERS_TABLE = "CREATE TABLE users (userId integer PRIMARY KEY, name VARCHAR(255))"; 27 | private static final String CREATE_WISHLISTS_TABLE = "CREATE TABLE wishlists (wishListId integer PRIMARY KEY, userId integer, name VARCHAR(255), creationDate TIMESTAMP)"; 28 | private static final String CREATE_PRODUCTS_TABLE = "CREATE TABLE products (productId integer PRIMARY KEY, name VARCHAR(255), date TIMESTAMP)"; 29 | private static final String CREATE_WISHLIST_PRODUCT_TABLE = "CREATE TABLE wishlist_product (id integer PRIMARY KEY, wishListId integer, productId integer)"; 30 | 31 | private static final String ID_SEQUENCE = "id_sequence"; 32 | private static final String CREATE_SEQUENCE_DDL = "CREATE SEQUENCE " + ID_SEQUENCE + " START WITH 1"; 33 | private static final String DROP_ALL_OBJECTS = "DROP ALL OBJECTS"; 34 | 35 | private static final String INSERT_USER_SQL = "insert into users (userId, name) values (?, ?)"; 36 | private static final String INSERT_PRODUCT_SQL = "insert into products (productId, name, date) values (?, ?, ?)"; 37 | private static final String INSERT_WISHLIST_SQL = "insert into wishlists (wishListId, userId, name, creationDate) values (?, ?, ?, ?)"; 38 | private static final String INSERT_WISHLIST_PRODUCT_SQL = "insert into wishlist_product (id, wishListId, productId) values (?, ?, ?)"; 39 | 40 | private static final String SELECT_WISHLISTS_PER_USER_SQL = 41 | "select u.userId, u.name, w.wishListId, w.userId, w.name, w.creationDate " + 42 | "from users u " + 43 | "left join wishlists w on u.userId = w.userId"; 44 | private static final String SELECT_WISHLISTS_WITH_PRODUCTS_PER_USER_SQL = 45 | "select w.wishListId, w.userId, w.name, w.creationDate, p.productId, p.name, p.date " + 46 | "from wishlists w " + 47 | "left join wishlist_product wp on w.wishListId = wp.wishListId " + 48 | "left join products p on wp.productId = p.productId " + 49 | "where w.userId = ?"; 50 | 51 | private static final User JOHN = new User(1, "John"); 52 | private static final User SALLY = new User(2, "Sally"); 53 | 54 | private static final Product MACBOOK_PRO = new Product(10, "MacBook Pro"); 55 | private static final Product IPHONE = new Product(20, "iPhone"); 56 | private static final Product IPAD = new Product(30, "iPad"); 57 | 58 | private static final LocalDate TODAY = LocalDate.of(2017, 07, 27); 59 | 60 | private static final WishList XMAS_WISHLIST = new WishList(1, JOHN.id(), "Xmas", TODAY); 61 | private static final WishList BIRTHDAY_WISHLIST = new WishList(2, JOHN.id(), "Birthday", TODAY); 62 | private static final WishList FATHERS_DAY_WISHLIST = new WishList(3, JOHN.id(), "Father's day", TODAY); 63 | 64 | private static LightAccess lightAccess; 65 | private static JdbcConnectionPool jdbcConnectionPool; 66 | 67 | @BeforeClass 68 | public static void before_all_tests() throws SQLException { 69 | jdbcConnectionPool = JdbcConnectionPool.create("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "user", "password"); 70 | lightAccess = new LightAccess(jdbcConnectionPool); 71 | } 72 | 73 | @Before 74 | public void before_each_test() throws Exception { 75 | lightAccess.executeDDLCommand(createTables()); 76 | } 77 | 78 | @After 79 | public void after_each_test() throws Exception { 80 | lightAccess.executeDDLCommand(dropAllObjects()); 81 | } 82 | 83 | @Test public void 84 | one_to_many_mapping_with_left_join_collected_to_a_list_of_dto() { 85 | givenWeHaveUsers(JOHN, SALLY); 86 | givenWeHaveProducts(MACBOOK_PRO, IPHONE, IPAD); 87 | givenWeHaveAWishListFor(JOHN, XMAS_WISHLIST, MACBOOK_PRO, IPHONE); 88 | givenWeHaveAWishListFor(JOHN, BIRTHDAY_WISHLIST, IPAD); 89 | 90 | OneToMany wishListsPerUser = whenWeReturnAllWishListsPerUser(); 91 | 92 | assertThat(wishListsPerUser.collect(this::toUserWithWishLists)) 93 | .containsExactlyInAnyOrder( 94 | new UserWithWishList(JOHN, asList(wishList(1, JOHN, "Xmas"), 95 | wishList(2, JOHN, "Birthday"))), 96 | new UserWithWishList(SALLY, emptyList())); 97 | } 98 | 99 | @Test public void 100 | collect_data_from_many_to_many_into_a_list_of_dto() { 101 | givenWeHaveUsers(JOHN, SALLY); 102 | givenWeHaveProducts(MACBOOK_PRO, IPHONE, IPAD); 103 | givenWeHaveAWishListFor(JOHN, XMAS_WISHLIST, MACBOOK_PRO, IPHONE); 104 | givenWeHaveAWishListFor(JOHN, BIRTHDAY_WISHLIST, IPAD); 105 | givenWeHaveAWishListFor(JOHN, FATHERS_DAY_WISHLIST); 106 | 107 | OneToMany userWishListWithProducts = whenWeReturnAllWishListsWithProductsBelongingTo(JOHN); 108 | 109 | assertThat(userWishListWithProducts.collect(this::toWishListProducts)) 110 | .containsExactlyInAnyOrder( 111 | wishListWithProducts(XMAS_WISHLIST, MACBOOK_PRO, IPHONE), 112 | wishListWithProducts(BIRTHDAY_WISHLIST, IPAD), 113 | wishListWithProducts(FATHERS_DAY_WISHLIST)); 114 | } 115 | 116 | private WishListProduct wishListWithProducts(WishList wishList, Product... products) { 117 | return new WishListProduct(wishList, asList(products)); 118 | } 119 | 120 | private WishListProduct toWishListProducts(WishList wishList, List products) { 121 | return new WishListProduct(wishList, products); 122 | } 123 | 124 | private OneToMany whenWeReturnAllWishListsWithProductsBelongingTo(User user) { 125 | return lightAccess.executeQuery((conn -> conn.prepareStatement(SELECT_WISHLISTS_WITH_PRODUCTS_PER_USER_SQL) 126 | .withParam(user.id()) 127 | .executeQuery() 128 | .normaliseOneToMany(this::toWithListWithProduct))); 129 | } 130 | 131 | private KeyValue> toWithListWithProduct(LAResultSet rs) { 132 | WishList wishList = new WishList(rs.getInt(1), 133 | rs.getInt(2), 134 | rs.getString(3), 135 | rs.getLocalDate(4)); 136 | Optional product = Optional.ofNullable((rs.getInt(5) > 0) 137 | ? new Product(rs.getInt(5), rs.getString(6), rs.getLocalDate(7)) 138 | : null); 139 | return new KeyValue<>(wishList, product); 140 | } 141 | 142 | private OneToMany whenWeReturnAllWishListsWithProductsPerUser() { 143 | throw new UnsupportedOperationException(); 144 | } 145 | 146 | private WishList wishList(int id, User user, String name, Product... products) { 147 | return new WishList(id, user.id(), name, TODAY); 148 | } 149 | 150 | private List allWishLists() { 151 | return lightAccess.executeQuery(c -> c.prepareStatement("select * from wishlists") 152 | .executeQuery() 153 | .mapResults(rs -> new WishList(rs.getInt(1), 154 | rs.getInt(2), 155 | rs.getString(3), 156 | rs.getLocalDate(4)))); 157 | } 158 | 159 | private List allProducts() { 160 | return lightAccess.executeQuery(c -> c.prepareStatement("select * from products") 161 | .executeQuery() 162 | .mapResults(rs -> new Product(rs.getInt(1), rs.getString(2)))); 163 | } 164 | 165 | private List allUsers() { 166 | return lightAccess.executeQuery(c -> c.prepareStatement("select * from users") 167 | .executeQuery() 168 | .mapResults(result -> new User(result.getInt(1), result.getString(2)))); 169 | } 170 | 171 | private UserWithWishList toUserWithWishLists(User user, List wishLists) { 172 | return new UserWithWishList(user, wishLists); 173 | } 174 | 175 | private OneToMany whenWeReturnAllWishListsPerUser() { 176 | return lightAccess.executeQuery((conn -> conn.prepareStatement(SELECT_WISHLISTS_PER_USER_SQL) 177 | .executeQuery() 178 | .normaliseOneToMany(this::toUserWishList))); 179 | } 180 | 181 | private KeyValue> toUserWishList(LAResultSet laResultSet) { 182 | User user = new User(laResultSet.getInt(1), laResultSet.getString(2)); 183 | 184 | Optional wishList = Optional.ofNullable((laResultSet.getInt(3) > 0) 185 | ? new WishList(laResultSet.getInt(3), 186 | laResultSet.getInt(4), 187 | laResultSet.getString(5), 188 | laResultSet.getLocalDate(6)) 189 | : null); 190 | return new KeyValue<>(user, wishList); 191 | } 192 | 193 | private void givenWeHaveAWishListFor(User user, WishList wishList, Product... products) { 194 | insertWithList(user, wishList); 195 | addProductsToWishList(wishList, products); 196 | } 197 | 198 | private void insertWithList(User user, WishList wishList) { 199 | lightAccess.executeCommand(conn -> conn.prepareStatement(INSERT_WISHLIST_SQL) 200 | .withParam(wishList.id()) 201 | .withParam(user.id()) 202 | .withParam(wishList.name()) 203 | .withParam(TODAY) 204 | .executeUpdate()); 205 | } 206 | 207 | private void addProductsToWishList(WishList wishList, Product[] products) { 208 | lightAccess.executeCommand((conn -> asList(products).forEach(product -> { 209 | int id = lightAccess.nextId(ID_SEQUENCE); 210 | conn.prepareStatement(INSERT_WISHLIST_PRODUCT_SQL) 211 | .withParam(id) 212 | .withParam(wishList.id()) 213 | .withParam(product.id()) 214 | .executeUpdate(); 215 | }))); 216 | } 217 | 218 | private void givenWeHaveUsers(User... users) { 219 | lightAccess.executeCommand((conn -> asList(users).forEach( 220 | user -> conn.prepareStatement(INSERT_USER_SQL) 221 | .withParam(user.id()) 222 | .withParam(user.name()) 223 | .executeUpdate()))); 224 | } 225 | 226 | private void givenWeHaveProducts(Product... products) { 227 | lightAccess.executeCommand((conn -> asList(products).forEach( 228 | product -> conn.prepareStatement(INSERT_PRODUCT_SQL) 229 | .withParam(product.id()) 230 | .withParam(product.name()) 231 | .withParam(product.date()) 232 | .executeUpdate()))); 233 | } 234 | 235 | private DDLCommand createTables() { 236 | return (conn) -> { 237 | conn.statement(CREATE_SEQUENCE_DDL).execute(); 238 | conn.statement(CREATE_USERS_TABLE).execute(); 239 | conn.statement(CREATE_WISHLISTS_TABLE).execute(); 240 | conn.statement(CREATE_PRODUCTS_TABLE).execute(); 241 | conn.statement(CREATE_WISHLIST_PRODUCT_TABLE).execute(); 242 | }; 243 | } 244 | 245 | private DDLCommand dropAllObjects() { 246 | return (conn) -> conn.statement(DROP_ALL_OBJECTS).execute(); 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /src/test/java/integration/LightAccessIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package integration; 2 | 3 | import com.codurance.lightaccess.LightAccess; 4 | import com.codurance.lightaccess.executables.DDLCommand; 5 | import com.codurance.lightaccess.executables.SQLCommand; 6 | import com.codurance.lightaccess.executables.SQLQuery; 7 | import com.codurance.lightaccess.mapping.LAResultSet; 8 | import integration.dtos.Product; 9 | import integration.dtos.ProductID; 10 | import org.h2.jdbcx.JdbcConnectionPool; 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.BeforeClass; 14 | import org.junit.Test; 15 | 16 | import java.sql.SQLException; 17 | import java.time.LocalDate; 18 | import java.util.List; 19 | import java.util.Optional; 20 | 21 | import static java.lang.String.format; 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | public class LightAccessIntegrationTest { 25 | 26 | private static final String CREATE_PRODUCTS_TABLE = "CREATE TABLE products (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), date TIMESTAMP)"; 27 | private static final String CREATE_SEQUENCE_DDL = "CREATE SEQUENCE %s START WITH %s"; 28 | private static final String DROP_ALL_OBJECTS = "DROP ALL OBJECTS"; 29 | 30 | private static final String INSERT_PRODUCT_SQL = "insert into products (id, name, date) values (?, ?, ?)"; 31 | private static final String DELETE_PRODUCTS_SQL = "delete from products"; 32 | private static final String DELETE_PRODUCT_SQL = "delete from products where id = ?"; 33 | private static final String UPDATE_PRODUCT_NAME_SQL = "update products set name = ? where id = ?"; 34 | private static final String SELECT_ALL_PRODUCTS_SQL = "select * from products"; 35 | private static final String SELECT_PRODUCT_BY_ID_SQL = "select * from products where id = ?"; 36 | 37 | private static final LocalDate TODAY = LocalDate.of(2017, 07, 27); 38 | private static final LocalDate YESTERDAY = LocalDate.of(2017, 07, 26); 39 | 40 | private static Product PRODUCT_ONE = new Product(1, "Product 1", YESTERDAY); 41 | private static Product PRODUCT_TWO = new Product(2, "Product 2", TODAY); 42 | 43 | private static LightAccess lightAccess; 44 | private static JdbcConnectionPool jdbcConnectionPool; 45 | 46 | @BeforeClass 47 | public static void before_all_tests() throws SQLException { 48 | jdbcConnectionPool = JdbcConnectionPool.create("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "user", "password"); 49 | lightAccess = new LightAccess(jdbcConnectionPool); 50 | } 51 | 52 | @Before 53 | public void before_each_test() throws Exception { 54 | lightAccess.executeDDLCommand(createProductsTable()); 55 | } 56 | 57 | @After 58 | public void after_each_test() throws Exception { 59 | lightAccess.executeDDLCommand(dropAllObjects()); 60 | } 61 | 62 | @Test public void 63 | close_connection_after_executing_a_query() { 64 | lightAccess.executeQuery((conn) -> SELECT_ALL_PRODUCTS_SQL); 65 | 66 | assertThat(jdbcConnectionPool.getActiveConnections()).isEqualTo(0); 67 | } 68 | 69 | @Test public void 70 | close_connection_after_executing_a_command() { 71 | lightAccess.executeCommand(deleteProducts()); 72 | 73 | assertThat(jdbcConnectionPool.getActiveConnections()).isEqualTo(0); 74 | } 75 | 76 | @Test public void 77 | close_connection_after_executing_a_DDL_command() { 78 | lightAccess.executeDDLCommand(dropAllObjects()); 79 | 80 | assertThat(jdbcConnectionPool.getActiveConnections()).isEqualTo(0); 81 | } 82 | 83 | @Test public void 84 | insert_records() { 85 | lightAccess.executeCommand(insert(PRODUCT_ONE)); 86 | lightAccess.executeCommand(insert(PRODUCT_TWO)); 87 | 88 | List products = lightAccess.executeQuery(retrieveAllProducts()); 89 | 90 | assertThat(products).containsExactlyInAnyOrder(PRODUCT_ONE, PRODUCT_TWO); 91 | } 92 | 93 | @Test public void 94 | retrieve_a_single_record_and_map_it_to_an_object() { 95 | lightAccess.executeCommand(insert(PRODUCT_ONE)); 96 | lightAccess.executeCommand(insert(PRODUCT_TWO)); 97 | 98 | Optional product = lightAccess.executeQuery(retrieveProductWithId(PRODUCT_TWO.id())); 99 | 100 | assertThat(product.get()).isEqualTo(PRODUCT_TWO); 101 | } 102 | 103 | @Test public void 104 | retrieve_an_empty_optional_when_not_record_is_found() { 105 | lightAccess.executeCommand(insert(PRODUCT_ONE)); 106 | 107 | Optional product = lightAccess.executeQuery(retrieveProductWithId(PRODUCT_TWO.id())); 108 | 109 | assertThat(product.isPresent()).isEqualTo(false); 110 | } 111 | 112 | @Test public void 113 | delete_a_record() { 114 | lightAccess.executeCommand(insert(PRODUCT_ONE)); 115 | lightAccess.executeCommand(insert(PRODUCT_TWO)); 116 | 117 | lightAccess.executeCommand(delete(PRODUCT_ONE)); 118 | 119 | List products = lightAccess.executeQuery(retrieveAllProducts()); 120 | assertThat(products).containsExactlyInAnyOrder(PRODUCT_TWO); 121 | } 122 | 123 | @Test public void 124 | update_a_record() { 125 | lightAccess.executeCommand(insert(PRODUCT_ONE)); 126 | lightAccess.executeCommand(updateProductName(1, "Another name")); 127 | 128 | Optional product = lightAccess.executeQuery(retrieveProductWithId(PRODUCT_ONE.id())); 129 | 130 | assertThat(product.get()).isEqualTo(new Product(PRODUCT_ONE.id(), "Another name", PRODUCT_ONE.date())); 131 | } 132 | 133 | @Test public void 134 | return_next_integer_id_using_sequence() throws Exception { 135 | lightAccess.executeDDLCommand(createSequence("id_sequence", "10")); 136 | 137 | int firstId = lightAccess.nextId("id_sequence"); 138 | int secondId = lightAccess.nextId("id_sequence"); 139 | 140 | assertThat(firstId).isEqualTo(10); 141 | assertThat(secondId).isEqualTo(11); 142 | } 143 | 144 | @Test public void 145 | return_next_id_converted_to_a_different_type() throws Exception { 146 | lightAccess.executeDDLCommand(createSequence("id_sequence", "10")); 147 | 148 | String firstId = lightAccess.nextId("id_sequence", Object::toString); 149 | ProductID secondId = lightAccess.nextId("id_sequence", ProductID::new); 150 | 151 | assertThat(firstId).isEqualTo("10"); 152 | assertThat(secondId).isEqualTo(new ProductID(11)); 153 | } 154 | 155 | private SQLCommand updateProductName(int id, String name) { 156 | return conn -> conn.prepareStatement(UPDATE_PRODUCT_NAME_SQL) 157 | .withParam(name) 158 | .withParam(id) 159 | .executeUpdate(); 160 | } 161 | 162 | private SQLCommand delete(Product product) { 163 | return conn -> conn.prepareStatement(DELETE_PRODUCT_SQL) 164 | .withParam(product.id()) 165 | .executeUpdate(); 166 | } 167 | 168 | private SQLCommand insert(Product product) { 169 | return conn -> conn.prepareStatement(INSERT_PRODUCT_SQL) 170 | .withParam(product.id()) 171 | .withParam(product.name()) 172 | .withParam(product.date()) 173 | .executeUpdate(); 174 | } 175 | 176 | private SQLQuery> retrieveProductWithId(int id) { 177 | return conn -> conn.prepareStatement(SELECT_PRODUCT_BY_ID_SQL) 178 | .withParam(id) 179 | .executeQuery() 180 | .onlyResult(this::toProduct); 181 | } 182 | 183 | private SQLQuery> retrieveAllProducts() { 184 | return conn -> conn.prepareStatement(SELECT_ALL_PRODUCTS_SQL) 185 | .executeQuery() 186 | .mapResults(this::toProduct); 187 | } 188 | 189 | private SQLCommand deleteProducts() { 190 | return conn -> conn.prepareStatement(DELETE_PRODUCTS_SQL).executeUpdate(); 191 | } 192 | 193 | private Product toProduct(LAResultSet laResultSet) { 194 | return new Product(laResultSet.getInt(1), 195 | laResultSet.getString(2), 196 | laResultSet.getLocalDate(3)); 197 | } 198 | 199 | private DDLCommand createSequence(String sequenceName, String initialValue) { 200 | String id_sequence = format(CREATE_SEQUENCE_DDL, sequenceName, initialValue); 201 | return (conn) -> conn.statement(id_sequence).execute(); 202 | } 203 | 204 | private DDLCommand createProductsTable() { 205 | return (conn) -> conn.statement(CREATE_PRODUCTS_TABLE).execute(); 206 | } 207 | 208 | private DDLCommand dropAllObjects() { 209 | return (conn) -> conn.statement(DROP_ALL_OBJECTS).execute(); 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/test/java/integration/dtos/Product.java: -------------------------------------------------------------------------------- 1 | package integration.dtos; 2 | 3 | import java.time.LocalDate; 4 | 5 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 6 | import static org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode; 7 | 8 | public class Product { 9 | private int id; 10 | private String name; 11 | private LocalDate date; 12 | 13 | public Product(int id, String name, LocalDate date) { 14 | this.id = id; 15 | this.name = name; 16 | this.date = date; 17 | } 18 | 19 | public Product(int id, String name) { 20 | this(id, name, LocalDate.of(2017, 07, 29)); 21 | } 22 | 23 | public int id() { 24 | return id; 25 | } 26 | 27 | public String name() { 28 | return name; 29 | } 30 | 31 | public LocalDate date() { 32 | return date; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object o) { 37 | return reflectionEquals(this, o); 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return reflectionHashCode(this); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "Product{" + 48 | "id=" + id + 49 | ", name='" + name + '\'' + 50 | ", date=" + date + 51 | '}'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/integration/dtos/ProductID.java: -------------------------------------------------------------------------------- 1 | package integration.dtos; 2 | 3 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 4 | import static org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode; 5 | 6 | public class ProductID { 7 | private int id; 8 | 9 | public ProductID(int id) { 10 | this.id = id; 11 | } 12 | 13 | @Override 14 | public boolean equals(Object other) { 15 | return reflectionEquals(this, other); 16 | } 17 | 18 | @Override 19 | public int hashCode() { 20 | return reflectionHashCode(this); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/integration/dtos/User.java: -------------------------------------------------------------------------------- 1 | package integration.dtos; 2 | 3 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 4 | import static org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode; 5 | 6 | public class User { 7 | 8 | private final Integer id; 9 | private final String name; 10 | 11 | public User(Integer id, String name) { 12 | this.id = id; 13 | this.name = name; 14 | } 15 | 16 | public Integer id() { 17 | return id; 18 | } 19 | 20 | public String name() { 21 | return name; 22 | } 23 | 24 | @Override 25 | public boolean equals(Object other) { 26 | return reflectionEquals(this, other); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return reflectionHashCode(this); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "User{" + 37 | "id=" + id + 38 | ", name='" + name + '\'' + 39 | '}'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/integration/dtos/UserWithWishList.java: -------------------------------------------------------------------------------- 1 | package integration.dtos; 2 | 3 | import java.util.List; 4 | 5 | import static java.util.Collections.unmodifiableList; 6 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 7 | import static org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode; 8 | 9 | public class UserWithWishList { 10 | 11 | private final User user; 12 | private final List wishLists; 13 | 14 | public UserWithWishList(User user, List wishLists) { 15 | this.user = user; 16 | this.wishLists = unmodifiableList(wishLists); 17 | } 18 | 19 | public User user() { 20 | return user; 21 | } 22 | 23 | public List wishLists() { 24 | return unmodifiableList(wishLists); 25 | } 26 | 27 | @Override 28 | public boolean equals(Object other) { 29 | return reflectionEquals(this, other); 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return reflectionHashCode(this); 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return "UserWithWishList{" + 40 | "user=" + user + 41 | ", wishLists=" + wishLists + 42 | '}'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/integration/dtos/WishList.java: -------------------------------------------------------------------------------- 1 | package integration.dtos; 2 | 3 | import java.time.LocalDate; 4 | 5 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 6 | import static org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode; 7 | 8 | public class WishList { 9 | 10 | private final Integer id; 11 | private final Integer userId; 12 | private final String name; 13 | private final LocalDate creationDate; 14 | 15 | public WishList(Integer id, Integer userId, String name, LocalDate creationDate) { 16 | this.id = id; 17 | this.userId = userId; 18 | this.name = name; 19 | this.creationDate = creationDate; 20 | } 21 | 22 | public Integer id() { 23 | return id; 24 | } 25 | 26 | public Integer userId() { 27 | return userId; 28 | } 29 | 30 | public String name() { 31 | return name; 32 | } 33 | 34 | public LocalDate creationDate() { 35 | return creationDate; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object other) { 40 | return reflectionEquals(this, other); 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | return reflectionHashCode(this); 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "WishList{" + 51 | "id=" + id + 52 | ", userId=" + userId + 53 | ", name='" + name + '\'' + 54 | ", creationDate=" + creationDate + 55 | '}'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/integration/dtos/WishListProduct.java: -------------------------------------------------------------------------------- 1 | package integration.dtos; 2 | 3 | import java.util.List; 4 | 5 | import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; 6 | import static org.apache.commons.lang3.builder.HashCodeBuilder.reflectionHashCode; 7 | 8 | public class WishListProduct { 9 | 10 | private final WishList wishList; 11 | private final List products; 12 | 13 | public WishListProduct(WishList wishList, List products) { 14 | this.wishList = wishList; 15 | this.products = products; 16 | } 17 | 18 | @Override 19 | public boolean equals(Object other) { 20 | return reflectionEquals(this, other); 21 | } 22 | 23 | @Override 24 | public int hashCode() { 25 | return reflectionHashCode(this); 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return "WithListProduct{" + 31 | "wishList=" + wishList + 32 | ", products=" + products + 33 | '}'; 34 | } 35 | } 36 | --------------------------------------------------------------------------------