├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ ├── module-info.java │ └── org │ └── yorm │ ├── IdentifiableFunction.java │ ├── MapBuilder.java │ ├── Yorm.java │ ├── YormTable.java │ ├── YormTuple.java │ ├── db │ ├── FilteringFieldValue.java │ ├── QueryBuilder.java │ └── operations │ │ ├── FilterPredicates.java │ │ ├── QueryDelete.java │ │ ├── QueryFind.java │ │ ├── QuerySave.java │ │ ├── operators │ │ ├── ComparisonOperator.java │ │ └── WhereOperator.java │ │ └── select │ │ └── Select.java │ ├── exception │ └── YormException.java │ └── util │ ├── DbType.java │ ├── Levenshtein.java │ └── RowRecordConverter.java └── test ├── java └── org │ └── yorm │ ├── PerformanceTest.java │ ├── YormMySqlTest.java │ ├── YormPostgreSqlTest.java │ ├── records │ ├── Company.java │ ├── CompanyType.java │ ├── HistoryAnnotation.java │ ├── Invoice.java │ ├── Person.java │ └── PersonCompany.java │ └── utils │ ├── TestConnectionFactory.java │ └── TestDbHelper.java └── resources ├── init_sample_db_my_sql.sql ├── init_sample_db_postgre_sql.sql └── log4j2-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | target/ 26 | bower_components/ 27 | node_modules/ 28 | dist/ 29 | .idea/ 30 | .tmp/ 31 | .settings 32 | .metadata/ 33 | *.iml 34 | *.log 35 | *.tmp 36 | *.bak 37 | dependency-reduced-pom.xml 38 | logs/ 39 | .attach_pid* 40 | 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | 4 | ## 2024-09-08 version 0.8.1 5 | 6 | - Added fix for type Char 7 | 8 | ## 2024-06-23 version 0.8.0 9 | 10 | - Fix on records with several boolean fields 11 | - Added support for Enums, thanks to [thewildllama] 12 | 13 | ## 2023-04-17 version 0.7.0 14 | 15 | - Small fix when using ids with integer 16 | 17 | ## 2023-04-14 version 0.6.0 18 | 19 | - Added support for records with several constructors 20 | 21 | ## 2023-01-11 version 0.5.0 22 | 23 | - Added support for TEXT types 24 | - Fixed bug when saving a record without id 25 | 26 | ## 2022-05-31 version 0.4.0 27 | 28 | - Added support for views 29 | - Allow different order in object fields and table fields 30 | 31 | ## 2022-05-22 version 0.3.0 32 | 33 | - Checked support for Snowflake, it works as long as the table has no primary keys 34 | - Fixed bug on Time type 35 | - Fixed bug on tables with slightly different names than classes 36 | 37 | ## 2022-04-10 version 0.2.0 38 | 39 | - Added initial support for Postgresql, thanks to [PabloGrisafi] 40 | - Deletion now returns true or false 41 | - Added fluent API, based on [Benjiql] idea 42 | 43 | ## 2022-03-19 version 0.1.0 44 | 45 | - Yorm is born, just working with Mysql 46 | 47 | 48 | [PabloGrisafi]: 49 | [Benjiql]: 50 | [thewildllama]: 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yorm 2 | ## _Yet another ORM_ 3 | 4 | ![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master) 5 | [![Maven metadata URL](https://img.shields.io/maven-metadata/v?color=blue&logo=apache-maven&metadataUrl=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Forg%2Fyorm%2Fyorm%2Fmaven-metadata.xml)](https://mvnrepository.com/artifact/org.yorm/yorm) 6 | ![Supported JVM Versions](https://img.shields.io/badge/JVM-17-success.svg?style=flat&logo=Java) 7 | [![License](https://img.shields.io/github/license/naynecoder/yorm?style=flat&logo=apache&color=success)](https://www.apache.org/licenses/LICENSE-2.0) 8 | 9 | Yorm is a basic ORM-alike framework designed to work with Java Records, without class generation, neither annotations. 10 | In the world of microservices, there is a tendency to have very contained logic within every service, 11 | and hence reduced databases, that in many cases are simply no more than several tables with not that many fields. 12 | Java Records usually are a perfect fit for basic CRUD operations, and here is where Yorm shines. 13 | 14 | **Yorm** needs at least Java 17, although it works of newer versions as well. 15 | 16 | **Yorm** might be for you in case: 17 | 18 | - You are working with microservices with Java and like Java Records 19 | - Your relational databases are pretty simple and basic 20 | - Your tables contain auto increment ids 21 | - You don't need complex INNER JOIN queries, just basic CRUD 22 | 23 | Although the Java industry offers very well maintained ORM solutions like [Hibernate] or [Jooq], they tend not to work that well with Java Records. **Yorm** on the other side is specifically designed to leverage this Java capability. 24 | 25 | Due to the immutable nature of Java Records, **Yorm** cannot be understood as a persistent ORM, not even as an ORM, as there aren't really any relationships. 26 | ## Features 27 | 28 | - No need to generate classes 29 | - No need to add annotations 30 | - No need to write SQL for basic operations 31 | - Seamless flow with API REST and CRUD operations 32 | 33 | **Yorm** doesn't need to generate classes or to annotate them, but it works on conventions. It will assume that your 34 | table has a Primary Key called *id*, probably with an autoincrement. Also it will assume that foreign keys will follow the naming patter of *table_id*. The convention will assume as well that fields in the table and fields in the record will have the same name, or a very similar one. 35 | 36 | When a Java Record is operated with **Yorm**, a reflection inspection will came in, and all the methods of the Record will be matched with their counterparts from the database. This matching will be kept in memory as a map, to avoid using reflection again. However, if there is a change in the database structure, the microservice will probably need to be restarted to refresh this mapping. 37 | 38 | ## Dependencies 39 | 40 | *Yorm* has been designed to need very few dependencies: 41 | 42 | - [HikariCP] - Hikari, to deal with the database 43 | - [MySql] - Database supported and tested 44 | - [PostgreSql] - Database supported and tested, courtesy of [PabloGrisafi] 45 | - [Junit 5] - For the unit tests 46 | - [TestContainers] - Also for the unit tests 47 | - [Slf4j] - Logging is usually useful 48 | 49 | And that's it, the **Yorm** lies heavily on Java Records and Reflections. 50 | 51 | **Yorm** has been tested with Snowflake, and it works as long as the table 52 | has no primary keys, and the Jvm is run with the option *--add-opens java.base/java.nio=ALL-UNNAMED* 53 | 54 | ## How to use it with examples 55 | 56 | Imagine you have a database with a table called Person, defined like: 57 | 58 | ```sql 59 | CREATE TABLE person 60 | ( 61 | id INT(10) AUTO_INCREMENT PRIMARY KEY NOT NULL, 62 | name VARCHAR(20) NOT NULL, 63 | email VARCHAR(55) NOT NULL, 64 | company_id int(10) NOT NULL 65 | ); 66 | ``` 67 | 68 | Hence, you a Java Record that can match that table: 69 | 70 | ```java 71 | public record Person(int id, String name, String email, int companyId) {} 72 | ``` 73 | Please note how *companyId* follows the traditional camelcase in Java, and *company_id* the underscore which is 74 | very popular in the database world. It can be like that, *Yorm* will take care and match both fields. 75 | 76 | Saving an object will be something as easy as: 77 | ```java 78 | Person person = new Person(0, "John", "john.doe@um.com", 1); 79 | int idPerson = yorm.save(person); 80 | ``` 81 | This will translate to SQL: 82 | ```sql 83 | INSERT INTO person (name, email, company_id) VALUES ("John", "john.doe@um.com", 1) 84 | ``` 85 | 86 | **Yorm** will detect that the id has a 0, consider it an INSERT, and insert it in the database, in table Person. It will return the id of the object just inserted. 87 | 88 | We might need to insert the object with its id, there is no problem: 89 | ```java 90 | Person person = new Person(2, "Mark", "mark.doe@um.com", 1); 91 | int idPerson = yorm.insert(person); 92 | ``` 93 | This operation will be automatically translated into 94 | ```sql 95 | INSERT INTO person (id, name, email, company_id) VALUES (2, "Mark", "mark.doe@um.com", 1) 96 | ``` 97 | 98 | The update operation follows the same pattern. Please bear in mind that Records are immutable, so we have to 99 | create a new one, and the id will be used to detect that it's an update operation: 100 | ```java 101 | Person person = new Person(2, "Jacob", "jacob.doe@um.com", 1); 102 | yorm.save(person); 103 | ``` 104 | Whose equivalent SQL would be: 105 | ```sql 106 | UPDATE person SET name = "Jacob", email="jacob.doe@um.com" WHERE id=2 AND company_id=1 107 | ``` 108 | Why is using company_id here? Well, it looks like a candidate to be a foreign key to a company table: 109 | ```sql 110 | CREATE TABLE company 111 | ( 112 | id INT(10) AUTO_INCREMENT PRIMARY KEY NOT NULL, 113 | name VARCHAR(20) NOT NULL, 114 | country_code VARCHAR(2) NOT NULL, 115 | creation_date DATE NOT NULL, 116 | is_active TINYINT NOT NULL 117 | ); 118 | 119 | ALTER TABLE `person` 120 | ADD CONSTRAINT `person_id` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`) ON DELETE CASCADE; 121 | ``` 122 | **Yorm** will detect that there is one Primary Key and a Multiple Key, and use them for the update. Nevertheless, **Yorm** also allows the update operation: 123 | ```java 124 | Person person = new Person(2, "Jacob", "jacob.doe@um.com", 1); 125 | yorm.update(person); 126 | ``` 127 | Insertion can even be massive: 128 | ```java 129 | Person person1 = new Person(2, "Hermione", "hermione.granger@hogwarts.com", 1); 130 | Person person2 = new Person(3, "Harry", "harry.potter@hogwarts.com", 1); 131 | Person person3 = new Person(4, "Sauron", "sauron@mordor.com", 2); 132 | List list = List.of(person1, person2, person3); 133 | yorm.insert(list); 134 | ``` 135 | We've inserted and updated elements in the table. How can we retrieve them into Records? The first and easiest way would be retrieving all the elements. It's just one line: 136 | ```java 137 | List personList = yorm.find(Person.class); 138 | ``` 139 | Which is the same as the SQL: 140 | ```sql 141 | SELECT id, name, email, company_id FROM person 142 | ``` 143 | And automatically wrapping the result into a List. 144 | Of course, we could just get one element, if we know the id: 145 | ```java 146 | Person person = yorm.find(Person.class, 1); 147 | ``` 148 | Which translates to SQL of: 149 | ```sql 150 | SELECT id, name, email, company_id FROM person WHERE id=1 151 | ``` 152 | Or even retrieve elements with a foreign key: 153 | ```java 154 | Company company = new Company(1, null, null, null, false); 155 | List personList = yorm.find(Person.class, company); 156 | ``` 157 | Pay attention how the Record company, with id 1, is used to retrieve a list of objects Person whose company_id is 1. **Yorm** will detect that Person has a foreign key with Company and use that to build the query: 158 | ```sql 159 | SELECT id, name, email, company_id FROM person WHERE company_id=1 160 | ``` 161 | Some kinds of filtering is also allowed: 162 | ```java 163 | Person personFilter1 = new Person(0, "harry", "john", 0); 164 | Person personFilter2 = new Person(0, null, null, 2); 165 | List list = List.of(personFilter1, personFilter2); 166 | List personList = yorm.find(list); 167 | ``` 168 | Let's review this one a bit. **Yorm** deals with Records. Here we have defined two Person records, the first one with some information on *name* and *email*, and the second one with a *company_id*. **Yorm** will ignore all the nulls and 0s on fields that are ids, but use the rest of the information to build a SELECT query and retrieve a List of Person Records: 169 | ```sql 170 | SELECT id, name, email, company_id FROM person WHERE name like '%harry%' OR email like '%john%' OR company_id=2 171 | ``` 172 | Based on [Benjiql] idea, **Yorm** also has some sort of fluent API capabilities to find records. The previous example could also be written like: 173 | ```java 174 | List personList = yorm.from(Person.class).where(Person::name).like("harry") 175 | .or(Person::email).like("john") 176 | .or(Person::companyId).equalTo(2) 177 | .find(); 178 | ``` 179 | **Yorm** can chain operands to build SQL alike sentences just with code: 180 | ```java 181 | List thirdList = yorm.from(Person.class).where(Person::name).equalTo("John") 182 | .and(Person::lastLogin).greaterThan(LocalDateTime.of(2019, 01, 01, 0, 0, 0)) 183 | .find(); 184 | ``` 185 | Which translated to SQL would be: 186 | ```sql 187 | SELECT id, name, email, last_login, company_id FROM person WHERE name = 'John' AND last_login >= '2019-01-01 00:00' 188 | ``` 189 | As a final note, Yorm works just by creating an instance of Yorm with a *javax.sql.DataSource*: 190 | ```java 191 | DataSource ds = DbConnector.getDatasource(parameters); 192 | Yorm yorm = new Yorm(ds); 193 | ``` 194 | **Yorm** is very young but nevertheless quite useful if you just want to perform basic CRUD operations, specially the ones that involve REST endpoints in the world of microservices. It's very to useful, and very transparent, since you just need a DataSource to put it to work. 195 | 196 | #### Use it with Maven 197 | You can easily add Yorm to your project with 198 | 199 | ```xml 200 | 201 | org.yorm 202 | yorm 203 | 0.8.1 204 | 205 | ``` 206 | 207 | 208 | #### Building from source 209 | ##### Dependencies 210 | * Docker 211 | 212 | **Yorm** is a small library published in Maven Central. It can be very easily compiled like 213 | 214 | ```sh 215 | mvn clean install 216 | ``` 217 | 218 | 219 | 220 | ## License 221 | 222 | Apache 2.0 223 | 224 | 225 | [//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax) 226 | 227 | [HikariCP]: 228 | [Mysql]: 229 | [PostgreSql]: 230 | [Junit 5]: 231 | [TestContainers]: 232 | [Slf4j]: 233 | [PabloGrisafi]: 234 | [Hibernate]: 235 | [Jooq]: 236 | [Benjiql]: 237 | 238 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.yorm 8 | yorm 9 | 0.8.1 10 | 11 | jar 12 | 13 | Yorm 14 | Simple ORM-alike based on Java Records 15 | https://github.com/naynecoder/yorm 16 | 17 | 18 | 19 | https://github.com/naynecoder/yorm 20 | 21 | 22 | 23 | 24 | 25 | The Apache License, Version 2.0 26 | http://www.apache.org/licenses/LICENSE-2.0.txt 27 | 28 | 29 | 30 | 31 | scm:git:git://github.com/naynecoder/yorm.git 32 | scm:git:ssh://github.com:naynecoder/yorm.git 33 | https://github.com/naynecoder/yorm 34 | 35 | 36 | 37 | 17 38 | 17 39 | 5.10.3 40 | 5.1.0 41 | 8.0.33 42 | 2.0.13 43 | 2.23.1 44 | 1.20.0 45 | 3.0.0-M5 46 | 2.23.1 47 | 42.7.3 48 | 49 | 50 | 51 | org.slf4j 52 | slf4j-api 53 | ${version.slf4j-api} 54 | 55 | 56 | org.apache.logging.log4j 57 | log4j-slf4j-impl 58 | ${version.log4j-slf4j-impl} 59 | test 60 | 61 | 62 | org.slf4j 63 | slf4j-simple 64 | 2.0.13 65 | test 66 | 67 | 68 | org.apache.logging.log4j 69 | log4j-api 70 | ${version.log4j} 71 | test 72 | 73 | 74 | org.apache.logging.log4j 75 | log4j-core 76 | ${version.log4j} 77 | test 78 | 79 | 80 | 81 | org.junit.jupiter 82 | junit-jupiter-api 83 | ${version.junit} 84 | 85 | 86 | org.testcontainers 87 | testcontainers 88 | ${version.testcontainers} 89 | test 90 | 91 | 92 | com.zaxxer 93 | HikariCP 94 | ${version.hikaricp} 95 | test 96 | 97 | 98 | mysql 99 | mysql-connector-java 100 | ${version.mysql} 101 | test 102 | 103 | 104 | org.postgresql 105 | postgresql 106 | ${version.postgresql} 107 | test 108 | 109 | 110 | org.testcontainers 111 | mysql 112 | ${version.testcontainers} 113 | test 114 | 115 | 116 | org.testcontainers 117 | postgresql 118 | ${version.testcontainers} 119 | test 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-compiler-plugin 129 | 3.8.0 130 | 131 | 17 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-surefire-plugin 137 | ${version.maven-surefire} 138 | 139 | 140 | org.apache.maven.plugins 141 | maven-source-plugin 142 | 3.2.1 143 | 144 | 145 | attach-sources 146 | 147 | jar-no-fork 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module org.yorm { 2 | requires java.sql; 3 | requires org.slf4j; 4 | exports org.yorm; 5 | opens org.yorm; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/IdentifiableFunction.java: -------------------------------------------------------------------------------- 1 | package org.yorm; 2 | 3 | import org.yorm.exception.YormException; 4 | 5 | import java.io.Serializable; 6 | import java.lang.invoke.MethodHandle; 7 | import java.lang.invoke.MethodHandles; 8 | import java.lang.invoke.MethodType; 9 | import java.lang.invoke.SerializedLambda; 10 | import java.lang.reflect.Constructor; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.function.Function; 15 | 16 | /** 17 | * A {@link Serializable} version of {@link Function}. While we do not actually serialize lambdas, the serialized 18 | * form provides information about the invocation. Like the method name, and the type 19 | * @param 20 | * @param 21 | */ 22 | public interface IdentifiableFunction extends Serializable { 23 | R apply(T t); 24 | 25 | default String getCallingFunctionName() { 26 | return IdentifiableFunction.getCallingFunctionName(this); 27 | } 28 | 29 | class PrivateData 30 | { 31 | static PrivateData INSTANCE = new PrivateData(); 32 | 33 | final Map, MethodHandle> writeReplaceMethodHandles = new HashMap<>(); 34 | 35 | private PrivateData() { 36 | } 37 | } 38 | 39 | private static String getCallingFunctionName(IdentifiableFunction identifiableFunction) { 40 | try { 41 | MethodHandle writeReplaceMethodHandle = 42 | PrivateData.INSTANCE.writeReplaceMethodHandles.computeIfAbsent(identifiableFunction.getClass(), clazz -> { 43 | try { 44 | return MethodHandles.privateLookupIn(clazz, MethodHandles.lookup()).findVirtual( 45 | identifiableFunction.getClass(), 46 | "writeReplace", 47 | MethodType.methodType(Object.class) 48 | ); 49 | } catch (NoSuchMethodException | IllegalAccessException e) { 50 | throw new RuntimeException(e); 51 | } 52 | } 53 | ); 54 | SerializedLambda serializedLambda = (SerializedLambda) writeReplaceMethodHandle.invoke(identifiableFunction); 55 | 56 | return serializedLambda.getImplMethodName(); 57 | } catch (Throwable t) { 58 | throw new RuntimeException("Could not identify function name.", t); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/MapBuilder.java: -------------------------------------------------------------------------------- 1 | package org.yorm; 2 | 3 | import static org.yorm.util.RowRecordConverter.converterFor; 4 | 5 | import java.lang.reflect.Constructor; 6 | import java.lang.reflect.Field; 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Parameter; 9 | import java.sql.Connection; 10 | import java.sql.DatabaseMetaData; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | import java.sql.Types; 14 | import java.util.ArrayList; 15 | import java.util.Arrays; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | import java.util.Locale; 19 | import java.util.Map; 20 | import java.util.Set; 21 | import java.util.stream.Collectors; 22 | import javax.sql.DataSource; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.yorm.exception.YormException; 26 | import org.yorm.util.DbType; 27 | import org.yorm.util.Levenshtein; 28 | 29 | public class MapBuilder { 30 | 31 | private final DataSource ds; 32 | private static Logger logger = LoggerFactory.getLogger(MapBuilder.class); 33 | 34 | public MapBuilder(DataSource ds) { 35 | this.ds = ds; 36 | } 37 | 38 | public YormTable buildMap(Class recordClass) throws YormException { 39 | String recordClassName = recordClass.getSimpleName().toLowerCase(Locale.ROOT); 40 | String dbTable = recordClassName; 41 | Field[] objectFields = recordClass.getDeclaredFields(); 42 | Map methods = Arrays.stream(recordClass.getDeclaredMethods()) 43 | .collect(Collectors.toMap( 44 | method -> method.getName().toLowerCase(), 45 | method -> method 46 | )); 47 | List tuples; 48 | try (Connection connection = ds.getConnection()) { 49 | DatabaseMetaData metaData = connection.getMetaData(); 50 | dbTable = getMatchingTableName(metaData, recordClassName); 51 | List descriptionList = getDescription(metaData, dbTable); 52 | tuples = populateMap(objectFields, descriptionList, methods); 53 | } catch (SQLException | YormException e) { 54 | throw new YormException("Error mapping record " + recordClassName, e); 55 | } 56 | if (logger.isDebugEnabled()) { 57 | logger.debug("Record:{} mapped to table:{}", recordClass.getName(), dbTable); 58 | for (var tuple : tuples) { 59 | logger.debug(" Field:{} mapped to column:{} type:{} nullable:{} primaryKey:{} autoIncrement:{}", 60 | tuple.objectFieldName(), tuple.dbFieldName(), tuple.type(), tuple.isNullable(), tuple.isPrimaryKey(), tuple.isAutoincrement()); 61 | } 62 | } 63 | Constructor constructor = findMatchingConstructor(recordClass, tuples); 64 | tuples = sortParametersForConstructor(tuples, constructor); 65 | return new YormTable(dbTable, tuples, constructor); 66 | } 67 | 68 | private Constructor findMatchingConstructor(Class recordClass, List tuples) throws YormException { 69 | Constructor[] constructors = (Constructor[]) recordClass.getConstructors(); 70 | for (Constructor constructor : constructors) { 71 | Parameter[] params = constructor.getParameters(); 72 | if (params.length == tuples.size()) { 73 | return constructor; 74 | } 75 | } 76 | throw new YormException("Couldn't find a constructor that matches the number of fields in the record: " + recordClass.getName()); 77 | } 78 | 79 | private List sortParametersForConstructor(List tuples, Constructor constructor) throws YormException { 80 | Parameter[] params = constructor.getParameters(); 81 | List sortedList = new ArrayList<>(); 82 | for (Parameter param : params) { 83 | String name = param.getName(); 84 | YormTuple mt = tuples.stream().filter(t -> t.objectFieldName().equals(name)).findFirst() 85 | .orElseThrow(() -> new YormException("Couldn't find a field name that matches the constructor: " + name)); 86 | sortedList.add(mt); 87 | } 88 | return sortedList; 89 | } 90 | 91 | private String getMatchingTableName(DatabaseMetaData metaData, String recordClassName) throws SQLException { 92 | ResultSet resultSet = metaData.getTables(null, null, null, new String[]{"TABLE", "VIEW"}); 93 | List tables = new ArrayList<>(); 94 | while (resultSet.next()) { 95 | tables.add(resultSet.getString("TABLE_NAME")); 96 | } 97 | return findClosest(tables, recordClassName); 98 | } 99 | 100 | private List populateMap(Field[] objectFields, List descriptionList, Map methods) throws YormException { 101 | List tuples = new ArrayList<>(); 102 | Set alreadyUsedObjectFields = new HashSet<>(); 103 | List objectFieldNames = Arrays.stream(objectFields).map(Field::getName).toList(); 104 | for (Description description : descriptionList) { 105 | String objectField = findClosest(objectFieldNames, description.columnName()); 106 | if (objectField != null && alreadyUsedObjectFields.contains(objectField)) { 107 | continue; 108 | } 109 | Method method = methods.get(objectField.toLowerCase()); 110 | alreadyUsedObjectFields.add(objectField); 111 | Class recordType = method.getReturnType(); 112 | Class dbType = DbType.getType(description.type()).javaType; 113 | var tuple = new YormTuple( 114 | description.columnName(), 115 | objectField, 116 | DbType.getType(description.type()), 117 | Integer.parseInt(description.size()), 118 | yesNoToBoolean(description.isNullable()), 119 | description.isPrimaryKey(), 120 | yesNoToBoolean(description.isAutoincrement()), 121 | method, 122 | converterFor(recordType, dbType), 123 | converterFor(dbType, recordType) 124 | ); 125 | tuples.add(tuple); 126 | 127 | } 128 | return tuples; 129 | } 130 | 131 | private boolean yesNoToBoolean(String str) throws YormException { 132 | if (str == null || str.isBlank()) { 133 | return false; 134 | } 135 | if ("YES".equalsIgnoreCase(str)) { 136 | return true; 137 | } 138 | if ("NO".equalsIgnoreCase(str)) { 139 | return false; 140 | } 141 | throw new YormException("Invalid value " + str + ". Must be YES or NO or null or blank"); 142 | 143 | } 144 | 145 | private List getDescription(DatabaseMetaData metaData, String table) throws SQLException { 146 | List descriptionList = new ArrayList<>(); 147 | List primaryKeysColumns = new ArrayList<>(); 148 | try (ResultSet rsColumn = metaData.getPrimaryKeys(null, null, table)) { 149 | while (rsColumn.next()) { 150 | primaryKeysColumns.add(rsColumn.getString("COLUMN_NAME")); 151 | } 152 | } 153 | try (ResultSet rsColumn = metaData.getColumns(null, null, table, null)) { 154 | while (rsColumn.next()) { 155 | String columnName = rsColumn.getString("COLUMN_NAME"); 156 | String typeName = rsColumn.getString("TYPE_NAME"); 157 | String type; 158 | if ("ENUM".equals(typeName)) { 159 | type = String.valueOf(Types.VARCHAR); 160 | } else { 161 | type = rsColumn.getString("DATA_TYPE"); 162 | } 163 | String size = rsColumn.getString("COLUMN_SIZE"); 164 | String isNull = rsColumn.getString("IS_NULLABLE"); 165 | String isAutoincrement = rsColumn.getString("IS_AUTOINCREMENT"); 166 | boolean isPrimaryKey = primaryKeysColumns.contains(columnName); 167 | descriptionList.add(new Description(columnName, type, size, isNull, isPrimaryKey, isAutoincrement)); 168 | } 169 | } catch (SQLException e) { 170 | logger.error("Error mapping table {}", table, e); 171 | } 172 | return descriptionList; 173 | } 174 | 175 | private record Description(String columnName, String type, String size, String isNullable, Boolean isPrimaryKey, String isAutoincrement) { 176 | 177 | } 178 | 179 | private String findClosest(List candidates, String target) { 180 | String closest = null; 181 | int distance = 100; 182 | for (String field : candidates) { 183 | int tempDist = Levenshtein.calculate(cleanName(field), cleanName(target)); 184 | if (tempDist < distance) { 185 | closest = field; 186 | distance = tempDist; 187 | } 188 | } 189 | return closest; 190 | } 191 | 192 | private String cleanName(String str) { 193 | return str.toLowerCase(Locale.ROOT).replace("_", ""); 194 | } 195 | } 196 | 197 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/Yorm.java: -------------------------------------------------------------------------------- 1 | package org.yorm; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Locale; 8 | import java.util.Map; 9 | import javax.sql.DataSource; 10 | import org.yorm.db.QueryBuilder; 11 | import org.yorm.db.operations.select.Select; 12 | import org.yorm.exception.YormException; 13 | 14 | public class Yorm { 15 | 16 | private final Map map = new HashMap<>(); 17 | private final MapBuilder mapBuilder; 18 | private final QueryBuilder queryBuilder; 19 | private final DataSource ds; 20 | 21 | public Yorm(DataSource ds) { 22 | this.ds = ds; 23 | this.mapBuilder = new MapBuilder(ds); 24 | this.queryBuilder = new QueryBuilder(ds); 25 | } 26 | 27 | public long save(T recordObj) throws YormException { 28 | String objectName = getRecordName(recordObj); 29 | YormTable yormTable = getTable(objectName, recordObj.getClass()); 30 | long result; 31 | try { 32 | result = queryBuilder.save(ds, recordObj, yormTable); 33 | } catch (InvocationTargetException | IllegalAccessException e) { 34 | throw new YormException("Error while saving record:" + recordObj, e); 35 | } 36 | return result; 37 | } 38 | 39 | public long insert(T recordObj) throws YormException { 40 | String objectName = getRecordName(recordObj); 41 | YormTable yormTable = getTable(objectName, recordObj.getClass()); 42 | return queryBuilder.insert(ds, recordObj, yormTable); 43 | } 44 | 45 | public void insert(List recordListObj) throws YormException { 46 | if (recordListObj.isEmpty()) { 47 | return; 48 | } 49 | T recordObj = recordListObj.get(0); 50 | String objectName = getRecordName(recordObj); 51 | YormTable yormTable = getTable(objectName, recordObj.getClass()); 52 | queryBuilder.bulkInsert(ds, recordListObj, yormTable); 53 | } 54 | 55 | public void update(T recordObj) throws YormException { 56 | String objectName = getRecordName(recordObj); 57 | YormTable yormTable = getTable(objectName, recordObj.getClass()); 58 | queryBuilder.update(ds, recordObj, yormTable); 59 | } 60 | 61 | public T find(Class recordObject, long id) throws YormException { 62 | String objectName = getClassName(recordObject); 63 | YormTable yormTable = getTable(objectName, recordObject); 64 | return queryBuilder.find(ds, yormTable, id); 65 | } 66 | 67 | public List find(Class referenceObject, Record filterObject) throws YormException { 68 | String filterObjectName = getRecordName(filterObject); 69 | String referenceObjectName = getClassName(referenceObject); 70 | YormTable yormTableFilter = getTable(filterObjectName, filterObject.getClass()); 71 | YormTable yormTableObject = getTable(referenceObjectName, referenceObject); 72 | List result; 73 | try { 74 | result = queryBuilder.find(ds, filterObject, yormTableFilter, yormTableObject); 75 | } catch (InvocationTargetException | IllegalAccessException | YormException e) { 76 | throw new YormException("Error while finding records with reference:" + referenceObject + " and filter:" + filterObject, e); 77 | } 78 | return result; 79 | } 80 | 81 | public List find(Class referenceObject) throws YormException { 82 | String referenceObjectName = getClassName(referenceObject); 83 | YormTable yormTable = getTable(referenceObjectName, referenceObject); 84 | return queryBuilder.find(ds, yormTable); 85 | } 86 | 87 | public Select from(Class referenceObject) throws YormException { 88 | String referenceObjectName = getClassName(referenceObject); 89 | YormTable yormTable = getTable(referenceObjectName, referenceObject); 90 | return new Select<>(ds, yormTable); 91 | } 92 | 93 | public List find(List list) throws YormException { 94 | List result = new ArrayList<>(); 95 | if (list.isEmpty()) { 96 | return result; 97 | } 98 | Record recordObj = list.get(0); 99 | String objectName = getRecordName(recordObj); 100 | YormTable yormTable = getTable(objectName, recordObj.getClass()); 101 | try { 102 | result = queryBuilder.find(ds, list, yormTable); 103 | } catch (InvocationTargetException | IllegalAccessException | YormException e) { 104 | throw new YormException("Error while finding records with list", e); 105 | } 106 | return result; 107 | } 108 | 109 | 110 | private String getClassName(Class clazz) { 111 | return clazz.getSimpleName().toLowerCase(Locale.ROOT); 112 | } 113 | 114 | private String getRecordName(Record recordObj) { 115 | return recordObj.getClass().getSimpleName().toLowerCase(Locale.ROOT); 116 | } 117 | 118 | public boolean delete(Class recordObject, long id) throws YormException { 119 | String objectName = getClassName(recordObject); 120 | YormTable yormTable = getTable(objectName, recordObject); 121 | return queryBuilder.delete(ds, yormTable, id); 122 | } 123 | 124 | private YormTable getTable(String objectName, Class recordObject) throws YormException { 125 | YormTable yormTable = map.get(objectName); 126 | if (yormTable == null) { 127 | yormTable = mapBuilder.buildMap(recordObject); 128 | } 129 | map.put(objectName, yormTable); 130 | return yormTable; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/YormTable.java: -------------------------------------------------------------------------------- 1 | package org.yorm; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import org.yorm.exception.YormException; 8 | 9 | public record YormTable( 10 | String dbTable, 11 | List tuples, 12 | Constructor constructor, 13 | String concatenatedFieldNames, 14 | String selectAllFromTable, 15 | boolean hasPrimaryKey 16 | ) { 17 | 18 | public YormTable(String dbTable, List tuples, Constructor constructor) throws YormException { 19 | this( 20 | dbTable, 21 | tuples, 22 | constructor, 23 | tuples.stream().map(YormTuple::dbFieldName).collect(Collectors.joining(", ")), 24 | tuples.stream().map(YormTuple::dbFieldName).collect(Collectors.joining(", ", "SELECT ", " FROM " + dbTable)), 25 | tuples.stream().anyMatch(YormTuple::isPrimaryKey) 26 | ); 27 | } 28 | 29 | public YormTuple getTupleWithDBFieldName(String fieldName) throws YormException { 30 | return this.tuples().stream().filter(tuple -> tuple.dbFieldName().equals(fieldName)) 31 | .findFirst() 32 | .orElseThrow(() -> new YormException("Field not found: " + fieldName)); 33 | } 34 | 35 | public YormTuple getTupleWithObjectFieldName(String fieldName) throws YormException { 36 | return this.tuples().stream().filter(tuple -> tuple.objectFieldName().equals(fieldName)) 37 | .findFirst() 38 | .orElseThrow(() -> new YormException("Field not found: " + fieldName)); 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/YormTuple.java: -------------------------------------------------------------------------------- 1 | package org.yorm; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import org.yorm.util.DbType; 6 | import org.yorm.util.RowRecordConverter; 7 | 8 | public record YormTuple( 9 | String dbFieldName, 10 | String objectFieldName, 11 | DbType type, 12 | int size, 13 | boolean isNullable, 14 | boolean isPrimaryKey, 15 | boolean isAutoincrement, 16 | Method method, 17 | RowRecordConverter.Converter serializer, 18 | RowRecordConverter.Converter deserializer 19 | ) { 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/FilteringFieldValue.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db; 2 | 3 | import org.yorm.db.operations.operators.ComparisonOperator; 4 | import org.yorm.db.operations.operators.WhereOperator; 5 | import org.yorm.util.DbType; 6 | 7 | public record FilteringFieldValue(String fieldName, DbType dbType, Object value, ComparisonOperator comparisonOperator, WhereOperator whereOperator) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/QueryBuilder.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Optional; 8 | import javax.sql.DataSource; 9 | import org.yorm.YormTable; 10 | import org.yorm.YormTuple; 11 | import org.yorm.db.operations.FilterPredicates; 12 | import org.yorm.db.operations.QueryDelete; 13 | import org.yorm.db.operations.QueryFind; 14 | import org.yorm.db.operations.QuerySave; 15 | import org.yorm.db.operations.operators.ComparisonOperator; 16 | import org.yorm.db.operations.operators.WhereOperator; 17 | import org.yorm.exception.YormException; 18 | import org.yorm.util.DbType; 19 | 20 | public class QueryBuilder { 21 | 22 | DataSource ds; 23 | 24 | public QueryBuilder(DataSource ds) { 25 | this.ds = ds; 26 | } 27 | 28 | public long insert(DataSource ds, Record obj, YormTable yormTable) throws YormException { 29 | return QuerySave.forceInsert(ds, obj, yormTable); 30 | } 31 | 32 | public void update(DataSource ds, Record obj, YormTable yormTable) throws YormException { 33 | QuerySave.update(ds, obj, yormTable); 34 | } 35 | 36 | public void bulkInsert(DataSource ds, List list, YormTable yormTable) throws YormException { 37 | QuerySave.bulkInsert(ds, list, yormTable); 38 | } 39 | 40 | public long save(DataSource ds, Record obj, YormTable yormTable) throws InvocationTargetException, IllegalAccessException, YormException { 41 | Optional idField = yormTable.tuples().stream().filter(FilterPredicates.filterAutoIncrementKey()).findFirst(); 42 | long idInsert = 0; 43 | if (idField.isEmpty()) { 44 | idInsert = QuerySave.forceInsert(ds, obj, yormTable); 45 | } else { 46 | YormTuple yormTuple = idField.get(); 47 | if (emptyId(obj, yormTuple)) { 48 | idInsert = QuerySave.insert(ds, obj, yormTable); 49 | } else { 50 | QuerySave.update(ds, obj, yormTable); 51 | } 52 | } 53 | return idInsert; 54 | } 55 | 56 | private boolean emptyId(Record obj, YormTuple yormTuple) throws InvocationTargetException, IllegalAccessException { 57 | Object idObject = yormTuple.method().invoke(obj); 58 | if (idObject == null) { 59 | return true; 60 | } 61 | if (yormTuple.method().getReturnType().getName().equalsIgnoreCase("long")) { 62 | return (long) idObject == 0; 63 | } 64 | if (yormTuple.method().getReturnType().getName().equalsIgnoreCase("int")) { 65 | return (int) idObject == 0; 66 | } 67 | return false; 68 | } 69 | 70 | public boolean delete(DataSource ds, YormTable yormTable, long id) throws YormException { 71 | return QueryDelete.delete(ds, yormTable, id); 72 | } 73 | 74 | public T find(DataSource ds, YormTable yormTable, long id) throws YormException { 75 | return QueryFind.findById(ds, yormTable, id); 76 | } 77 | 78 | public List find(DataSource ds, Record filterObject, YormTable yormTableFilter, YormTable yormTableObject) 79 | throws InvocationTargetException, IllegalAccessException, YormException { 80 | String foreignKeyFilter = yormTableFilter.dbTable() + "_id"; 81 | String foreignKey = yormTableFilter.dbTable() + "_id"; 82 | Optional yormTuple = yormTableObject.tuples().stream().filter(t -> t.dbFieldName().equalsIgnoreCase(foreignKeyFilter)).findFirst(); 83 | if (yormTuple.isEmpty()) { 84 | String foreignKeySecondAttempt = "id_" + yormTableFilter.dbTable(); 85 | yormTuple = yormTableObject.tuples().stream().filter(t -> t.dbFieldName().equalsIgnoreCase(foreignKeySecondAttempt)).findFirst(); 86 | foreignKey = foreignKeySecondAttempt; 87 | } 88 | if (yormTuple.isEmpty()) { 89 | return new ArrayList<>(); 90 | } 91 | Optional optionalFilteringTupleId = yormTableFilter.tuples().stream().filter(FilterPredicates.getId()).findFirst(); 92 | if (optionalFilteringTupleId.isEmpty()) { 93 | return new ArrayList<>(); 94 | } 95 | int id = (int) optionalFilteringTupleId.get().method().invoke(filterObject); 96 | return QueryFind.findByForeignId(ds, yormTableObject, foreignKey, id); 97 | } 98 | 99 | public List find(DataSource ds, YormTable yormTable) throws YormException { 100 | return QueryFind.findAll(ds, yormTable); 101 | } 102 | 103 | public List find(DataSource ds, List list, YormTable yormTable, WhereOperator whereOperator) 104 | throws InvocationTargetException, IllegalAccessException, YormException { 105 | List filteringFieldValueList = getFieldValues(list, yormTable, whereOperator); 106 | return QueryFind.findFiltering(ds, yormTable, filteringFieldValueList); 107 | } 108 | 109 | public List find(DataSource ds, List list, YormTable yormTable) throws InvocationTargetException, IllegalAccessException, YormException { 110 | List filteringFieldValueList = getFieldValues(list, yormTable, WhereOperator.OR); 111 | return QueryFind.findFiltering(ds, yormTable, filteringFieldValueList); 112 | } 113 | 114 | private List getFieldValues(List list, YormTable yormTable, WhereOperator whereOperator) 115 | throws IllegalAccessException, InvocationTargetException { 116 | List filteringFieldValueList = new ArrayList<>(); 117 | List yormTuples = yormTable.tuples(); 118 | for (Record obj : list) { 119 | for (YormTuple yormTuple : yormTuples) { 120 | Method method = yormTuple.method(); 121 | Object value = method.invoke(obj); 122 | if (value != null) { 123 | mapValues(filteringFieldValueList, yormTuple, value, whereOperator); 124 | } 125 | } 126 | } 127 | return filteringFieldValueList; 128 | } 129 | 130 | private void mapValues(List filteringFieldValueList, YormTuple yormTuple, Object value, WhereOperator whereOperator) { 131 | String dbFieldName = yormTuple.dbFieldName(); 132 | DbType type = yormTuple.type(); 133 | if (type.equals(DbType.CHAR) || type.equals(DbType.VARCHAR)) { 134 | String valueStr = (String) value; 135 | if (!valueStr.isEmpty()) { 136 | filteringFieldValueList.add(new FilteringFieldValue(dbFieldName, type, "%" + valueStr + "%", ComparisonOperator.LIKE, whereOperator)); 137 | } 138 | } else { 139 | filteringFieldValueList.add(new FilteringFieldValue(dbFieldName, type, value, ComparisonOperator.EQUALS, whereOperator)); 140 | } 141 | } 142 | } 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/operations/FilterPredicates.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db.operations; 2 | 3 | import java.lang.reflect.Method; 4 | import java.util.function.Predicate; 5 | import org.yorm.YormTuple; 6 | 7 | public class FilterPredicates { 8 | 9 | private FilterPredicates(){} 10 | 11 | public static Predicate getId() { 12 | return yt -> yt.isPrimaryKey() && yt.dbFieldName().equalsIgnoreCase("id"); 13 | } 14 | 15 | public static Predicate filterAutoIncrementKey() { 16 | return yt -> yt.isPrimaryKey() && yt.isAutoincrement(); 17 | } 18 | 19 | public static Predicate getMethod(String fieldName) { 20 | return m -> m.getName().equalsIgnoreCase(fieldName); 21 | } 22 | 23 | public static Predicate filterOutPrimaryKeys() { 24 | return yt -> !yt.isPrimaryKey(); 25 | } 26 | 27 | public static Predicate filterKeepKeys() { 28 | return YormTuple::isPrimaryKey; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/operations/QueryDelete.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db.operations; 2 | 3 | import java.sql.Connection; 4 | import java.sql.PreparedStatement; 5 | import java.sql.SQLException; 6 | import javax.sql.DataSource; 7 | import org.yorm.YormTable; 8 | import org.yorm.exception.YormException; 9 | 10 | public class QueryDelete { 11 | 12 | private QueryDelete() { 13 | } 14 | 15 | public static boolean delete(DataSource ds, YormTable yormTable, long id) throws YormException { 16 | StringBuilder query = new StringBuilder("DELETE "); 17 | query.append(" FROM ") 18 | .append(yormTable.dbTable()) 19 | .append(" WHERE id=?"); 20 | try (Connection connection = ds.getConnection(); 21 | PreparedStatement preparedStatement = connection.prepareStatement(query.toString())) { 22 | preparedStatement.setLong(1, id); 23 | return preparedStatement.executeUpdate() > 0; 24 | } catch (SQLException e) { 25 | throw new YormException("Error while deleting record with id:" + id + " from table:" + yormTable.dbTable(), e); 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/operations/QueryFind.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db.operations; 2 | 3 | import org.yorm.YormTable; 4 | import org.yorm.YormTuple; 5 | import org.yorm.db.FilteringFieldValue; 6 | import org.yorm.exception.YormException; 7 | import org.yorm.util.RowRecordConverter; 8 | 9 | import javax.sql.DataSource; 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.sql.Connection; 12 | import java.sql.PreparedStatement; 13 | import java.sql.ResultSet; 14 | import java.sql.SQLException; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | public class QueryFind { 19 | 20 | private static final RowRecordConverter rowRecordConverter = new RowRecordConverter(); 21 | 22 | private QueryFind() { 23 | } 24 | 25 | public static List findAll(DataSource ds, YormTable yormTable) throws YormException { 26 | List tuples = yormTable.tuples(); 27 | List resultList = new ArrayList<>(); 28 | try (Connection connection = ds.getConnection(); 29 | PreparedStatement preparedStatement = connection.prepareStatement(yormTable.selectAllFromTable())) { 30 | ResultSet rs = preparedStatement.executeQuery(); 31 | while (rs.next()) { 32 | Object[] values = new Object[tuples.size()]; 33 | int params = 0; 34 | loopResults(tuples, rs, values, params); 35 | resultList.add((T) yormTable.constructor().newInstance(values)); 36 | } 37 | } catch (SQLException | InstantiationException | IllegalAccessException | InvocationTargetException e) { 38 | throw new YormException("Error while getting all records from table:" + yormTable.dbTable(), e); 39 | } 40 | return resultList; 41 | } 42 | 43 | public static T findById(DataSource ds, YormTable yormTable, long id) throws YormException { 44 | List tuples = yormTable.tuples(); 45 | String query = yormTable.selectAllFromTable() + " WHERE ID = ?"; 46 | Object result = null; 47 | try (Connection connection = ds.getConnection(); 48 | PreparedStatement preparedStatement = connection.prepareStatement(query)) { 49 | preparedStatement.setLong(1, id); 50 | ResultSet rs = preparedStatement.executeQuery(); 51 | if (rs.next()) { 52 | Object[] values = new Object[tuples.size()]; 53 | int params = 0; 54 | loopResults(tuples, rs, values, params); 55 | result = yormTable.constructor().newInstance(values); 56 | } 57 | 58 | 59 | } catch (SQLException | InstantiationException | IllegalAccessException | InvocationTargetException e) { 60 | throw new YormException("Error while getting record with id:" + id + " from table:" + yormTable.dbTable(), e); 61 | } 62 | return (T) result; 63 | } 64 | 65 | public static List findByForeignId(DataSource ds, YormTable yormTable, String fieldName, long id) throws YormException { 66 | List resultList = new ArrayList<>(); 67 | List tuples = yormTable.tuples(); 68 | String query = yormTable.selectAllFromTable() + " WHERE " + fieldName + " = ?"; 69 | try (Connection connection = ds.getConnection(); 70 | PreparedStatement preparedStatement = connection.prepareStatement(query)) { 71 | preparedStatement.setLong(1, id); 72 | ResultSet rs = preparedStatement.executeQuery(); 73 | while (rs.next()) { 74 | Object[] values = new Object[tuples.size()]; 75 | int params = 0; 76 | loopResults(tuples, rs, values, params); 77 | Object result = yormTable.constructor().newInstance(values); 78 | resultList.add((T) result); 79 | } 80 | 81 | 82 | } catch (SQLException | InstantiationException | IllegalAccessException | InvocationTargetException e) { 83 | throw new YormException("Error while deleting record with foreign id:" + id + " from table:" + yormTable.dbTable(), e); 84 | } 85 | return resultList; 86 | } 87 | 88 | public static List findFiltering(DataSource ds, YormTable yormTable, List filteringList) throws YormException { 89 | List resultList = new ArrayList<>(); 90 | List tuples = yormTable.tuples(); 91 | StringBuilder query = new StringBuilder(yormTable.selectAllFromTable()); 92 | if (!filteringList.isEmpty()) { 93 | query.append(" WHERE ") 94 | .append(String.join(" ", 95 | filteringList.stream().map(fv -> fv.whereOperator().getOperator() + " " + fv.fieldName() + " " + fv.comparisonOperator().getOperator() + " ? ").toList())); 96 | } 97 | String completeQuery = cleanSql(query.toString()); 98 | try (Connection connection = ds.getConnection(); 99 | PreparedStatement preparedStatement = connection.prepareStatement(completeQuery)) { 100 | int paramIndex = 1; 101 | for (FilteringFieldValue fieldValue : filteringList) { 102 | rowRecordConverter.recordToRow(paramIndex, preparedStatement, fieldValue.fieldName(), fieldValue.value(), fieldValue.dbType(), yormTable.getTupleWithDBFieldName(fieldValue.fieldName()).serializer()); 103 | paramIndex++; 104 | } 105 | ResultSet rs = preparedStatement.executeQuery(); 106 | while (rs.next()) { 107 | Object[] values = new Object[tuples.size()]; 108 | int params = 0; 109 | loopResults(tuples, rs, values, params); 110 | Object result = yormTable.constructor().newInstance(values); 111 | resultList.add((T) result); 112 | } 113 | } catch (SQLException | InstantiationException | IllegalAccessException | InvocationTargetException e) { 114 | throw new YormException("Error while filtering records with filtering list:" + filteringList + " from table:" + yormTable.dbTable(), e); 115 | } 116 | return resultList; 117 | } 118 | 119 | private static String cleanSql(String query) { 120 | query = query.replace(" ", " "); 121 | query = query.replace("WHERE OR", "WHERE"); 122 | query = query.replace("WHERE AND", "WHERE"); 123 | return query; 124 | } 125 | 126 | private static void loopResults(List tuples, ResultSet rs, Object[] values, int params) throws SQLException, YormException { 127 | for (YormTuple tuple : tuples) { 128 | params = rowRecordConverter.rowToRecord(rs, values, params, tuple.dbFieldName(), tuple.type(), tuple.deserializer()); 129 | } 130 | } 131 | 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/operations/QuerySave.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db.operations; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | import java.sql.Connection; 6 | import java.sql.PreparedStatement; 7 | import java.sql.ResultSet; 8 | import java.sql.SQLException; 9 | import java.sql.Statement; 10 | import java.util.List; 11 | import java.util.function.Predicate; 12 | import javax.sql.DataSource; 13 | import org.yorm.YormTable; 14 | import org.yorm.YormTuple; 15 | import org.yorm.exception.YormException; 16 | import org.yorm.util.DbType; 17 | import org.yorm.util.RowRecordConverter; 18 | 19 | public class QuerySave { 20 | 21 | private static final RowRecordConverter rowRecordConverter = new RowRecordConverter(); 22 | 23 | private QuerySave() { 24 | } 25 | 26 | public static void bulkInsert(DataSource ds, List objList, YormTable yormTable) throws YormException { 27 | String op = "?,"; 28 | List tuples = yormTable.tuples(); 29 | StringBuilder query = new StringBuilder("INSERT INTO "); 30 | query.append(yormTable.dbTable()) 31 | .append(" (") 32 | .append(yormTable.concatenatedFieldNames()) 33 | .append(") VALUES "); 34 | String operands = op.repeat(tuples.size()); 35 | operands = operands.substring(0, operands.length() - 1); 36 | for (int k = 0; k < objList.size(); k++) { 37 | query.append("(").append(operands).append("),"); 38 | } 39 | query.deleteCharAt(query.length() - 1); 40 | query.append(";"); 41 | try (Connection connection = ds.getConnection(); 42 | PreparedStatement preparedStatement = connection.prepareStatement(query.toString(), 43 | yormTable.hasPrimaryKey() ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS)) { 44 | int paramIndex = 1; 45 | for (Record obj : objList) { 46 | paramIndex = populatePreparedStatement(tuples, paramIndex, preparedStatement, obj); 47 | } 48 | preparedStatement.executeUpdate(); 49 | } catch (SQLException | IllegalAccessException | InvocationTargetException e) { 50 | throw new YormException("Error while bulk inserting records:" + objList + " into table:" + yormTable.dbTable(), e); 51 | } 52 | } 53 | 54 | public static long forceInsert(DataSource ds, Record obj, YormTable yormTable) throws YormException { 55 | String op = "?,"; 56 | long id = 0; 57 | List tuples = yormTable.tuples(); 58 | StringBuilder query = new StringBuilder("INSERT INTO "); 59 | query.append(yormTable.dbTable()) 60 | .append(" (") 61 | .append(yormTable.concatenatedFieldNames()) 62 | .append(") VALUES (") 63 | .append(op.repeat(tuples.size())); 64 | query.deleteCharAt(query.length() - 1); 65 | query.append(")"); 66 | try (Connection connection = ds.getConnection(); 67 | PreparedStatement preparedStatement = connection.prepareStatement(query.toString(), 68 | yormTable.hasPrimaryKey() ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS)) { 69 | int paramIndex = 1; 70 | populatePreparedStatement(tuples, paramIndex, preparedStatement, obj); 71 | preparedStatement.executeUpdate(); 72 | if (yormTable.hasPrimaryKey()) { 73 | ResultSet rs = preparedStatement.getGeneratedKeys(); 74 | if (rs.next()) { 75 | id = rs.getInt(1); 76 | } 77 | } 78 | } catch (SQLException | IllegalAccessException | InvocationTargetException e) { 79 | throw new YormException("Error while force inserting record:" + obj + " into table:" + yormTable.dbTable(), e); 80 | } 81 | return id; 82 | } 83 | 84 | public static long insert(DataSource ds, Record obj, YormTable yormTable) throws YormException { 85 | String op = "?,"; 86 | long id = 0; 87 | Predicate predicateFilterOutPrimaryKeys = FilterPredicates.filterOutPrimaryKeys(); 88 | List tuples = yormTable.tuples().stream().filter(predicateFilterOutPrimaryKeys).toList(); 89 | StringBuilder query = new StringBuilder("INSERT INTO "); 90 | query.append(yormTable.dbTable()) 91 | .append(" (") 92 | .append(String.join(",", tuples.stream().map(YormTuple::dbFieldName).toList())) 93 | .append(") VALUES (") 94 | .append(op.repeat(tuples.size())); 95 | query.deleteCharAt(query.length() - 1); 96 | query.append(")"); 97 | try (Connection connection = ds.getConnection(); 98 | PreparedStatement preparedStatement = connection.prepareStatement(query.toString(), 99 | yormTable.hasPrimaryKey() ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS)) { 100 | int paramIndex = 1; 101 | populatePreparedStatement(tuples, paramIndex, preparedStatement, obj); 102 | preparedStatement.executeUpdate(); 103 | ResultSet rs = preparedStatement.getGeneratedKeys(); 104 | if (rs.next()) { 105 | id = rs.getInt(1); 106 | } 107 | } catch (SQLException | InvocationTargetException | IllegalAccessException e) { 108 | throw new YormException("Error while inserting record:" + obj + " into table:" + yormTable.dbTable(), e); 109 | } 110 | return id; 111 | } 112 | 113 | public static void update(DataSource ds, Record obj, YormTable yormTable) throws YormException { 114 | String op = " = ?,"; 115 | String op2 = "=? AND "; 116 | List tuples = yormTable.tuples(); 117 | Predicate predicateFilterKeepKeys = FilterPredicates.filterKeepKeys(); 118 | List keyTuples = tuples.stream().filter(predicateFilterKeepKeys).toList(); 119 | StringBuilder query = new StringBuilder("UPDATE "); 120 | query.append(yormTable.dbTable()) 121 | .append(" SET ") 122 | .append(String.join(op, tuples.stream().map(YormTuple::dbFieldName).toList())) 123 | .append("=?"); 124 | query.append(" WHERE ") 125 | .append(String.join(op2, keyTuples.stream().map(YormTuple::dbFieldName).toList())) 126 | .append("=?"); 127 | try (Connection connection = ds.getConnection(); 128 | PreparedStatement preparedStatement = connection.prepareStatement(query.toString())) { 129 | int paramIndex = 1; 130 | populatePreparedStatement(tuples, paramIndex, preparedStatement, obj); 131 | populatePreparedStatement(keyTuples, tuples.size() + 1, preparedStatement, obj); 132 | preparedStatement.executeUpdate(); 133 | } catch (SQLException | IllegalAccessException | InvocationTargetException e) { 134 | throw new YormException("Error while updating record:" + obj + " in table:" + yormTable.dbTable(), e); 135 | } 136 | } 137 | 138 | private static int populatePreparedStatement(List tuples, int paramIndex, PreparedStatement preparedStatement, Record obj) 139 | throws SQLException, IllegalAccessException, InvocationTargetException, YormException { 140 | for (YormTuple tuple : tuples) { 141 | Method method = tuple.method(); 142 | final Object value = method.invoke(obj); 143 | final DbType type = tuple.type(); 144 | rowRecordConverter.recordToRow(paramIndex, preparedStatement, tuple.dbFieldName(), value, type, tuple.serializer()); 145 | paramIndex++; 146 | } 147 | return paramIndex; 148 | } 149 | 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/operations/operators/ComparisonOperator.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db.operations.operators; 2 | 3 | public enum ComparisonOperator { 4 | EQUALS("="), 5 | NOT_EQUALS("="), 6 | LIKE("LIKE"), 7 | GREATER_THAN(">="), 8 | LESS_THAN("<="); 9 | 10 | private String operator; 11 | 12 | ComparisonOperator(String operator) { 13 | this.operator = operator; 14 | } 15 | 16 | public String getOperator() { 17 | return this.operator; 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/operations/operators/WhereOperator.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db.operations.operators; 2 | 3 | public enum WhereOperator { 4 | AND("AND"), 5 | OR("OR"), 6 | EMPTY(""); 7 | 8 | 9 | private String operator; 10 | 11 | WhereOperator(String operator) { 12 | this.operator = operator; 13 | } 14 | 15 | public String getOperator() { 16 | return this.operator; 17 | } 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/db/operations/select/Select.java: -------------------------------------------------------------------------------- 1 | package org.yorm.db.operations.select; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import javax.sql.DataSource; 6 | 7 | import org.yorm.IdentifiableFunction; 8 | import org.yorm.YormTable; 9 | import org.yorm.YormTuple; 10 | import org.yorm.db.FilteringFieldValue; 11 | import org.yorm.db.operations.QueryFind; 12 | import org.yorm.db.operations.operators.ComparisonOperator; 13 | import org.yorm.db.operations.operators.WhereOperator; 14 | import org.yorm.exception.YormException; 15 | 16 | public class Select { 17 | 18 | private final YormTable yormTable; 19 | private final List list = new ArrayList<>(); 20 | private final DataSource ds; 21 | 22 | public Select(DataSource ds, YormTable yormTable) { 23 | this.ds = ds; 24 | this.yormTable = yormTable; 25 | } 26 | 27 | public SelectComparison where(IdentifiableFunction getter) throws YormException { 28 | return where(getter, WhereOperator.EMPTY); 29 | } 30 | 31 | public SelectComparison and(IdentifiableFunction getter) throws YormException { 32 | return where(getter, WhereOperator.AND); 33 | } 34 | 35 | public SelectComparison or(IdentifiableFunction getter) throws YormException { 36 | return where(getter, WhereOperator.OR); 37 | } 38 | 39 | public List find() throws YormException { 40 | return QueryFind.findFiltering(ds, yormTable, list); 41 | } 42 | 43 | 44 | private SelectComparison where(IdentifiableFunction getter, WhereOperator whereOperator) 45 | throws YormException { 46 | String getterName = getter.getCallingFunctionName(); 47 | YormTuple currentTuple = yormTable.getTupleWithObjectFieldName(getterName); 48 | return new SelectComparison<>() { 49 | public Select equalTo(U value) { 50 | FilteringFieldValue filteringFieldValue = new FilteringFieldValue(currentTuple.dbFieldName(), currentTuple.type(), value, ComparisonOperator.EQUALS, whereOperator); 51 | list.add(filteringFieldValue); 52 | return Select.this; 53 | } 54 | 55 | public Select greaterThan(U value) { 56 | FilteringFieldValue filteringFieldValue = new FilteringFieldValue(currentTuple.dbFieldName(), currentTuple.type(), value, ComparisonOperator.GREATER_THAN, whereOperator); 57 | list.add(filteringFieldValue); 58 | return Select.this; 59 | } 60 | 61 | public Select lessThan(U value) { 62 | FilteringFieldValue filteringFieldValue = new FilteringFieldValue(currentTuple.dbFieldName(), currentTuple.type(), value, ComparisonOperator.LESS_THAN, whereOperator); 63 | list.add(filteringFieldValue); 64 | return Select.this; 65 | } 66 | 67 | public Select notEqualTo(U value) { 68 | FilteringFieldValue filteringFieldValue = new FilteringFieldValue(currentTuple.dbFieldName(), currentTuple.type(), value, ComparisonOperator.NOT_EQUALS, 69 | whereOperator); 70 | list.add(filteringFieldValue); 71 | return Select.this; 72 | } 73 | 74 | public Select like(U value) { 75 | String valueStr = "%" + value + "%"; 76 | FilteringFieldValue filteringFieldValue = new FilteringFieldValue(currentTuple.dbFieldName(), currentTuple.type(), valueStr, ComparisonOperator.LIKE, 77 | whereOperator); 78 | list.add(filteringFieldValue); 79 | return Select.this; 80 | } 81 | }; 82 | } 83 | 84 | public interface SelectComparison { 85 | 86 | Select equalTo(U value); 87 | 88 | Select notEqualTo(U value); 89 | 90 | Select like(U value); 91 | 92 | Select greaterThan(U value); 93 | 94 | Select lessThan(U value); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/exception/YormException.java: -------------------------------------------------------------------------------- 1 | package org.yorm.exception; 2 | 3 | public class YormException extends Exception { 4 | 5 | public YormException(String cause) { 6 | super(cause); 7 | } 8 | 9 | public YormException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/util/DbType.java: -------------------------------------------------------------------------------- 1 | package org.yorm.util; 2 | 3 | import org.yorm.exception.YormException; 4 | 5 | import java.math.BigDecimal; 6 | import java.sql.Time; 7 | import java.sql.Types; 8 | import java.time.Instant; 9 | import java.time.LocalDate; 10 | import java.time.LocalDateTime; 11 | import java.time.LocalTime; 12 | import java.util.Arrays; 13 | import java.util.Date; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | public enum DbType { 18 | //TODO: include all Types.whatever 19 | TINYINT(Types.TINYINT, boolean.class), 20 | BOOLEAN(Types.BOOLEAN, boolean.class), 21 | SMALLINT(Types.SMALLINT, int.class), 22 | INTEGER(Types.INTEGER, int.class), 23 | // TODO: Should this be an actual BigInteger? 24 | BIGINT(Types.BIGINT, long.class), 25 | FLOAT(Types.FLOAT, float.class), 26 | DOUBLE(Types.DOUBLE, double.class), 27 | REAL(Types.REAL, float.class), 28 | DECIMAL(Types.DECIMAL, BigDecimal.class), 29 | BIT(Types.BIT, boolean.class), 30 | DATE(Types.DATE, LocalDate.class), 31 | TIME(Types.TIME, LocalTime.class), 32 | TIMESTAMP(Types.TIMESTAMP, LocalDateTime.class), 33 | CHAR(Types.CHAR, char.class), 34 | VARCHAR(Types.VARCHAR, String.class), 35 | TEXT(Types.LONGVARCHAR, String.class); 36 | 37 | public final int sqlType; 38 | public final Class javaType; 39 | 40 | DbType(int sqlType, Class javaType) { 41 | this.sqlType = sqlType; 42 | this.javaType = javaType; 43 | } 44 | 45 | private static final Map MAP = new HashMap<>(); 46 | 47 | static { 48 | Arrays.stream(values()).forEach(t -> MAP.put(t.sqlType, t)); 49 | } 50 | 51 | public static DbType getType(String str) throws YormException { 52 | int sqlType = Integer.parseInt(str); 53 | DbType dbType = MAP.get(sqlType); 54 | if (dbType == null) { 55 | throw new YormException("There is no DbType for sqlType:" + sqlType); 56 | } 57 | return dbType; 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/util/Levenshtein.java: -------------------------------------------------------------------------------- 1 | package org.yorm.util; 2 | 3 | import java.util.Arrays; 4 | 5 | public class Levenshtein { 6 | 7 | private Levenshtein() { 8 | } 9 | 10 | public static int calculate(String x, String y) { 11 | int[][] dp = new int[x.length() + 1][y.length() + 1]; 12 | for (int i = 0; i <= x.length(); i++) { 13 | for (int j = 0; j <= y.length(); j++) { 14 | if (i == 0) { 15 | dp[i][j] = j; 16 | } else if (j == 0) { 17 | dp[i][j] = i; 18 | } else { 19 | dp[i][j] = min(dp[i - 1][j - 1] 20 | + costOfSubstitution(x.charAt(i - 1), y.charAt(j - 1)), 21 | dp[i - 1][j] + 1, 22 | dp[i][j - 1] + 1); 23 | } 24 | } 25 | } 26 | return dp[x.length()][y.length()]; 27 | } 28 | 29 | private static int costOfSubstitution(char a, char b) { 30 | return a == b ? 0 : 1; 31 | } 32 | 33 | private static int min(int... numbers) { 34 | return Arrays.stream(numbers) 35 | .min().orElse(Integer.MAX_VALUE); 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/yorm/util/RowRecordConverter.java: -------------------------------------------------------------------------------- 1 | package org.yorm.util; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodType; 6 | import java.math.BigDecimal; 7 | import java.sql.Date; 8 | import java.sql.PreparedStatement; 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | import java.sql.Time; 12 | import java.sql.Timestamp; 13 | import java.time.LocalDate; 14 | import java.time.LocalDateTime; 15 | import java.time.LocalTime; 16 | import java.util.Map; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.yorm.exception.YormException; 20 | 21 | public class RowRecordConverter { 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(RowRecordConverter.class); 24 | 25 | private static final Map, Converter> PASS_THROUGH_CONVERTERS = Map.ofEntries( 26 | Map.entry(boolean.class, input -> input), 27 | Map.entry(byte.class, input -> input), 28 | Map.entry(char.class, input -> input), 29 | Map.entry(short.class, input -> input), 30 | Map.entry(int.class, input -> input), 31 | Map.entry(long.class, input -> input), 32 | Map.entry(float.class, input -> input), 33 | Map.entry(double.class, input -> input), 34 | Map.entry(Boolean.class, input -> input), 35 | Map.entry(Byte.class, input -> (byte) input), 36 | Map.entry(Character.class, input -> (char) input), 37 | Map.entry(Short.class, input -> (short) input), 38 | Map.entry(Integer.class, input -> (int) input), 39 | Map.entry(Long.class, input -> (long) input), 40 | Map.entry(Float.class, input -> (float) input), 41 | Map.entry(Double.class, input -> (double) input), 42 | Map.entry(String.class, input -> (String) input) 43 | ); 44 | 45 | private static final Map, Converter> ENUM_SERIALIZERS = Map.ofEntries( 46 | Map.entry(String.class, input -> ((Enum) input).toString()), 47 | Map.entry(char.class, input -> (char) ((Enum) input).ordinal()), 48 | Map.entry(int.class, input -> ((Enum) input).ordinal()), 49 | Map.entry(long.class, input -> (long) ((Enum) input).ordinal()), 50 | Map.entry(Character.class, input -> (char) ((Enum) input).ordinal()), 51 | Map.entry(Integer.class, input -> ((Enum) input).ordinal()), 52 | Map.entry(Long.class, input -> (long) ((Enum) input).ordinal()) 53 | ); 54 | 55 | private static final Converter TO_STRING_CONVERTER = input -> input.toString(); 56 | 57 | 58 | public static Converter converterFor(Class inputTypeClass, Class outputTypeClass) { 59 | if (logger.isDebugEnabled()) { 60 | logger.debug("Building a deserializer for {} to {}.", inputTypeClass.getName(), outputTypeClass.getName()); 61 | } 62 | if (outputTypeClass.isAssignableFrom(inputTypeClass)) { 63 | if (outputTypeClass.isPrimitive()) { 64 | //noinspection unchecked 65 | return (Converter) PASS_THROUGH_CONVERTERS.get(inputTypeClass); 66 | } else { 67 | return outputTypeClass::cast; 68 | } 69 | } 70 | if (outputTypeClass.isEnum() && inputTypeClass == String.class) { 71 | return enumFromString(outputTypeClass); 72 | } 73 | if (outputTypeClass.isEnum() && inputTypeClass == char.class) { 74 | return enumFromOrdinal(outputTypeClass); 75 | } 76 | if (inputTypeClass.isEnum()) { 77 | //noinspection unchecked 78 | return (Converter) ENUM_SERIALIZERS.get(outputTypeClass); 79 | } 80 | if (outputTypeClass == String.class) { 81 | //noinspection unchecked 82 | return (Converter) TO_STRING_CONVERTER; 83 | } 84 | // YUCK! 85 | if (inputTypeClass.equals(Boolean.class) && outputTypeClass.equals(boolean.class)) { 86 | //noinspection unchecked 87 | return (Converter) PASS_THROUGH_CONVERTERS.get(Boolean.class); 88 | } 89 | if (inputTypeClass.equals(boolean.class) && outputTypeClass.equals(Boolean.class)) { 90 | //noinspection unchecked 91 | return (Converter) PASS_THROUGH_CONVERTERS.get(boolean.class); 92 | } 93 | if (inputTypeClass.equals(Byte.class) && outputTypeClass.equals(byte.class)) { 94 | //noinspection unchecked 95 | return (Converter) PASS_THROUGH_CONVERTERS.get(Byte.class); 96 | } 97 | if (inputTypeClass.equals(byte.class) && outputTypeClass.equals(Byte.class)) { 98 | //noinspection unchecked 99 | return (Converter) PASS_THROUGH_CONVERTERS.get(byte.class); 100 | } 101 | if (inputTypeClass.equals(Character.class) && outputTypeClass.equals(char.class)) { 102 | //noinspection unchecked 103 | return (Converter) PASS_THROUGH_CONVERTERS.get(Character.class); 104 | } 105 | if (inputTypeClass.equals(char.class) && outputTypeClass.equals(Character.class)) { 106 | //noinspection unchecked 107 | return (Converter) PASS_THROUGH_CONVERTERS.get(char.class); 108 | } 109 | if (inputTypeClass == String.class && outputTypeClass == char.class) { 110 | return (Converter) TO_STRING_CONVERTER; 111 | } 112 | if (inputTypeClass.equals(Short.class) && outputTypeClass.equals(short.class)) { 113 | //noinspection unchecked 114 | return (Converter) PASS_THROUGH_CONVERTERS.get(Short.class); 115 | } 116 | if (inputTypeClass.equals(short.class) && outputTypeClass.equals(Short.class)) { 117 | //noinspection unchecked 118 | return (Converter) PASS_THROUGH_CONVERTERS.get(short.class); 119 | } 120 | if (inputTypeClass.equals(Integer.class) && outputTypeClass.equals(int.class)) { 121 | //noinspection unchecked 122 | return (Converter) PASS_THROUGH_CONVERTERS.get(Integer.class); 123 | } 124 | if (inputTypeClass.equals(int.class) && outputTypeClass.equals(Integer.class)) { 125 | //noinspection unchecked 126 | return (Converter) PASS_THROUGH_CONVERTERS.get(int.class); 127 | } 128 | if (inputTypeClass.equals(Long.class) && outputTypeClass.equals(long.class)) { 129 | //noinspection unchecked 130 | return (Converter) PASS_THROUGH_CONVERTERS.get(Long.class); 131 | } 132 | if (inputTypeClass.equals(long.class) && outputTypeClass.equals(Long.class)) { 133 | //noinspection unchecked 134 | return (Converter) PASS_THROUGH_CONVERTERS.get(long.class); 135 | } 136 | if (inputTypeClass.equals(Float.class) && outputTypeClass.equals(float.class)) { 137 | //noinspection unchecked 138 | return (Converter) PASS_THROUGH_CONVERTERS.get(Float.class); 139 | } 140 | if (inputTypeClass.equals(float.class) && outputTypeClass.equals(Float.class)) { 141 | //noinspection unchecked 142 | return (Converter) PASS_THROUGH_CONVERTERS.get(float.class); 143 | } 144 | if (inputTypeClass.equals(Double.class) && outputTypeClass.equals(double.class)) { 145 | //noinspection unchecked 146 | return (Converter) PASS_THROUGH_CONVERTERS.get(Double.class); 147 | } 148 | if (inputTypeClass.equals(double.class) && outputTypeClass.equals(Double.class)) { 149 | //noinspection unchecked 150 | return (Converter) PASS_THROUGH_CONVERTERS.get(double.class); 151 | } 152 | throw new RuntimeException(String.format( 153 | "No deserializer found for %s to %s. You should file a bug to let us know what we need to add!", 154 | inputTypeClass.getName(), outputTypeClass.getName() 155 | )); 156 | } 157 | 158 | // TODO: Cache for each enum type? I could imagine times when we'd be calling this a lot for the same thing. 159 | private static Converter enumFromOrdinal(Class outputTypeClass) { 160 | try { 161 | MethodHandle methodHandle = MethodHandles 162 | .lookup() 163 | .findStatic( 164 | outputTypeClass, 165 | "values", 166 | MethodType.methodType( 167 | // Like typing MyEnum[].class. 168 | java.lang.reflect.Array.newInstance(outputTypeClass, 0).getClass() 169 | ) 170 | ); 171 | return input -> { 172 | try { 173 | // noinspection unchecked 174 | return (OutputType) ((OutputType[]) methodHandle.invokeExact())[(int) input]; 175 | } catch (IllegalArgumentException e) { 176 | // Rethrow real errors... 177 | throw e; 178 | } catch (Throwable e) { 179 | // This should never happen, but let's give helpful output just in case weird stuff goes down! 180 | throw new IllegalStateException( 181 | String.format( 182 | "Unexpected exception invoking %s.value()[%d].", 183 | outputTypeClass.getName(), 184 | (int) input 185 | ), 186 | e 187 | ); 188 | } 189 | }; 190 | } catch (NoSuchMethodException | IllegalAccessException e) { 191 | // Again... This should never happen, but let's give helpful output just in case weird stuff goes down! 192 | throw new IllegalStateException( 193 | String.format("Unexpected exception finding %s.value()[] method.", outputTypeClass.getName()), 194 | e 195 | ); 196 | } 197 | } 198 | 199 | // TODO: Cache for each enum type? I could imagine times when we'd be calling this a lot for the same thing. 200 | private static Converter enumFromString(Class outputTypeClass) { 201 | try { 202 | MethodHandle methodHandle = MethodHandles 203 | .lookup() 204 | .findStatic( 205 | outputTypeClass, 206 | "valueOf", 207 | MethodType.methodType(outputTypeClass, String.class) 208 | ); 209 | return input -> { 210 | try { 211 | // noinspection unchecked 212 | return (OutputType) methodHandle.invoke((String) input); 213 | } catch (IllegalArgumentException e) { 214 | // Rethrow real errors... 215 | throw e; 216 | } catch (Throwable e) { 217 | // This should never happen, but let's give helpful output just in case weird stuff goes down! 218 | throw new IllegalStateException( 219 | String.format( 220 | "Unexpected exception invoking %s.valueOf(\"%s\").", 221 | outputTypeClass.getName(), 222 | (String) input 223 | ), 224 | e 225 | ); 226 | } 227 | }; 228 | } catch (NoSuchMethodException | IllegalAccessException e) { 229 | // Again... This should never happen, but let's give helpful output just in case weird stuff goes down! 230 | throw new IllegalStateException( 231 | String.format("Unexpected exception finding %s.valueOf() method.", outputTypeClass.getName()), 232 | e 233 | ); 234 | } 235 | } 236 | 237 | public void recordToRow( 238 | int paramIndex, 239 | PreparedStatement preparedStatement, 240 | String dbColumnName, 241 | Object value, 242 | DbType type, 243 | Converter converter 244 | ) throws SQLException, YormException { 245 | switch (type) { 246 | //MySql does not have a truly boolean type, bool/boolean are a synonym of tinyint(1) 247 | //Postgresql maps booleans to bits 248 | case TINYINT, BIT, BOOLEAN -> preparedStatement.setBoolean(paramIndex, (boolean) value); 249 | case SMALLINT, INTEGER -> { 250 | if (value instanceof Integer) { 251 | preparedStatement.setInt(paramIndex, (int) value); 252 | } else if (value instanceof Long) { 253 | preparedStatement.setLong(paramIndex, (long) value); 254 | } else { 255 | throw new YormException("Incompatible value:" + value + " of class:" + value.getClass().getName() + " for column type:" + type); 256 | } 257 | } 258 | case BIGINT -> preparedStatement.setLong(paramIndex, (long) converter.convert(value)); 259 | case VARCHAR, TEXT, CHAR -> preparedStatement.setString(paramIndex, (String) converter.convert(value)); 260 | case DOUBLE -> preparedStatement.setDouble(paramIndex, (double) converter.convert(value)); 261 | case FLOAT, REAL -> preparedStatement.setFloat(paramIndex, (float) converter.convert(value)); 262 | case DECIMAL -> preparedStatement.setBigDecimal(paramIndex, (BigDecimal) converter.convert(value)); 263 | case DATE -> preparedStatement.setDate(paramIndex, Date.valueOf((LocalDate) converter.convert(value))); 264 | case TIME -> preparedStatement.setTime(paramIndex, Time.valueOf((LocalTime) converter.convert(value))); 265 | case TIMESTAMP -> preparedStatement.setTimestamp(paramIndex, Timestamp.valueOf((LocalDateTime) converter.convert(value))); 266 | default -> throw new YormException("Couldn't find type for " + dbColumnName); 267 | } 268 | } 269 | 270 | public int rowToRecord( 271 | ResultSet rs, 272 | Object[] values, 273 | int params, 274 | String dbColumnName, 275 | DbType type, 276 | Converter converter 277 | ) throws SQLException, YormException { 278 | switch (type) { 279 | case TINYINT, BIT, BOOLEAN -> { 280 | //MySql does not have a truly boolean type, bool/boolean are a synonym of tinyint(1) 281 | boolean tiny = rs.getBoolean(dbColumnName); 282 | values[params++] = converter.convert(tiny); 283 | } 284 | case SMALLINT, INTEGER -> { 285 | int ii = rs.getInt(dbColumnName); 286 | values[params++] = converter.convert(ii); 287 | } 288 | case BIGINT -> { 289 | long ll = rs.getLong(dbColumnName); 290 | values[params++] = converter.convert(ll); 291 | } 292 | case VARCHAR, CHAR, TEXT -> { 293 | String str = rs.getString(dbColumnName); 294 | values[params++] = converter.convert(str); 295 | } 296 | case DOUBLE -> { 297 | double dd = rs.getDouble(dbColumnName); 298 | values[params++] = converter.convert(dd); 299 | } 300 | case FLOAT, REAL -> { 301 | float ff = rs.getFloat(dbColumnName); 302 | values[params++] = converter.convert(ff); 303 | } 304 | case DECIMAL -> { 305 | BigDecimal bb = rs.getBigDecimal(dbColumnName); 306 | values[params++] = converter.convert(bb); 307 | } 308 | case DATE -> { 309 | Date date = rs.getDate(dbColumnName); 310 | values[params++] = converter.convert(date.toLocalDate()); 311 | } 312 | case TIME -> { 313 | Time time = rs.getTime(dbColumnName); 314 | values[params++] = converter.convert(time.toLocalTime()); 315 | } 316 | case TIMESTAMP -> { 317 | Timestamp ts = rs.getTimestamp(dbColumnName); 318 | values[params++] = converter.convert(ts.toLocalDateTime()); 319 | } 320 | default -> throw new YormException("Couldn't find type for " + dbColumnName); 321 | } 322 | return params; 323 | } 324 | 325 | public interface Converter { 326 | 327 | OutputType convert(Object input); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/PerformanceTest.java: -------------------------------------------------------------------------------- 1 | package org.yorm; 2 | 3 | import static junit.framework.TestCase.assertTrue; 4 | 5 | import java.time.Duration; 6 | import java.time.Instant; 7 | import java.time.LocalDate; 8 | import java.time.LocalDateTime; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import javax.sql.DataSource; 12 | import org.junit.jupiter.api.BeforeAll; 13 | import org.junit.jupiter.api.Disabled; 14 | import org.junit.jupiter.api.Test; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.yorm.exception.YormException; 18 | import org.yorm.records.Company; 19 | import org.yorm.records.CompanyType; 20 | import org.yorm.records.Person; 21 | import org.yorm.utils.TestConnectionFactory; 22 | import org.yorm.utils.TestDbHelper; 23 | 24 | @Disabled 25 | class PerformanceTest { 26 | 27 | private static Logger logger = LoggerFactory.getLogger(MapBuilder.class); 28 | private static DataSource ds; 29 | private static Yorm yorm; 30 | 31 | @BeforeAll 32 | static void initDb() { 33 | ds = TestConnectionFactory.getMySqlConnection(); 34 | yorm = new Yorm(ds); 35 | } 36 | 37 | @Test 38 | void testPerformance() throws YormException { 39 | int companyId = 1; 40 | Company company = new Company(companyId, "randomString", "ZZ", LocalDate.now(), 0, true, CompanyType.GREEDY, true); 41 | yorm.insert(company); 42 | List metricsInsertingYorm = new ArrayList<>(); 43 | List metricsFindingYorm = new ArrayList<>(); 44 | List metricsFindingYormFluent = new ArrayList<>(); 45 | List metricsDeletingYorm = new ArrayList<>(); 46 | int cycles = 15; 47 | int operations = 55; 48 | for (int op = 0; op < cycles; op++) { 49 | Instant clockStart = Instant.now(); 50 | massiveInsertWithYorm(operations); 51 | Instant clockFinishes = Instant.now(); 52 | metricsInsertingYorm.add(Duration.between(clockStart, clockFinishes).toMillis()); 53 | clockStart = Instant.now(); 54 | massiveFindWithYorm(operations, company); 55 | clockFinishes = Instant.now(); 56 | metricsFindingYorm.add(Duration.between(clockStart, clockFinishes).toMillis()); 57 | clockStart = Instant.now(); 58 | massiveFindWithYormFluentApi(operations, companyId); 59 | clockFinishes = Instant.now(); 60 | metricsFindingYormFluent.add(Duration.between(clockStart, clockFinishes).toMillis()); 61 | clockStart = Instant.now(); 62 | massiveDeletionWithYorm(operations); 63 | clockFinishes = Instant.now(); 64 | metricsDeletingYorm.add(Duration.between(clockStart, clockFinishes).toMillis()); 65 | } 66 | List metricsInsertingJdbc = new ArrayList<>(); 67 | List metricsFindingJdbc = new ArrayList<>(); 68 | List metricsDeletingJdbc = new ArrayList<>(); 69 | for (int op = 0; op < cycles; op++) { 70 | Instant clockStart = Instant.now(); 71 | massiveInsertWithJdbc(operations); 72 | Instant clockFinishes = Instant.now(); 73 | metricsInsertingJdbc.add(Duration.between(clockStart, clockFinishes).toMillis()); 74 | clockStart = Instant.now(); 75 | massiveFindWithJdbc(operations, company); 76 | clockFinishes = Instant.now(); 77 | metricsFindingJdbc.add(Duration.between(clockStart, clockFinishes).toMillis()); 78 | clockStart = Instant.now(); 79 | massiveDeletionWithJdbc(operations); 80 | clockFinishes = Instant.now(); 81 | metricsDeletingJdbc.add(Duration.between(clockStart, clockFinishes).toMillis()); 82 | } 83 | long averageInsertYorm = getAverage(metricsInsertingYorm); 84 | long averageInsertJdbc = getAverage(metricsInsertingJdbc); 85 | long averageDeleteYorm = getAverage(metricsDeletingYorm); 86 | long averageDeleteJdbc = getAverage(metricsDeletingJdbc); 87 | long averageFindingYorm = getAverage(metricsFindingYorm); 88 | long averageFindingYormFluent = getAverage(metricsFindingYormFluent); 89 | long averageFindingJdbc = getAverage(metricsFindingJdbc); 90 | logger.info("Average Inserting: yorm: {} ms - Jdbc: {} ms", averageInsertYorm, averageInsertJdbc); 91 | logger.info("Average Finding: yorm {} ms - Jdbc: {} ms", averageFindingYorm, averageFindingJdbc); 92 | logger.info("Average Finding: yorm fluent api {} ms - Jdbc: {} ms", averageFindingYormFluent, averageFindingJdbc); 93 | logger.info("Average Deleting: yorm {} ms - Jdbc: {} ms", averageDeleteYorm, averageDeleteJdbc); 94 | long maxInserting = 35; 95 | long maxFinding = 68; 96 | long maxDeleting = 20; 97 | assertTrue(getPercentage(averageInsertYorm, averageInsertJdbc) < maxInserting); 98 | assertTrue(getPercentage(averageFindingYorm, averageFindingJdbc) < maxFinding); 99 | assertTrue(getPercentage(averageDeleteYorm, averageDeleteJdbc) < maxDeleting); 100 | 101 | 102 | } 103 | 104 | long getAverage(List list) { 105 | return list.stream().reduce(Long::sum).get() / list.size(); 106 | } 107 | 108 | long getPercentage(long value1, long value2) { 109 | return 100 - value2 * 100 / value1; 110 | } 111 | 112 | public void massiveInsertWithYorm(int operations) throws YormException { 113 | String str = "randomString"; 114 | for (int k = 1; k < operations; k++) { 115 | yorm.insert(new Person(k, str, str, LocalDateTime.now(), 1)); 116 | } 117 | } 118 | 119 | public void massiveDeletionWithYorm(int operations) throws YormException { 120 | for (int k = 1; k < operations; k++) { 121 | yorm.delete(Person.class, k); 122 | } 123 | } 124 | 125 | public void massiveFindWithYorm(int operations, Company company) throws YormException { 126 | for (int k = 1; k < operations; k++) { 127 | yorm.find(Person.class, company); 128 | } 129 | } 130 | 131 | public void massiveFindWithYormFluentApi(int operations, int companyId) throws YormException { 132 | for (int k = 1; k < operations; k++) { 133 | yorm.from(Person.class).where(Person::companyId).equalTo(companyId).find(); 134 | } 135 | } 136 | 137 | public void massiveInsertWithJdbc(int operations) throws YormException { 138 | String str = "randomString"; 139 | for (int k = 1; k < operations; k++) { 140 | TestDbHelper.insertPerson(ds, k, str, str, LocalDateTime.now(), 1); 141 | } 142 | } 143 | 144 | public void massiveDeletionWithJdbc(int operations) throws YormException { 145 | for (int k = 1; k < operations; k++) { 146 | TestDbHelper.deletePerson(ds, k); 147 | } 148 | } 149 | 150 | public void massiveFindWithJdbc(int operations, Company company) throws YormException { 151 | for (int k = 1; k < operations; k++) { 152 | TestDbHelper.get(ds, company.id()); 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/YormMySqlTest.java: -------------------------------------------------------------------------------- 1 | package org.yorm; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 6 | import static org.junit.jupiter.api.Assertions.assertNotNull; 7 | import static org.junit.jupiter.api.Assertions.assertNull; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import java.time.LocalDate; 12 | import java.time.LocalDateTime; 13 | import java.time.LocalTime; 14 | import java.util.List; 15 | import javax.sql.DataSource; 16 | import org.junit.jupiter.api.BeforeAll; 17 | import org.junit.jupiter.api.MethodOrderer; 18 | import org.junit.jupiter.api.Order; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.TestMethodOrder; 21 | import org.slf4j.simple.SimpleLogger; 22 | import org.yorm.exception.YormException; 23 | import org.yorm.records.*; 24 | import org.yorm.util.DbType; 25 | import org.yorm.utils.TestConnectionFactory; 26 | 27 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 28 | class YormMySqlTest { 29 | 30 | private static DataSource ds; 31 | private static Yorm yorm; 32 | 33 | @BeforeAll 34 | static void initDb() { 35 | System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "DEBUG"); 36 | ds = TestConnectionFactory.getMySqlConnection(); 37 | yorm = new Yorm(ds); 38 | } 39 | 40 | @Test 41 | @Order(1) 42 | void buildMap() throws YormException { 43 | MapBuilder mp = new MapBuilder(ds); 44 | YormTable map = mp.buildMap(Person.class); 45 | assertNotNull(map); 46 | List tuples = map.tuples(); 47 | assertEquals(5, tuples.size()); 48 | YormTuple tuple0 = tuples.get(0); 49 | assertEquals("id", tuple0.dbFieldName()); 50 | assertEquals(DbType.INTEGER, tuple0.type()); 51 | assertEquals("id", tuple0.method().getName()); 52 | assertEquals("id", tuple0.objectFieldName()); 53 | assertTrue(tuple0.isPrimaryKey()); 54 | YormTuple tuple3 = tuples.get(3); 55 | assertEquals("last_login", tuple3.dbFieldName()); 56 | assertEquals(DbType.TIMESTAMP, tuple3.type()); 57 | assertEquals("lastLogin", tuple3.method().getName()); 58 | assertEquals("lastLogin", tuple3.objectFieldName()); 59 | assertEquals("last_login", tuple3.dbFieldName()); 60 | assertFalse(tuple3.isPrimaryKey()); 61 | YormTuple tuple4 = tuples.get(4); 62 | assertEquals("company_id", tuple4.dbFieldName()); 63 | assertEquals(DbType.INTEGER, tuple0.type()); 64 | assertEquals("companyId", tuple4.method().getName()); 65 | assertEquals("companyId", tuple4.objectFieldName()); 66 | assertEquals("company_id", tuple4.dbFieldName()); 67 | assertFalse(tuple4.isPrimaryKey()); 68 | } 69 | 70 | @Test 71 | @Order(2) 72 | void saveCompany() throws YormException { 73 | Company company = new Company(0, "Hogwarts", "GB", LocalDate.of(1968, 2, 12), 154.1f, true, CompanyType.NOT_GREEDY, false); 74 | long id = yorm.save(company); 75 | assertEquals(1, id); 76 | Company company2 = new Company(0, "Mordor", "ZZ", LocalDate.of(114, 11, 5), 0f, false, CompanyType.GREEDY, true); 77 | long id2 = yorm.save(company2); 78 | assertEquals(2, id2); 79 | } 80 | 81 | @Test 82 | @Order(3) 83 | void getCompany() throws YormException { 84 | Company hogwarts = yorm.find(Company.class, 1); 85 | assertEquals("GB", hogwarts.countryCode()); 86 | assertEquals("Hogwarts", hogwarts.name()); 87 | assertEquals(LocalDate.of(1968, 2, 12), hogwarts.date()); 88 | assertEquals(154.1f, hogwarts.debt()); 89 | assertTrue(hogwarts.isActive()); 90 | assertEquals(CompanyType.NOT_GREEDY, hogwarts.companyType()); 91 | assertFalse(hogwarts.isEvil()); 92 | 93 | Company mordor = yorm.find(Company.class, 2); 94 | assertEquals("ZZ", mordor.countryCode()); 95 | assertEquals("Mordor", mordor.name()); 96 | assertEquals(LocalDate.of(114, 11, 5), mordor.date()); 97 | assertEquals(0f, mordor.debt()); 98 | assertFalse(mordor.isActive()); 99 | assertEquals(CompanyType.GREEDY, mordor.companyType()); 100 | assertTrue(mordor.isEvil()); 101 | } 102 | 103 | @Test 104 | @Order(4) 105 | void savePerson() throws YormException { 106 | Person person = new Person(0, "John", "john.doe@um.com", LocalDateTime.of(2022, 3, 22, 11, 14, 13), 1); 107 | long idPerson = yorm.save(person); 108 | assertEquals(1, idPerson); 109 | Person personWrong = new Person(0, "John", "john.doe@um.com", LocalDateTime.of(2022, 1, 15, 7, 53, 21), 0); 110 | assertThrows(YormException.class, () -> yorm.save(personWrong)); 111 | } 112 | 113 | @Test 114 | @Order(5) 115 | void saveListPersons() throws YormException { 116 | LocalDateTime localDateTime = LocalDateTime.now(); 117 | Person person1 = new Person(2, "Hermione", "hermione.granger@hogwarts.com", localDateTime, 1); 118 | Person person2 = new Person(3, "Harry", "harry.potter@hogwarts.com", localDateTime, 1); 119 | Person person3 = new Person(4, "Sauron", "sauron@mordor.com", localDateTime, 2); 120 | List list = List.of(person1, person2, person3); 121 | yorm.insert(list); 122 | List personList = yorm.find(Person.class); 123 | assertNotNull(personList); 124 | assertEquals(4, personList.size()); 125 | } 126 | 127 | @Test 128 | @Order(6) 129 | void getPerson() throws YormException { 130 | Person person = yorm.find(Person.class, 1); 131 | assertEquals("John", person.name()); 132 | assertEquals("john.doe@um.com", person.email()); 133 | assertEquals(1, person.companyId()); 134 | Person person2 = yorm.find(Person.class, 2); 135 | assertEquals("Hermione", person2.name()); 136 | assertEquals("hermione.granger@hogwarts.com", person2.email()); 137 | assertEquals(1, person2.companyId()); 138 | } 139 | 140 | @Test 141 | @Order(7) 142 | void getEverything() throws YormException { 143 | List personList = yorm.find(Person.class); 144 | assertNotNull(personList); 145 | assertEquals(4, personList.size()); 146 | Person person2 = personList.get(1); 147 | assertEquals("Hermione", person2.name()); 148 | assertEquals("hermione.granger@hogwarts.com", person2.email()); 149 | assertEquals(1, person2.companyId()); 150 | } 151 | 152 | @Test 153 | @Order(8) 154 | void getWithForeignKey() throws YormException { 155 | Company company = new Company(1, null, null, null, 0, false, CompanyType.GREEDY, true); 156 | List personList = yorm.find(Person.class, company); 157 | assertNotNull(personList); 158 | assertEquals(3, personList.size()); 159 | Person person2 = personList.get(1); 160 | assertEquals("Hermione", person2.name()); 161 | assertEquals("hermione.granger@hogwarts.com", person2.email()); 162 | assertEquals(1, person2.companyId()); 163 | List personList2 = yorm.find(Person.class, new Company(2, null, null, null, 0, false, CompanyType.NOT_GREEDY, false)); 164 | assertNotNull(personList2); 165 | assertEquals(1, personList2.size()); 166 | Person person1 = personList2.get(0); 167 | assertEquals("Sauron", person1.name()); 168 | assertEquals("sauron@mordor.com", person1.email()); 169 | assertEquals(2, person1.companyId()); 170 | } 171 | 172 | @Test 173 | @Order(9) 174 | void getFiltering() throws YormException { 175 | Person personFilter1 = new Person(0, "harry", "john", null, 0); 176 | Person personFilter2 = new Person(0, null, null, null, 2); 177 | List list = List.of(personFilter1, personFilter2); 178 | List personList = yorm.find(list); 179 | assertNotNull(personList); 180 | assertEquals(3, personList.size()); 181 | Person person1 = personList.stream().filter(p -> p.id() == 4).findFirst().get(); 182 | assertEquals(4, person1.id()); 183 | assertEquals("Sauron", person1.name()); 184 | assertEquals("sauron@mordor.com", person1.email()); 185 | assertEquals(2, person1.companyId()); 186 | List list2 = yorm.from(Person.class).where(Person::email).like(".com").find(); 187 | assertFalse(list2.isEmpty()); 188 | } 189 | 190 | @Test 191 | @Order(10) 192 | void getFilteringNoResults() throws YormException { 193 | Person personFilter1 = new Person(0, "Peter", null, null, 0); 194 | List personList = yorm.find(List.of(personFilter1)); 195 | assertTrue(personList.isEmpty()); 196 | } 197 | 198 | @Test 199 | @Order(11) 200 | void update() throws YormException { 201 | LocalDateTime localDateTime = LocalDateTime.of(2022, 1, 12, 14, 32, 14); 202 | Person personResultBefore = yorm.find(Person.class, 1); 203 | assertEquals(1, personResultBefore.id()); 204 | assertEquals("John", personResultBefore.name()); 205 | assertEquals("john.doe@um.com", personResultBefore.email()); 206 | assertEquals(1, personResultBefore.companyId()); 207 | Person person = new Person(1, "Draco", "draco.malfoy@hogwarts.com", localDateTime, 1); 208 | yorm.save(person);//Update because the id is 1 209 | Person personResult = yorm.find(Person.class, 1); 210 | assertEquals(1, personResult.id()); 211 | assertEquals("Draco", personResult.name()); 212 | assertEquals("draco.malfoy@hogwarts.com", personResult.email()); 213 | assertEquals(1, personResult.companyId()); 214 | Person person2 = new Person(1, "Lucius", "lucius.malfoy@hogwarts.com", localDateTime, 1); 215 | yorm.update(person2); 216 | Person personResult2 = yorm.find(Person.class, 1); 217 | assertEquals(1, personResult2.id()); 218 | assertEquals("Lucius", personResult2.name()); 219 | assertEquals("lucius.malfoy@hogwarts.com", personResult2.email()); 220 | } 221 | 222 | @Test 223 | @Order(12) 224 | void delete() throws YormException { 225 | var somethingWasDeleted = yorm.delete(Person.class, 1); 226 | assertTrue(somethingWasDeleted); 227 | Person personResult = yorm.find(Person.class, 1); 228 | assertNull(personResult); 229 | somethingWasDeleted = yorm.delete(Person.class, 1); 230 | assertFalse(somethingWasDeleted); 231 | } 232 | 233 | @Test 234 | @Order(13) 235 | void mapWrongRecord() { 236 | Invoice invoice = new Invoice(1, 1); 237 | assertThrows(YormException.class, () -> yorm.insert(invoice)); 238 | } 239 | 240 | @Test 241 | @Order(14) 242 | void testFluentWhere() throws YormException { 243 | List list = yorm.from(Person.class).where(Person::email).like(".com").find(); 244 | assertFalse(list.isEmpty()); 245 | List secondList = yorm.from(Person.class).where(Person::companyId).equalTo(100) 246 | .or(Person::email).like("hogwarts") 247 | .find(); 248 | assertEquals(2, secondList.size()); 249 | List thirdList = yorm.from(Person.class).where(Person::name).equalTo("Hermione") 250 | .and(Person::lastLogin).greaterThan(LocalDateTime.of(2019, 01, 01, 0, 0, 0)) 251 | .find(); 252 | assertEquals(1, thirdList.size()); 253 | List companyList = yorm.from(Company.class).where(Company::isActive).notEqualTo(false).find(); 254 | assertEquals(1, companyList.size()); 255 | } 256 | 257 | @Test 258 | @Order(15) 259 | void testTimeType() throws YormException { 260 | HistoryAnnotation refAnnotation = new HistoryAnnotation("Test subject", 12.4f, LocalTime.NOON, "This is a jaunty text\n", "Small side note"); 261 | List annotationHistories = List.of(refAnnotation); 262 | yorm.insert(annotationHistories); 263 | List retrievedList = yorm.find(HistoryAnnotation.class); 264 | assertEquals(1, retrievedList.size()); 265 | HistoryAnnotation retrievedAnnotation = retrievedList.get(0); 266 | assertEquals(retrievedAnnotation, refAnnotation); 267 | } 268 | 269 | @Test 270 | @Order(16) 271 | void testTextType() throws YormException { 272 | HistoryAnnotation refAnnotation = new HistoryAnnotation("Funny subject", 12.4f, LocalTime.MIDNIGHT, "Completely random generated text", "Small side note"); 273 | yorm.save(refAnnotation); 274 | List retrievedList = yorm.from(HistoryAnnotation.class).where(HistoryAnnotation::content).like("random").find(); 275 | assertEquals(1, retrievedList.size()); 276 | HistoryAnnotation retrievedAnnotation = retrievedList.get(0); 277 | assertEquals(retrievedAnnotation, refAnnotation); 278 | } 279 | 280 | @Test 281 | @Order(17) 282 | void testView() throws YormException { 283 | List list = yorm.from(PersonCompany.class).where(PersonCompany::isActive).equalTo(true).find(); 284 | assertNotEquals(0, list.size()); 285 | assertTrue(list.stream().filter(pc->!pc.isActive()).findAny().isEmpty()); 286 | } 287 | 288 | } 289 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/YormPostgreSqlTest.java: -------------------------------------------------------------------------------- 1 | package org.yorm; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 6 | import static org.junit.jupiter.api.Assertions.assertNotNull; 7 | import static org.junit.jupiter.api.Assertions.assertNull; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import java.time.LocalTime; 12 | import org.junit.jupiter.api.BeforeAll; 13 | import org.junit.jupiter.api.MethodOrderer; 14 | import org.junit.jupiter.api.Order; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.TestMethodOrder; 17 | import org.slf4j.simple.SimpleLogger; 18 | import org.yorm.exception.YormException; 19 | import org.yorm.records.Company; 20 | import org.yorm.records.CompanyType; 21 | import org.yorm.records.HistoryAnnotation; 22 | import org.yorm.records.Invoice; 23 | import org.yorm.records.Person; 24 | import org.yorm.records.PersonCompany; 25 | import org.yorm.util.DbType; 26 | import org.yorm.utils.TestConnectionFactory; 27 | 28 | import javax.sql.DataSource; 29 | import java.time.LocalDate; 30 | import java.time.LocalDateTime; 31 | import java.util.List; 32 | 33 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 34 | class YormPostgreSqlTest { 35 | 36 | private static DataSource ds; 37 | private static Yorm yorm; 38 | 39 | @BeforeAll 40 | static void initDb() { 41 | System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "DEBUG"); 42 | ds = TestConnectionFactory.getPostgreSqlConnection(); 43 | yorm = new Yorm(ds); 44 | } 45 | 46 | @Test 47 | @Order(1) 48 | void buildMap() throws YormException { 49 | MapBuilder mp = new MapBuilder(ds); 50 | YormTable map = mp.buildMap(Person.class); 51 | assertNotNull(map); 52 | List tuples = map.tuples(); 53 | assertEquals(5, tuples.size()); 54 | 55 | YormTuple tuple0 = tuples.get(0); 56 | assertEquals("id", tuple0.dbFieldName()); 57 | assertEquals(DbType.INTEGER, tuple0.type()); 58 | assertEquals("id", tuple0.method().getName()); 59 | assertEquals("id", tuple0.objectFieldName()); 60 | assertTrue(tuple0.isPrimaryKey()); 61 | 62 | YormTuple tuple3 = tuples.get(3); 63 | assertEquals("last_login", tuple3.dbFieldName()); 64 | assertEquals(DbType.TIMESTAMP, tuple3.type()); 65 | assertEquals("lastLogin", tuple3.method().getName()); 66 | assertEquals("lastLogin", tuple3.objectFieldName()); 67 | assertEquals("last_login", tuple3.dbFieldName()); 68 | assertFalse(tuple3.isPrimaryKey()); 69 | 70 | YormTuple tuple4 = tuples.get(4); 71 | assertEquals("company_id", tuple4.dbFieldName()); 72 | assertEquals(DbType.INTEGER, tuple0.type()); 73 | assertEquals("companyId", tuple4.method().getName()); 74 | assertEquals("companyId", tuple4.objectFieldName()); 75 | assertEquals("company_id", tuple4.dbFieldName()); 76 | assertFalse(tuple4.isPrimaryKey()); 77 | } 78 | 79 | @Test 80 | @Order(2) 81 | void saveCompany() throws YormException { 82 | Company company = new Company(0, "Hogwarts", "GB", LocalDate.of(1968, 2, 12), 154.1f, true, CompanyType.NOT_GREEDY, false); 83 | long id = yorm.save(company); 84 | assertEquals(1, id); 85 | Company company2 = new Company(0, "Mordor", "ZZ", LocalDate.of(114, 11, 5), 0f, false, CompanyType.GREEDY, true); 86 | long id2 = yorm.save(company2); 87 | assertEquals(2, id2); 88 | } 89 | 90 | @Test 91 | @Order(3) 92 | void getCompany() throws YormException { 93 | Company company = yorm.find(Company.class, 1); 94 | assertEquals("GB", company.countryCode()); 95 | assertEquals("Hogwarts", company.name()); 96 | assertEquals(LocalDate.of(1968, 2, 12), company.date()); 97 | assertEquals(154.1f, company.debt()); 98 | assertTrue(company.isActive()); 99 | 100 | } 101 | 102 | @Test 103 | @Order(4) 104 | void savePerson() throws YormException { 105 | Person person = new Person(0, "John", "john.doe@um.com", LocalDateTime.of(2022, 3, 22, 11, 14, 13), 1); 106 | long idPerson = yorm.save(person); 107 | assertEquals(1, idPerson); 108 | Person personWrong = new Person(0, "John", "john.doe@um.com", LocalDateTime.of(2022, 1, 15, 7, 53, 21), 0); 109 | assertThrows(YormException.class, () -> yorm.save(personWrong)); 110 | } 111 | 112 | @Test 113 | @Order(5) 114 | void saveListPersons() throws YormException { 115 | LocalDateTime localDateTime = LocalDateTime.now(); 116 | Person person1 = new Person(2, "Hermione", "hermione.granger@hogwarts.com", localDateTime, 1); 117 | Person person2 = new Person(3, "Harry", "harry.potter@hogwarts.com", localDateTime, 1); 118 | Person person3 = new Person(4, "Sauron", "sauron@mordor.com", localDateTime, 2); 119 | List list = List.of(person1, person2, person3); 120 | yorm.insert(list); 121 | List personList = yorm.find(Person.class); 122 | assertNotNull(personList); 123 | assertEquals(4, personList.size()); 124 | } 125 | 126 | @Test 127 | @Order(6) 128 | void getPerson() throws YormException { 129 | Person person = yorm.find(Person.class, 1); 130 | assertEquals("John", person.name()); 131 | assertEquals("john.doe@um.com", person.email()); 132 | assertEquals(1, person.companyId()); 133 | Person person2 = yorm.find(Person.class, 2); 134 | assertEquals("Hermione", person2.name()); 135 | assertEquals("hermione.granger@hogwarts.com", person2.email()); 136 | assertEquals(1, person2.companyId()); 137 | } 138 | 139 | @Test 140 | @Order(7) 141 | void getEverything() throws YormException { 142 | List personList = yorm.find(Person.class); 143 | assertNotNull(personList); 144 | assertEquals(4, personList.size()); 145 | Person person2 = personList.get(1); 146 | assertEquals("Hermione", person2.name()); 147 | assertEquals("hermione.granger@hogwarts.com", person2.email()); 148 | assertEquals(1, person2.companyId()); 149 | } 150 | 151 | @Test 152 | @Order(8) 153 | void getWithForeignKey() throws YormException { 154 | Company company = new Company(1, null, null, null, 0, false, CompanyType.NOT_GREEDY, true); 155 | List personList = yorm.find(Person.class, company); 156 | assertNotNull(personList); 157 | assertEquals(3, personList.size()); 158 | Person person2 = personList.get(1); 159 | assertEquals("Hermione", person2.name()); 160 | assertEquals("hermione.granger@hogwarts.com", person2.email()); 161 | assertEquals(1, person2.companyId()); 162 | List personList2 = yorm.find(Person.class, new Company(2, null, null, null, 0, false, CompanyType.GREEDY, false)); 163 | assertNotNull(personList2); 164 | assertEquals(1, personList2.size()); 165 | Person person1 = personList2.get(0); 166 | assertEquals("Sauron", person1.name()); 167 | assertEquals("sauron@mordor.com", person1.email()); 168 | assertEquals(2, person1.companyId()); 169 | } 170 | 171 | @Test 172 | @Order(9) 173 | void getFiltering() throws YormException { 174 | Person personFilter1 = new Person(0, "Harry", "john", null, 0); 175 | Person personFilter2 = new Person(0, null, null, null, 2); 176 | List list = List.of(personFilter1, personFilter2); 177 | List personList = yorm.find(list); 178 | assertNotNull(personList); 179 | assertEquals(3, personList.size()); 180 | Person person1 = personList.stream().filter(p -> p.id() == 4).findFirst().get(); 181 | assertEquals(4, person1.id()); 182 | assertEquals("Sauron", person1.name()); 183 | assertEquals("sauron@mordor.com", person1.email()); 184 | assertEquals(2, person1.companyId()); 185 | } 186 | 187 | @Test 188 | @Order(10) 189 | void getFilteringNoResults() throws YormException { 190 | Person personFilter1 = new Person(0, "Peter", null, null, 0); 191 | List personList = yorm.find(List.of(personFilter1)); 192 | assertTrue(personList.isEmpty()); 193 | } 194 | 195 | @Test 196 | @Order(11) 197 | void update() throws YormException { 198 | LocalDateTime localDateTime = LocalDateTime.of(2022, 1, 12, 14, 32, 14); 199 | Person personResultBefore = yorm.find(Person.class, 1); 200 | assertEquals(1, personResultBefore.id()); 201 | assertEquals("John", personResultBefore.name()); 202 | assertEquals("john.doe@um.com", personResultBefore.email()); 203 | assertEquals(1, personResultBefore.companyId()); 204 | Person person = new Person(1, "Draco", "draco.malfoy@hogwarts.com", localDateTime, 1); 205 | yorm.save(person);//Update because the id is 1 206 | Person personResult = yorm.find(Person.class, 1); 207 | assertEquals(1, personResult.id()); 208 | assertEquals("Draco", personResult.name()); 209 | assertEquals("draco.malfoy@hogwarts.com", personResult.email()); 210 | assertEquals(1, personResult.companyId()); 211 | Person person2 = new Person(1, "Lucius", "lucius.malfoy@hogwarts.com", localDateTime, 1); 212 | yorm.update(person2); 213 | Person personResult2 = yorm.find(Person.class, 1); 214 | assertEquals(1, personResult2.id()); 215 | assertEquals("Lucius", personResult2.name()); 216 | assertEquals("lucius.malfoy@hogwarts.com", personResult2.email()); 217 | } 218 | 219 | @Test 220 | @Order(12) 221 | void delete() throws YormException { 222 | var somethingWasDeleted = yorm.delete(Person.class, 1); 223 | assertTrue(somethingWasDeleted); 224 | Person personResult = yorm.find(Person.class, 1); 225 | assertNull(personResult); 226 | somethingWasDeleted = yorm.delete(Person.class, 1); 227 | assertFalse(somethingWasDeleted); 228 | } 229 | 230 | @Test 231 | @Order(13) 232 | void mapWrongRecord() { 233 | Invoice invoice = new Invoice(1, 1); 234 | assertThrows(YormException.class, () -> yorm.insert(invoice)); 235 | } 236 | 237 | @Test 238 | @Order(14) 239 | void testFluentWhere() throws YormException { 240 | List list = yorm.from(Person.class).where(Person::email).like(".com").find(); 241 | assertFalse(list.isEmpty()); 242 | List secondList = yorm.from(Person.class).where(Person::companyId).equalTo(100) 243 | .or(Person::email).like("hogwarts") 244 | .find(); 245 | assertEquals(2, secondList.size()); 246 | List thirdList = yorm.from(Person.class).where(Person::name).equalTo("Hermione") 247 | .and(Person::lastLogin).greaterThan(LocalDateTime.of(2019, 01, 01, 0, 0, 0)) 248 | .find(); 249 | assertEquals(1, thirdList.size()); 250 | List companyList = yorm.from(Company.class).where(Company::isActive).notEqualTo(false).find(); 251 | assertEquals(1, companyList.size()); 252 | } 253 | 254 | @Test 255 | @Order(15) 256 | void testTimeType() throws YormException { 257 | HistoryAnnotation refAnnotation = new HistoryAnnotation("Test subject", 12.4f, LocalTime.NOON, "This is a jaunty text\n", "Small side note"); 258 | List annotationHistories = List.of(refAnnotation); 259 | yorm.insert(annotationHistories); 260 | List retrievedList = yorm.find(HistoryAnnotation.class); 261 | assertEquals(1, retrievedList.size()); 262 | HistoryAnnotation retrievedAnnotation = retrievedList.get(0); 263 | assertEquals(retrievedAnnotation, refAnnotation); 264 | } 265 | 266 | @Test 267 | @Order(16) 268 | void testTextType() throws YormException { 269 | HistoryAnnotation refAnnotation = new HistoryAnnotation("Funny subject", 12.4f, LocalTime.MIDNIGHT, "Completely random generated text", "Small side note"); 270 | yorm.save(refAnnotation); 271 | List retrievedList = yorm.from(HistoryAnnotation.class).where(HistoryAnnotation::content).like("random").find(); 272 | assertEquals(1, retrievedList.size()); 273 | HistoryAnnotation retrievedAnnotation = retrievedList.get(0); 274 | assertEquals(retrievedAnnotation, refAnnotation); 275 | } 276 | 277 | @Test 278 | @Order(17) 279 | void testView() throws YormException { 280 | List list = yorm.from(PersonCompany.class).where(PersonCompany::isActive).equalTo(true).find(); 281 | assertNotEquals(0, list.size()); 282 | assertTrue(list.stream().filter(pc->!pc.isActive()).findAny().isEmpty()); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/records/Company.java: -------------------------------------------------------------------------------- 1 | package org.yorm.records; 2 | 3 | import java.time.LocalDate; 4 | 5 | public record Company( 6 | int id, 7 | String name, 8 | String countryCode, 9 | LocalDate date, 10 | float debt, 11 | boolean isActive, 12 | CompanyType companyType, 13 | Boolean isEvil 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/records/CompanyType.java: -------------------------------------------------------------------------------- 1 | package org.yorm.records; 2 | 3 | public enum CompanyType { 4 | GREEDY, 5 | NOT_GREEDY 6 | } 7 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/records/HistoryAnnotation.java: -------------------------------------------------------------------------------- 1 | package org.yorm.records; 2 | 3 | import java.time.LocalTime; 4 | 5 | public record HistoryAnnotation(String subject, float amount, LocalTime annotationTime, String content, String sideNote) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/records/Invoice.java: -------------------------------------------------------------------------------- 1 | package org.yorm.records; 2 | 3 | public record Invoice(int id, int company_id) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/records/Person.java: -------------------------------------------------------------------------------- 1 | package org.yorm.records; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public record Person(int id, String name, String email, LocalDateTime lastLogin, int companyId) { 6 | 7 | public Person(String name) { 8 | this(0, name, null, null, 0); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/records/PersonCompany.java: -------------------------------------------------------------------------------- 1 | package org.yorm.records; 2 | 3 | public record PersonCompany(String name, boolean isActive, String email) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/utils/TestConnectionFactory.java: -------------------------------------------------------------------------------- 1 | package org.yorm.utils; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import javax.sql.DataSource; 6 | 7 | public class TestConnectionFactory { 8 | 9 | private static DataSource mySqlDs; 10 | 11 | private static DataSource postgreSqlDs; 12 | 13 | private TestConnectionFactory() { 14 | } 15 | 16 | public static DataSource getMySqlConnection() { 17 | if (mySqlDs == null) { 18 | HikariConfig config = new HikariConfig(); 19 | config.setJdbcUrl("jdbc:tc:mysql:5.7.34:///databasename/yorm?TC_INITSCRIPT=file:src/test/resources/init_sample_db_my_sql.sql"); 20 | config.setUsername("root"); 21 | config.setPassword("test"); 22 | config.addDataSourceProperty("cachePrepStmts", "true"); 23 | config.addDataSourceProperty("prepStmtCacheSize", "250"); 24 | config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); 25 | mySqlDs = new HikariDataSource(config); 26 | } 27 | return mySqlDs; 28 | } 29 | 30 | public static DataSource getPostgreSqlConnection() { 31 | if(postgreSqlDs == null){ 32 | HikariConfig config = new HikariConfig(); 33 | config.setJdbcUrl("jdbc:tc:postgresql:9.6.8:///databasename/yorm?TC_INITSCRIPT=file:src/test/resources/init_sample_db_postgre_sql.sql"); 34 | config.setUsername("root"); 35 | config.setPassword("test"); 36 | config.addDataSourceProperty("cachePrepStmts", "true"); 37 | config.addDataSourceProperty("prepStmtCacheSize", "250"); 38 | config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); 39 | postgreSqlDs = new HikariDataSource(config); 40 | } 41 | return postgreSqlDs; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/org/yorm/utils/TestDbHelper.java: -------------------------------------------------------------------------------- 1 | package org.yorm.utils; 2 | 3 | import java.sql.Connection; 4 | import java.sql.PreparedStatement; 5 | import java.sql.ResultSet; 6 | import java.sql.SQLException; 7 | import java.sql.Statement; 8 | import java.sql.Timestamp; 9 | import java.time.LocalDateTime; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import javax.sql.DataSource; 13 | import org.yorm.exception.YormException; 14 | import org.yorm.records.Person; 15 | 16 | public class TestDbHelper { 17 | 18 | public static void insertPerson(DataSource ds, long id, String str, String str2, LocalDateTime localDateTime, long id2) throws YormException { 19 | String query = "INSERT INTO person (id, name, email, last_login, company_id) VALUES(?,?,?,?,?)"; 20 | try (Connection connection = ds.getConnection(); 21 | PreparedStatement preparedStatement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { 22 | preparedStatement.setLong(1, id); 23 | preparedStatement.setString(2, str); 24 | preparedStatement.setString(3, str2); 25 | preparedStatement.setTimestamp(4, Timestamp.valueOf(localDateTime)); 26 | preparedStatement.setLong(5, id2); 27 | preparedStatement.executeUpdate(); 28 | ResultSet rs = preparedStatement.getGeneratedKeys(); 29 | if (rs.next()) { 30 | id = rs.getInt(1); 31 | } 32 | } catch (SQLException e) { 33 | throw new YormException(e.getMessage()); 34 | } 35 | } 36 | 37 | public static void deletePerson(DataSource ds, long id) throws YormException { 38 | String query = "DELETE FROM person WHERE id=?"; 39 | try (Connection connection = ds.getConnection(); 40 | PreparedStatement preparedStatement = connection.prepareStatement(query)) { 41 | preparedStatement.setLong(1, id); 42 | preparedStatement.executeUpdate(); 43 | } catch (SQLException e) { 44 | throw new YormException(e.getMessage()); 45 | } 46 | } 47 | 48 | public static List get(DataSource ds, long id) throws YormException { 49 | String query = "SELECT id, name, email, last_login, company_id FROM person WHERE company_id=?"; 50 | List list = new ArrayList<>(); 51 | try (Connection connection = ds.getConnection(); 52 | PreparedStatement preparedStatement = connection.prepareStatement(query)) { 53 | preparedStatement.setLong(1, id); 54 | ResultSet rs = preparedStatement.executeQuery(); 55 | if (rs.next()) { 56 | list.add(new Person(rs.getInt(1), rs.getString(2), rs.getString(3), 57 | rs.getTimestamp(4).toLocalDateTime(), rs.getInt(5))); 58 | } 59 | } catch (SQLException e) { 60 | throw new YormException(e.getMessage()); 61 | } 62 | return list; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/resources/init_sample_db_my_sql.sql: -------------------------------------------------------------------------------- 1 | SET 2 | SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 3 | SET 4 | AUTOCOMMIT = 0; 5 | START TRANSACTION; 6 | SET 7 | time_zone = "+00:00"; 8 | 9 | -- 10 | -- Database: `yorm` 11 | -- 12 | 13 | DROP 14 | DATABASE IF EXISTS yorm; 15 | CREATE 16 | DATABASE yorm; 17 | 18 | USE 19 | yorm; 20 | 21 | DROP TABLE IF EXISTS person; 22 | CREATE TABLE person 23 | ( 24 | id INT(10) AUTO_INCREMENT PRIMARY KEY NOT NULL, 25 | name VARCHAR(20) NOT NULL, 26 | email VARCHAR(55) NOT NULL, 27 | last_login DATETIME NOT NULL, 28 | company_id INT(10) NOT NULL 29 | ); 30 | 31 | DROP TABLE IF EXISTS company; 32 | CREATE TABLE company 33 | ( 34 | id INT(10) AUTO_INCREMENT PRIMARY KEY NOT NULL, 35 | name VARCHAR(20) NOT NULL, 36 | country_code VARCHAR(2) NOT NULL, 37 | creation_date DATE NOT NULL, 38 | debt FLOAT DEFAULT 0 NULL, 39 | is_active TINYINT NOT NULL, 40 | company_type ENUM('GREEDY', 'NOT_GREEDY') NOT NULL, 41 | is_evil BOOL NOT NULL 42 | ); 43 | 44 | ALTER TABLE `person` 45 | ADD CONSTRAINT `person_id` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`) ON DELETE CASCADE; 46 | 47 | DROP TABLE IF EXISTS invoice; 48 | CREATE TABLE invoice 49 | ( 50 | id INT(10) AUTO_INCREMENT PRIMARY KEY NOT NULL, 51 | subject VARCHAR(20) NOT NULL, 52 | amount FLOAT DEFAULT 0 NULL, 53 | company_id INT(10) NOT NULL 54 | ); 55 | 56 | ALTER TABLE `invoice` 57 | ADD CONSTRAINT `invoice_id` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`) ON DELETE CASCADE; 58 | 59 | DROP TABLE IF EXISTS history_annotation; 60 | CREATE TABLE history_annotation 61 | ( 62 | subject VARCHAR(20) NOT NULL, 63 | amount FLOAT DEFAULT 0 NULL, 64 | annotation_time TIME NULL, 65 | content TEXT, 66 | side_note CHAR(20) NOT NULL 67 | ); 68 | 69 | DROP VIEW IF EXISTS person_company; 70 | CREATE VIEW person_company AS 71 | SELECT p.name, p.email, c.debt, c.is_active 72 | FROM person p 73 | INNER JOIN company c ON p.company_id = c.id; 74 | -------------------------------------------------------------------------------- /src/test/resources/init_sample_db_postgre_sql.sql: -------------------------------------------------------------------------------- 1 | -- SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 2 | -- SET AUTOCOMMIT = 0; 3 | -- START TRANSACTION; 4 | -- SET time_zone = "+00:00"; 5 | 6 | -- 7 | -- Database: "yorm" 8 | -- 9 | 10 | -- DROP DATABASE IF EXISTS yorm; 11 | -- CREATE DATABASE yorm; 12 | 13 | -- USE yorm; 14 | 15 | DROP TABLE IF EXISTS person; 16 | CREATE TABLE person 17 | ( 18 | id SERIAL PRIMARY KEY, 19 | name VARCHAR(20) NOT NULL, 20 | email VARCHAR(55) NOT NULL, 21 | last_login TIMESTAMP NOT NULL, 22 | company_id int NOT NULL 23 | ); 24 | 25 | DROP TABLE IF EXISTS company; 26 | CREATE TABLE company 27 | ( 28 | id SERIAL PRIMARY KEY, 29 | name VARCHAR(20) NOT NULL, 30 | country_code VARCHAR(2) NOT NULL, 31 | creation_date DATE NOT NULL, 32 | debt REAL DEFAULT 0 NULL, 33 | is_active BOOLEAN NOT NULL, 34 | company_type VARCHAR(10) NOT NULL, 35 | is_evil BOOLEAN NOT NULL 36 | ); 37 | 38 | ALTER TABLE "person" 39 | ADD CONSTRAINT "person_id" FOREIGN KEY ("company_id") REFERENCES "company" ("id") ON DELETE CASCADE; 40 | 41 | DROP TABLE IF EXISTS invoice; 42 | CREATE TABLE invoice 43 | ( 44 | id SERIAL PRIMARY KEY, 45 | subject VARCHAR(20) NOT NULL, 46 | amount FLOAT DEFAULT 0 NULL, 47 | company_id int NOT NULL 48 | ); 49 | 50 | ALTER TABLE "invoice" 51 | ADD CONSTRAINT "invoice_id" FOREIGN KEY ("company_id") REFERENCES "company" ("id") ON DELETE CASCADE; 52 | 53 | DROP TABLE IF EXISTS history_annotation; 54 | CREATE TABLE history_annotation 55 | ( 56 | subject VARCHAR(20) NOT NULL, 57 | amount REAL DEFAULT 0 NULL, 58 | annotation_time TIME NULL, 59 | content TEXT, 60 | side_note CHAR(15) NOT NULL 61 | ); 62 | 63 | DROP VIEW IF EXISTS person_company; 64 | CREATE VIEW person_company AS 65 | SELECT p.name, p.email, c.debt, c.is_active 66 | FROM person p 67 | INNER JOIN company c ON p.company_id = c.id; 68 | -------------------------------------------------------------------------------- /src/test/resources/log4j2-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | --------------------------------------------------------------------------------