├── .gitignore ├── LICENSE.MD ├── README.md ├── images ├── crud.png └── query.png ├── pom.xml └── src └── main ├── java └── com │ └── htech │ ├── data │ └── jpa │ │ └── reactive │ │ ├── config │ │ ├── EnableReactiveJpaAuditing.java │ │ └── ReactiveJpaAuditingRegistrar.java │ │ ├── core │ │ ├── ReactiveJpaDataAutoConfiguration.java │ │ ├── StageReactiveJpaEntityOperations.java │ │ └── StageReactiveJpaEntityTemplate.java │ │ ├── mapping │ │ └── event │ │ │ ├── BeforeSaveCallback.java │ │ │ └── ReactiveAuditingEntityCallback.java │ │ └── repository │ │ ├── ReactiveCrudRepository.java │ │ ├── ReactiveJpaRepository.java │ │ ├── ReactiveJpaSpecificationExecutor.java │ │ ├── ReactivePagingAndSortingRepository.java │ │ ├── auto │ │ ├── ReactiveJpaRepositoriesAutoConfiguration.java │ │ └── ReactiveJpaRepositoriesRegistrar1.java │ │ ├── config │ │ ├── EnableReactiveJpaRepositories.java │ │ ├── ReactiveJpaRepositoriesRegistrar.java │ │ └── ReactiveJpaRepositoryConfigExtension.java │ │ ├── query │ │ ├── AbstractQueryCreator.java │ │ ├── AbstractReactiveJpaQuery.java │ │ ├── AbstractStringBasedReactiveJpaQuery.java │ │ ├── BasicParameterValueEvaluator.java │ │ ├── DeclaredQuery.java │ │ ├── DefaultQueryEnhancer.java │ │ ├── DefaultReactiveJpaQueryExtractor.java │ │ ├── EmptyDeclaredQuery.java │ │ ├── ExpressionBasedStringQuery.java │ │ ├── Jpa21Utils.java │ │ ├── NamedQuery.java │ │ ├── NativeReactiveJpaQuery.java │ │ ├── ParameterBinder.java │ │ ├── ParameterBinderFactory.java │ │ ├── ParameterBinding.java │ │ ├── ParameterMetadataProvider.java │ │ ├── ParameterValueEvaluator.java │ │ ├── PartTreeReactiveJpaQuery.java │ │ ├── PredicateBuilder.java │ │ ├── ProcedureParameter.java │ │ ├── QueryEnhancer.java │ │ ├── QueryEnhancerFactory.java │ │ ├── QueryParameterSetter.java │ │ ├── QueryParameterSetterFactory.java │ │ ├── QueryUtils.java │ │ ├── ReactiveJpaCountQueryCreator.java │ │ ├── ReactiveJpaCriteriaDeleteQueryCreator.java │ │ ├── ReactiveJpaCriteriaQueryCreator.java │ │ ├── ReactiveJpaParameters.java │ │ ├── ReactiveJpaParametersParameterAccessor.java │ │ ├── ReactiveJpaQueryEnhancer.java │ │ ├── ReactiveJpaQueryExecution.java │ │ ├── ReactiveJpaQueryExecutionConverters.java │ │ ├── ReactiveJpaQueryExtractor.java │ │ ├── ReactiveJpaQueryFactory.java │ │ ├── ReactiveJpaQueryLookupStrategy.java │ │ ├── ReactiveJpaQueryMethod.java │ │ ├── ReactiveJpaQueryMethodFactory.java │ │ ├── ReactiveQueryRewriterProvider.java │ │ ├── SimpleReactiveJpaQuery.java │ │ ├── SpELParameterValueEvaluator.java │ │ ├── StoredProcedureAttributeSource.java │ │ ├── StoredProcedureAttributes.java │ │ └── StringQuery.java │ │ └── support │ │ ├── CrudMethodMetadataContextHolder.java │ │ ├── CrudMethodMetadataPostProcessor.java │ │ ├── PersistenceExceptionHandlerPostProcessor.java │ │ ├── ReactiveJpaRepositoryFactory.java │ │ ├── ReactiveJpaRepositoryFactoryBean.java │ │ ├── ReactiveJpaRepositoryImplementation.java │ │ ├── SessionAwarePostProcessor.java │ │ └── SimpleReactiveJpaRepository.java │ └── jpa │ ├── pu │ └── CustomPersistenceUnitManager.java │ ├── reactive │ ├── EntityManagerFactoryBuilder.java │ ├── EntityManagerFactoryBuilderCustomizer.java │ ├── ReactiveHibernateJpaAutoConfiguration.java │ ├── ReactiveHibernateJpaConfiguration.java │ └── connection │ │ ├── ConnectionFactoryUtils.java │ │ ├── ConnectionHolder.java │ │ ├── ConstantPool.java │ │ ├── IsolationLevel.java │ │ ├── Option.java │ │ ├── ReactiveHibernateTransactionManager.java │ │ ├── SessionContextHolder.java │ │ ├── TransactionDefinition.java │ │ └── TransactionUtils.java │ └── support │ └── EntityManagerFactoryBeanCreationExceptionFailureAnalyzer.java └── resources └── META-INF ├── spring.factories └── spring └── org.springframework.boot.autoconfigure.AutoConfiguration.imports /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | 10 | 11 | # Compiled Java class files 12 | *.class 13 | 14 | # Compiled Python bytecode 15 | *.py[cod] 16 | 17 | # Log files 18 | *.log 19 | 20 | # Package files 21 | *.jar 22 | 23 | # Maven 24 | target/ 25 | 26 | # JetBrains IDE 27 | .idea/ 28 | 29 | # Unit test reports 30 | TEST*.xml 31 | 32 | # Generated by MacOS 33 | .DS_Store 34 | 35 | # Generated by Windows 36 | Thumbs.db 37 | 38 | # Applications 39 | *.app 40 | *.exe 41 | *.war 42 | 43 | # Large media files 44 | *.mp4 45 | *.tiff 46 | *.avi 47 | *.flv 48 | *.mov 49 | *.wmv 50 | 51 | /*.iml 52 | /src/main/resources/git.properties 53 | /.flattened-pom.xml 54 | /build/ 55 | .settings/ 56 | .vscode 57 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. -------------------------------------------------------------------------------- /images/crud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaconda875/reactive-hibernate-spring-boot-starter/26e51c23f1e970a9da3af09e0b846665dbb9c293/images/crud.png -------------------------------------------------------------------------------- /images/query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaconda875/reactive-hibernate-spring-boot-starter/26e51c23f1e970a9da3af09e0b846665dbb9c293/images/query.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.2.0 10 | 11 | 12 | io.github.anaconda875 13 | reactive-hibernate-spring-boot-starter 14 | 1.1.1 15 | Integrate Reactive Hibernate to Spring Data with Auto-config 16 | 17 | 18 | 19 | Apache License, Version 2.0 20 | http://www.apache.org/licenses/LICENSE-2.0.txt 21 | repo 22 | 23 | 24 | 25 | 26 | 27 | anaconda875 28 | Bao Ngo 29 | hflbtmax@gmail.com 30 | 31 | 32 | 33 | 34 | scm:git:git@github.com:anaconda875/reactive-hibernate-spring-boot-starter.git 35 | scm:git:git@github.com:anaconda875/reactive-hibernate-spring-boot-starter.git 36 | 37 | https://github.com/anaconda875/reactive-hibernate-spring-boot-starter 38 | 39 | 40 | 41 | 17 42 | 43 | 2.2.2.Final 44 | 4.5.3 45 | 6.4.4.Final 46 | 47 | 48 | 3.0.0-M5 49 | 3.0.0-M5 50 | 51 | false 52 | true 53 | 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter 59 | 60 | 61 | org.springframework 62 | spring-orm 63 | 64 | 65 | org.springframework.data 66 | spring-data-jpa 67 | 68 | 69 | io.projectreactor 70 | reactor-core 71 | 72 | 73 | 74 | org.hibernate.reactive 75 | hibernate-reactive-core 76 | ${hibernate-reactive.version} 77 | 78 | 79 | org.apache.commons 80 | commons-lang3 81 | 3.14.0 82 | 83 | 84 | org.apache.commons 85 | commons-collections4 86 | 4.5.0-M1 87 | 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-starter-test 92 | test 93 | 94 | 95 | 96 | 97 | 98 | ossrh 99 | 100 | 101 | 102 | org.sonatype.central 103 | central-publishing-maven-plugin 104 | 0.4.0 105 | true 106 | 107 | central 108 | true 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-gpg-plugin 114 | 1.6 115 | 116 | 117 | sign-artifacts 118 | verify 119 | 120 | sign 121 | 122 | 123 | 125 | 126 | --pinentry-mode 127 | loopback 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | com.spotify.fmt 143 | fmt-maven-plugin 144 | 2.21 145 | 146 | 147 | 148 | format 149 | 150 | 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-surefire-plugin 156 | ${maven-surefire-plugin.version} 157 | 158 | true 159 | 160 | 161 | 162 | test 163 | unit-test 164 | 167 | 168 | 169 | **/*IT.java 170 | 171 | 172 | 173 | 174 | 175 | 176 | org.apache.maven.plugins 177 | maven-failsafe-plugin 178 | ${maven-failsafe-plugin.version} 179 | 180 | true 181 | 182 | 183 | 184 | integration-test 185 | integration-test 186 | 190 | 191 | 192 | **/*IT.java 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-source-plugin 202 | 3.2.1 203 | 204 | 205 | attach-sources 206 | 207 | jar-no-fork 208 | 209 | 210 | 211 | 212 | 213 | org.apache.maven.plugins 214 | maven-javadoc-plugin 215 | 3.2.0 216 | 217 | 218 | attach-javadocs 219 | 220 | jar 221 | 222 | 223 | 224 | 225 | ${java.home}/bin/javadoc 226 | 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/config/EnableReactiveJpaAuditing.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.config; 2 | 3 | import java.lang.annotation.*; 4 | import org.springframework.context.annotation.Import; 5 | 6 | /** 7 | * @author Bao.Ngo 8 | */ 9 | @Inherited 10 | @Documented 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Import(ReactiveJpaAuditingRegistrar.class) 14 | public @interface EnableReactiveJpaAuditing { 15 | 16 | String auditorAwareRef() default ""; 17 | 18 | boolean setDates() default true; 19 | 20 | boolean modifyOnCreate() default true; 21 | 22 | String dateTimeProviderRef() default ""; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/config/ReactiveJpaAuditingRegistrar.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.config; 2 | 3 | import com.htech.data.jpa.reactive.mapping.event.ReactiveAuditingEntityCallback; 4 | import java.lang.annotation.Annotation; 5 | import org.springframework.beans.factory.config.BeanDefinition; 6 | import org.springframework.beans.factory.support.BeanDefinitionBuilder; 7 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 8 | import org.springframework.beans.factory.support.RootBeanDefinition; 9 | import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; 10 | import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; 11 | import org.springframework.data.auditing.config.AuditingConfiguration; 12 | import org.springframework.data.config.ParsingUtils; 13 | import org.springframework.data.jpa.repository.config.JpaMetamodelMappingContextFactoryBean; 14 | import org.springframework.util.Assert; 15 | 16 | /** 17 | * @author Bao.Ngo 18 | */ 19 | class ReactiveJpaAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { 20 | 21 | private static final String JPA_MAPPING_CONTEXT_BEAN_NAME = "jpaMappingContext"; 22 | 23 | @Override 24 | protected Class getAnnotation() { 25 | return EnableReactiveJpaAuditing.class; 26 | } 27 | 28 | @Override 29 | protected String getAuditingHandlerBeanName() { 30 | return "reactiveJpaAuditingHandler"; 31 | } 32 | 33 | @Override 34 | protected void postProcess( 35 | BeanDefinitionBuilder builder, 36 | AuditingConfiguration configuration, 37 | BeanDefinitionRegistry registry) { 38 | builder.setFactoryMethod("from").addConstructorArgReference(JPA_MAPPING_CONTEXT_BEAN_NAME); 39 | } 40 | 41 | @Override 42 | protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder( 43 | AuditingConfiguration configuration) { 44 | Assert.notNull(configuration, "AuditingConfiguration must not be null"); 45 | 46 | return configureDefaultAuditHandlerAttributes( 47 | configuration, 48 | BeanDefinitionBuilder.rootBeanDefinition(ReactiveIsNewAwareAuditingHandler.class)); 49 | } 50 | 51 | @Override 52 | protected void registerAuditListenerBeanDefinition( 53 | BeanDefinition auditingHandlerDefinition, BeanDefinitionRegistry registry) { 54 | Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null"); 55 | Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); 56 | 57 | if (!registry.containsBeanDefinition(JPA_MAPPING_CONTEXT_BEAN_NAME)) { 58 | registry.registerBeanDefinition( 59 | JPA_MAPPING_CONTEXT_BEAN_NAME, // 60 | new RootBeanDefinition(JpaMetamodelMappingContextFactoryBean.class)); 61 | } 62 | 63 | BeanDefinitionBuilder listenerBeanDefinitionBuilder = 64 | BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); 65 | listenerBeanDefinitionBuilder.addConstructorArgValue( 66 | ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); 67 | 68 | registerInfrastructureBeanWithId( 69 | listenerBeanDefinitionBuilder.getBeanDefinition(), 70 | ReactiveAuditingEntityCallback.class.getName(), 71 | registry); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/core/ReactiveJpaDataAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.core; 2 | 3 | import com.htech.jpa.reactive.ReactiveHibernateJpaAutoConfiguration; 4 | import org.hibernate.reactive.stage.Stage; 5 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * @author Bao.Ngo 13 | */ 14 | @Configuration(proxyBeanMethods = false) 15 | @ConditionalOnClass({Stage.SessionFactory.class}) 16 | @AutoConfigureAfter(ReactiveHibernateJpaAutoConfiguration.class) 17 | public class ReactiveJpaDataAutoConfiguration { 18 | 19 | @Bean 20 | @ConditionalOnMissingBean 21 | public StageReactiveJpaEntityOperations reactiveJpaEntityTemplate( 22 | Stage.SessionFactory sessionFactory) { 23 | return new StageReactiveJpaEntityTemplate(sessionFactory); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/core/StageReactiveJpaEntityOperations.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.core; 2 | 3 | import org.hibernate.reactive.stage.Stage; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.publisher.Mono; 6 | 7 | /** 8 | * @author Bao.Ngo 9 | */ 10 | public interface StageReactiveJpaEntityOperations { 11 | 12 | Mono persist(T entity); 13 | 14 | Flux persist(Iterable entity); 15 | 16 | Stage.SessionFactory sessionFactory(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/core/StageReactiveJpaEntityTemplate.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.core; 2 | 3 | import static reactor.core.scheduler.Schedulers.DEFAULT_POOL_SIZE; 4 | 5 | import com.htech.data.jpa.reactive.mapping.event.BeforeSaveCallback; 6 | import com.htech.jpa.reactive.connection.SessionContextHolder; 7 | import org.apache.commons.collections4.IterableUtils; 8 | import org.hibernate.reactive.stage.Stage; 9 | import org.springframework.beans.BeansException; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.ApplicationContextAware; 12 | import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; 13 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | /** 18 | * @author Bao.Ngo 19 | */ 20 | public class StageReactiveJpaEntityTemplate 21 | implements StageReactiveJpaEntityOperations, ApplicationContextAware { 22 | 23 | private static final ThreadPoolTaskExecutor EXECUTOR; 24 | 25 | private final Stage.SessionFactory sessionFactory; 26 | private ReactiveEntityCallbacks entityCallbacks; 27 | 28 | static { 29 | EXECUTOR = new ThreadPoolTaskExecutor(); 30 | EXECUTOR.setCorePoolSize(DEFAULT_POOL_SIZE); 31 | EXECUTOR.setMaxPoolSize(DEFAULT_POOL_SIZE * 2); 32 | EXECUTOR.setThreadNamePrefix("custom-parallel-"); 33 | EXECUTOR.initialize(); 34 | } 35 | 36 | public StageReactiveJpaEntityTemplate(Stage.SessionFactory sessionFactory) { 37 | this.sessionFactory = sessionFactory; 38 | } 39 | 40 | @Override 41 | public Mono persist(T entity) { 42 | return doInsert(entity); 43 | } 44 | 45 | @Override 46 | public Flux persist(Iterable entities) { 47 | return doInsert(entities); 48 | } 49 | 50 | private Flux doInsert(Iterable entities) { 51 | if (IterableUtils.isEmpty(entities)) { 52 | return Flux.empty(); 53 | } 54 | 55 | return SessionContextHolder.currentSession() 56 | .flatMap( 57 | session -> 58 | Flux.fromIterable(entities) 59 | .concatMap(this::maybeCallBeforeSave) 60 | .collectList() 61 | .flatMap( 62 | list -> 63 | Mono.defer( 64 | () -> 65 | Mono.fromCompletionStage(session.persist(list.toArray())) 66 | .then(deferFlushing(session)) 67 | .thenReturn(list)))) 68 | .flatMapMany(Flux::fromIterable); 69 | } 70 | 71 | private Mono doInsert(T entity) { 72 | return maybeCallBeforeSave(entity) 73 | .flatMap( 74 | e -> 75 | SessionContextHolder.currentSession() 76 | .flatMap( 77 | session -> 78 | Mono.defer( 79 | () -> 80 | Mono.fromCompletionStage(session.persist(e)) 81 | .then(deferFlushing(session)) 82 | .thenReturn(e)))); 83 | 84 | // Mono tMono = test.flatMap(v -> { 85 | // return this.maybeCallBeforeSave(entity); 86 | // }); 87 | // return tMono.as(e -> { 88 | // return toWrapper(e, Uni.class).chain(t -> session 89 | // .persist(t) 90 | // .chain(session::flush) 91 | // .replaceWith(t) 92 | // .emitOn(EXECUTOR) 93 | // .runSubscriptionOn(EXECUTOR)); 94 | // }) 95 | // ; 96 | } 97 | 98 | private Mono maybeCallBeforeSave(T entity) { 99 | if (entityCallbacks != null) { 100 | return entityCallbacks.callback(BeforeSaveCallback.class, entity); 101 | } 102 | 103 | return Mono.just(entity); 104 | } 105 | 106 | private static Mono deferFlushing(Stage.Session session) { 107 | return Mono.defer(() -> Mono.fromCompletionStage(session.flush())); 108 | } 109 | 110 | @Override 111 | public Stage.SessionFactory sessionFactory() { 112 | return sessionFactory; 113 | } 114 | 115 | @Override 116 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 117 | if (entityCallbacks == null) { 118 | setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext)); 119 | } 120 | } 121 | 122 | public void setEntityCallbacks(ReactiveEntityCallbacks entityCallbacks) { 123 | this.entityCallbacks = entityCallbacks; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/mapping/event/BeforeSaveCallback.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.mapping.event; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.springframework.data.mapping.callback.EntityCallback; 5 | 6 | /** 7 | * @author Bao.Ngo 8 | */ 9 | @FunctionalInterface 10 | public interface BeforeSaveCallback extends EntityCallback { 11 | 12 | Publisher onBeforeConvert(T entity); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/mapping/event/ReactiveAuditingEntityCallback.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.mapping.event; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.springframework.beans.factory.ObjectFactory; 5 | import org.springframework.core.Ordered; 6 | import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; 7 | import org.springframework.util.Assert; 8 | 9 | /** 10 | * @author Bao.Ngo 11 | */ 12 | public class ReactiveAuditingEntityCallback implements BeforeSaveCallback, Ordered { 13 | 14 | private final ObjectFactory auditingHandlerFactory; 15 | 16 | public ReactiveAuditingEntityCallback( 17 | ObjectFactory auditingHandlerFactory) { 18 | Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null"); 19 | this.auditingHandlerFactory = auditingHandlerFactory; 20 | } 21 | 22 | @Override 23 | public Publisher onBeforeConvert(Object entity) { 24 | return auditingHandlerFactory.getObject().markAudited(entity); 25 | } 26 | 27 | @Override 28 | public int getOrder() { 29 | return 100; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/ReactiveCrudRepository.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository; 2 | 3 | import org.springframework.data.jpa.repository.Modifying; 4 | import org.springframework.data.repository.NoRepositoryBean; 5 | import org.springframework.data.repository.Repository; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * @author Bao.Ngo 12 | */ 13 | @NoRepositoryBean 14 | public interface ReactiveCrudRepository extends Repository { 15 | Flux findAll(); 16 | 17 | // Uni findById(ID id); 18 | Mono findById(ID id); 19 | 20 | Mono getReferenceById(ID id); 21 | 22 | @Transactional 23 | @Modifying 24 | Mono save(S entity); 25 | 26 | @Transactional 27 | Flux saveAll(Iterable entities); 28 | 29 | Mono existsById(ID id); 30 | 31 | Flux findAllById(Iterable ids); 32 | 33 | Mono count(); 34 | 35 | @Transactional 36 | Mono deleteById(ID id); 37 | 38 | @Transactional 39 | Mono delete(T entity); 40 | 41 | @Transactional 42 | Mono deleteAllById(Iterable ids); 43 | 44 | @Transactional 45 | Mono deleteAll(Iterable entities); 46 | 47 | @Transactional 48 | Mono deleteAll(); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/ReactiveJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository; 2 | 3 | import org.springframework.data.repository.NoRepositoryBean; 4 | 5 | /** 6 | * @author Bao.Ngo 7 | */ 8 | @NoRepositoryBean 9 | public interface ReactiveJpaRepository 10 | extends ReactiveCrudRepository, ReactivePagingAndSortingRepository {} 11 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/ReactiveJpaSpecificationExecutor.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository; 2 | 3 | import java.util.List; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.domain.Sort; 7 | import org.springframework.data.jpa.domain.Specification; 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | 11 | public interface ReactiveJpaSpecificationExecutor { 12 | 13 | Mono findOne(Specification spec); 14 | 15 | Flux findAll(Specification spec); 16 | 17 | Flux findAll(Specification spec, Sort sort); 18 | 19 | Mono> findAllToList(Specification spec); 20 | 21 | Mono> findAll(Specification spec, Pageable pageable); 22 | 23 | Mono count(Specification spec); 24 | 25 | Mono exists(Specification spec); 26 | 27 | Mono delete(Specification spec); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/ReactivePagingAndSortingRepository.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.domain.Sort; 6 | import org.springframework.data.repository.NoRepositoryBean; 7 | import org.springframework.data.repository.Repository; 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | 11 | /** 12 | * @author Bao.Ngo 13 | */ 14 | @NoRepositoryBean 15 | public interface ReactivePagingAndSortingRepository extends Repository { 16 | 17 | Flux findAll(Sort sort); 18 | 19 | Mono> findAll(Pageable pageable); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/auto/ReactiveJpaRepositoriesAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.auto; 2 | 3 | import com.htech.jpa.reactive.ReactiveHibernateJpaAutoConfiguration; 4 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 5 | import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.context.annotation.ImportSelector; 9 | import org.springframework.core.type.AnnotationMetadata; 10 | 11 | /** 12 | * @author Bao.Ngo 13 | */ 14 | @Configuration(proxyBeanMethods = false) 15 | @AutoConfigureAfter({ 16 | ReactiveHibernateJpaAutoConfiguration.class, 17 | TaskExecutionAutoConfiguration.class 18 | }) 19 | @Import(ReactiveJpaRepositoriesAutoConfiguration.ReactiveJpaRepositoriesImportSelector.class) 20 | public class ReactiveJpaRepositoriesAutoConfiguration { 21 | 22 | static class ReactiveJpaRepositoriesImportSelector implements ImportSelector { 23 | 24 | @Override 25 | public String[] selectImports(AnnotationMetadata importingClassMetadata) { 26 | return new String[] {determineImport()}; 27 | } 28 | 29 | private String determineImport() { 30 | return ReactiveJpaRepositoriesRegistrar1.class.getName(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/auto/ReactiveJpaRepositoriesRegistrar1.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.auto; 2 | 3 | import com.htech.data.jpa.reactive.repository.config.EnableReactiveJpaRepositories; 4 | import com.htech.data.jpa.reactive.repository.config.ReactiveJpaRepositoryConfigExtension; 5 | import java.lang.annotation.Annotation; 6 | import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport; 7 | import org.springframework.data.repository.config.RepositoryConfigurationExtension; 8 | 9 | /** 10 | * @author Bao.Ngo 11 | */ 12 | class ReactiveJpaRepositoriesRegistrar1 extends AbstractRepositoryConfigurationSourceSupport { 13 | 14 | @Override 15 | protected Class getAnnotation() { 16 | return EnableReactiveJpaRepositories.class; 17 | } 18 | 19 | @Override 20 | protected Class getConfiguration() { 21 | return EnableReactiveRepositoriesConfiguration.class; 22 | } 23 | 24 | @Override 25 | protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { 26 | return new ReactiveJpaRepositoryConfigExtension(); 27 | } 28 | 29 | @EnableReactiveJpaRepositories 30 | private static class EnableReactiveRepositoriesConfiguration {} 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/config/EnableReactiveJpaRepositories.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.config; 2 | 3 | import com.htech.data.jpa.reactive.repository.support.ReactiveJpaRepositoryFactoryBean; 4 | import java.lang.annotation.*; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Import; 7 | import org.springframework.data.repository.config.BootstrapMode; 8 | import org.springframework.data.repository.config.DefaultRepositoryBaseClass; 9 | import org.springframework.data.repository.query.QueryLookupStrategy; 10 | 11 | /** 12 | * @author Bao.Ngo 13 | */ 14 | @Target(ElementType.TYPE) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @Documented 17 | @Inherited 18 | @Import(ReactiveJpaRepositoriesRegistrar.class) 19 | public @interface EnableReactiveJpaRepositories { 20 | 21 | String[] value() default {}; 22 | 23 | String[] basePackages() default {}; 24 | 25 | Class[] basePackageClasses() default {}; 26 | 27 | ComponentScan.Filter[] includeFilters() default {}; 28 | 29 | ComponentScan.Filter[] excludeFilters() default {}; 30 | 31 | String repositoryImplementationPostfix() default "Impl"; 32 | 33 | String namedQueriesLocation() default ""; 34 | 35 | QueryLookupStrategy.Key queryLookupStrategy() default QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND; 36 | 37 | Class repositoryFactoryBeanClass() default ReactiveJpaRepositoryFactoryBean.class; 38 | 39 | Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; 40 | 41 | // JPA specific configuration 42 | 43 | String sessionFactoryRef() default "sessionFactory"; 44 | 45 | String reactiveJpaEntityOperationsRef() default "reactiveJpaEntityTemplate"; 46 | 47 | boolean considerNestedRepositories() default false; 48 | 49 | boolean enableDefaultTransactions() default true; 50 | 51 | BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT; 52 | 53 | char escapeCharacter() default '\\'; 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/config/ReactiveJpaRepositoriesRegistrar.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.config; 2 | 3 | import java.lang.annotation.Annotation; 4 | import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; 5 | import org.springframework.data.repository.config.RepositoryConfigurationExtension; 6 | 7 | /** 8 | * @author Bao.Ngo 9 | */ 10 | class ReactiveJpaRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { 11 | 12 | @Override 13 | protected Class getAnnotation() { 14 | return EnableReactiveJpaRepositories.class; 15 | } 16 | 17 | @Override 18 | protected RepositoryConfigurationExtension getExtension() { 19 | return new ReactiveJpaRepositoryConfigExtension(); 20 | } 21 | 22 | // @Override 23 | // protected Class getConfiguration() { 24 | // return EnableReactiveRepositoriesConfiguration.class; 25 | // } 26 | 27 | // @Override 28 | // protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { 29 | // return new ReactiveJpaRepositoryConfigExtension(); 30 | // } 31 | 32 | @EnableReactiveJpaRepositories 33 | private static class EnableReactiveRepositoriesConfiguration {} 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/config/ReactiveJpaRepositoryConfigExtension.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.config; 2 | 3 | import com.htech.data.jpa.reactive.repository.support.ReactiveJpaRepositoryFactoryBean; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.MappedSuperclass; 6 | import java.lang.annotation.Annotation; 7 | import java.util.*; 8 | import org.springframework.beans.factory.support.BeanDefinitionBuilder; 9 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 10 | import org.springframework.beans.factory.support.RootBeanDefinition; 11 | import org.springframework.core.annotation.AnnotationAttributes; 12 | import org.springframework.data.jpa.repository.JpaRepository; 13 | import org.springframework.data.jpa.repository.config.JpaMetamodelMappingContextFactoryBean; 14 | import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension; 15 | import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; 16 | import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; 17 | import org.springframework.data.repository.config.RepositoryConfigurationSource; 18 | import org.springframework.data.repository.core.RepositoryMetadata; 19 | 20 | /** 21 | * @author Bao.Ngo 22 | */ 23 | public class ReactiveJpaRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { 24 | 25 | private static final String JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME = 26 | "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup"; 27 | private static final String JPA_MAPPING_CONTEXT_BEAN_NAME = "jpaMappingContext"; 28 | private static final String ESCAPE_CHARACTER_PROPERTY = "escapeCharacter"; 29 | 30 | @Override 31 | public String getModuleName() { 32 | return "REACTIVE_JPA"; 33 | } 34 | 35 | @Override 36 | public String getRepositoryFactoryBeanClassName() { 37 | return ReactiveJpaRepositoryFactoryBean.class.getName(); 38 | } 39 | 40 | @Override 41 | protected String getModulePrefix() { 42 | return getModuleName().toLowerCase(Locale.US); 43 | } 44 | 45 | @Override 46 | protected Collection> getIdentifyingAnnotations() { 47 | return Arrays.asList(Entity.class, MappedSuperclass.class); 48 | } 49 | 50 | @Override 51 | protected Collection> getIdentifyingTypes() { 52 | return Collections.>singleton(JpaRepository.class); 53 | } 54 | 55 | @Override 56 | public void postProcess( 57 | BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { 58 | AnnotationAttributes attributes = config.getAttributes(); 59 | 60 | String reactiveJpaEntityOperationsRef = attributes.getString("reactiveJpaEntityOperationsRef"); 61 | // if (StringUtils.hasText(reactiveJpaEntityOperationsRef)) { 62 | builder.addPropertyReference("entityOperations", reactiveJpaEntityOperationsRef); 63 | builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(config).orElse('\\')); 64 | // } else { 65 | // //TODO 66 | // } 67 | } 68 | 69 | private static Optional getEscapeCharacter(RepositoryConfigurationSource source) { 70 | try { 71 | return source.getAttribute(ESCAPE_CHARACTER_PROPERTY, Character.class); 72 | } catch (IllegalArgumentException ___) { 73 | return Optional.empty(); 74 | } 75 | } 76 | 77 | @Override 78 | public void registerBeansForRoot( 79 | BeanDefinitionRegistry registry, RepositoryConfigurationSource config) { 80 | 81 | super.registerBeansForRoot(registry, config); 82 | 83 | // registerSharedEntityMangerIfNotAlreadyRegistered(registry, config); 84 | 85 | Object source = config.getSource(); 86 | 87 | // registerLazyIfNotAlreadyRegistered( 88 | // () -> new RootBeanDefinition(EntityManagerBeanDefinitionRegistrarPostProcessor.class), 89 | // registry, 90 | // EM_BEAN_DEFINITION_REGISTRAR_POST_PROCESSOR_BEAN_NAME, source); 91 | 92 | registerLazyIfNotAlreadyRegistered( 93 | () -> new RootBeanDefinition(JpaMetamodelMappingContextFactoryBean.class), 94 | registry, 95 | JPA_MAPPING_CONTEXT_BEAN_NAME, 96 | source); 97 | 98 | // registerLazyIfNotAlreadyRegistered(() -> new RootBeanDefinition(PAB_POST_PROCESSOR), 99 | // registry, 100 | // AnnotationConfigUtils.PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME, source); 101 | 102 | // Register bean definition for DefaultJpaContext 103 | 104 | // registerLazyIfNotAlreadyRegistered(() -> { 105 | // 106 | // RootBeanDefinition contextDefinition = new RootBeanDefinition(DefaultJpaContext.class); 107 | // contextDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); 108 | // 109 | // return contextDefinition; 110 | // 111 | // }, registry, JPA_CONTEXT_BEAN_NAME, source); 112 | 113 | registerIfNotAlreadyRegistered( 114 | () -> new RootBeanDefinition(JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME), 115 | registry, 116 | JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME, 117 | source); 118 | 119 | // EvaluationContextExtension for JPA specific SpEL functions 120 | 121 | registerIfNotAlreadyRegistered( 122 | () -> { 123 | Object value = 124 | AnnotationRepositoryConfigurationSource.class.isInstance(config) // 125 | ? config.getRequiredAttribute(ESCAPE_CHARACTER_PROPERTY, Character.class) // 126 | : config.getAttribute(ESCAPE_CHARACTER_PROPERTY).orElse("\\"); 127 | 128 | BeanDefinitionBuilder builder = 129 | BeanDefinitionBuilder.rootBeanDefinition(JpaEvaluationContextExtension.class); 130 | builder.addConstructorArgValue(value); 131 | 132 | return builder.getBeanDefinition(); 133 | }, 134 | registry, 135 | JpaEvaluationContextExtension.class.getName(), 136 | source); 137 | } 138 | 139 | @Override 140 | protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { 141 | return metadata.isReactiveRepository(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/AbstractQueryCreator.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.Collections; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import org.springframework.data.domain.Sort; 8 | import org.springframework.data.repository.query.ParameterAccessor; 9 | import org.springframework.data.repository.query.parser.Part; 10 | import org.springframework.data.repository.query.parser.PartTree; 11 | import org.springframework.lang.Nullable; 12 | import org.springframework.util.Assert; 13 | 14 | /** 15 | * @author Bao.Ngo 16 | */ 17 | public abstract class AbstractQueryCreator { 18 | 19 | protected final Optional parameters; 20 | protected final PartTree tree; 21 | 22 | public AbstractQueryCreator(PartTree tree) { 23 | this(tree, Optional.empty()); 24 | } 25 | 26 | public AbstractQueryCreator(PartTree tree, ParameterAccessor parameters) { 27 | this(tree, Optional.of(parameters)); 28 | } 29 | 30 | private AbstractQueryCreator(PartTree tree, Optional parameters) { 31 | Assert.notNull(tree, "PartTree must not be null"); 32 | Assert.notNull(parameters, "ParameterAccessor must not be null"); 33 | 34 | this.tree = tree; 35 | this.parameters = parameters; 36 | } 37 | 38 | public abstract List> getParameterExpressions(); 39 | 40 | public T createQuery() { 41 | return createQuery( 42 | parameters 43 | .map(ParameterAccessor::getSort) // 44 | .orElse(Sort.unsorted())); 45 | } 46 | 47 | public T createQuery(Sort dynamicSort) { 48 | Assert.notNull(dynamicSort, "DynamicSort must not be null"); 49 | return complete(createCriteria(tree), tree.getSort().and(dynamicSort)); 50 | } 51 | 52 | @Nullable 53 | private S createCriteria(PartTree tree) { 54 | S base = null; 55 | Iterator iterator = 56 | parameters.map(ParameterAccessor::iterator).orElse(Collections.emptyIterator()); 57 | 58 | for (PartTree.OrPart node : tree) { 59 | Iterator parts = node.iterator(); 60 | 61 | if (!parts.hasNext()) { 62 | throw new IllegalStateException(String.format("No part found in PartTree %s", tree)); 63 | } 64 | 65 | S criteria = create(parts.next(), iterator); 66 | while (parts.hasNext()) { 67 | criteria = and(parts.next(), criteria, iterator); 68 | } 69 | 70 | base = base == null ? criteria : or(base, criteria); 71 | } 72 | 73 | return base; 74 | } 75 | 76 | protected abstract S create(Part part, Iterator iterator); 77 | 78 | protected abstract S and(Part part, S base, Iterator iterator); 79 | 80 | protected abstract S or(S base, S criteria); 81 | 82 | protected abstract T complete(@Nullable S criteria, Sort sort); 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/BasicParameterValueEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.Optional; 4 | import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; 5 | import reactor.core.publisher.Mono; 6 | 7 | /** 8 | * @author Bao.Ngo 9 | */ 10 | public class BasicParameterValueEvaluator implements ParameterValueEvaluator { 11 | 12 | private final ReactiveJpaParameters.JpaParameter parameter; 13 | 14 | public BasicParameterValueEvaluator(ReactiveJpaParameters.JpaParameter parameter) { 15 | this.parameter = parameter; 16 | } 17 | 18 | @Override 19 | public Mono> evaluate(JpaParametersParameterAccessor accessor) { 20 | return Mono.just(accessor).map(a -> Optional.ofNullable(a.getValue(parameter))); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/DeclaredQuery.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.List; 4 | import org.springframework.lang.Nullable; 5 | import org.springframework.util.ObjectUtils; 6 | 7 | public interface DeclaredQuery { 8 | static DeclaredQuery of(@Nullable String query, boolean nativeQuery) { 9 | return ObjectUtils.isEmpty(query) 10 | ? EmptyDeclaredQuery.EMPTY_QUERY 11 | : new StringQuery(query, nativeQuery); 12 | } 13 | 14 | boolean hasNamedParameter(); 15 | 16 | boolean usesJdbcStyleParameters(); 17 | 18 | String getQueryString(); 19 | 20 | @Nullable 21 | String getAlias(); 22 | 23 | boolean hasConstructorExpression(); 24 | 25 | boolean isDefaultProjection(); 26 | 27 | List getParameterBindings(); 28 | 29 | DeclaredQuery deriveCountQuery( 30 | @Nullable String countQuery, @Nullable String countQueryProjection); 31 | 32 | default boolean usesPaging() { 33 | return false; 34 | } 35 | 36 | default boolean isNativeQuery() { 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/DefaultQueryEnhancer.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.Set; 4 | import org.springframework.data.domain.Sort; 5 | import org.springframework.lang.Nullable; 6 | 7 | /** 8 | * @author Bao.Ngo 9 | */ 10 | public class DefaultQueryEnhancer implements QueryEnhancer { 11 | 12 | protected final DeclaredQuery query; 13 | 14 | public DefaultQueryEnhancer(DeclaredQuery query) { 15 | this.query = query; 16 | } 17 | 18 | @Override 19 | public String applySorting(Sort sort, @Nullable String alias) { 20 | return QueryUtils.applySorting(this.query.getQueryString(), sort, alias); 21 | } 22 | 23 | @Override 24 | public String detectAlias() { 25 | return QueryUtils.detectAlias(this.query.getQueryString()); 26 | } 27 | 28 | @Override 29 | public String createCountQueryFor(@Nullable String countProjection) { 30 | return QueryUtils.createCountQueryFor( 31 | this.query.getQueryString(), countProjection, this.query.isNativeQuery()); 32 | } 33 | 34 | @Override 35 | public String getProjection() { 36 | return QueryUtils.getProjection(this.query.getQueryString()); 37 | } 38 | 39 | @Override 40 | public Set getJoinAliases() { 41 | return QueryUtils.getOuterJoinAliases(this.query.getQueryString()); 42 | } 43 | 44 | @Override 45 | public DeclaredQuery getQuery() { 46 | return this.query; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/DefaultReactiveJpaQueryExtractor.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.lang.reflect.Field; 4 | import org.hibernate.reactive.query.ReactiveQuery; 5 | import org.hibernate.reactive.stage.Stage; 6 | import org.hibernate.reactive.stage.impl.StageQueryImpl; 7 | import org.springframework.data.util.ReflectionUtils; 8 | 9 | /** 10 | * @author Bao.Ngo 11 | */ 12 | public class DefaultReactiveJpaQueryExtractor implements ReactiveJpaQueryExtractor { 13 | 14 | @Override 15 | public String extractQueryString(Stage.AbstractQuery query) { 16 | if (query instanceof StageQueryImpl sqi) { 17 | try { 18 | Field delegate = ReflectionUtils.findRequiredField(StageQueryImpl.class, "delegate"); 19 | org.springframework.util.ReflectionUtils.makeAccessible(delegate); 20 | ReactiveQuery q = (ReactiveQuery) delegate.get(sqi); 21 | return q.getQueryString(); 22 | } catch (IllegalAccessException e) { 23 | throw new RuntimeException(e); 24 | } 25 | } 26 | 27 | throw new IllegalArgumentException("Don't know how to extract the query string from " + query); 28 | } 29 | 30 | @Override 31 | public boolean canExtractQuery() { 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/EmptyDeclaredQuery.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import org.springframework.lang.Nullable; 6 | import org.springframework.util.Assert; 7 | 8 | public class EmptyDeclaredQuery implements DeclaredQuery { 9 | 10 | static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery(); 11 | 12 | @Override 13 | public boolean hasNamedParameter() { 14 | return false; 15 | } 16 | 17 | @Override 18 | public boolean usesJdbcStyleParameters() { 19 | return false; 20 | } 21 | 22 | @Override 23 | public String getQueryString() { 24 | return ""; 25 | } 26 | 27 | @Override 28 | public String getAlias() { 29 | return null; 30 | } 31 | 32 | @Override 33 | public boolean hasConstructorExpression() { 34 | return false; 35 | } 36 | 37 | @Override 38 | public boolean isDefaultProjection() { 39 | return false; 40 | } 41 | 42 | @Override 43 | public List getParameterBindings() { 44 | return Collections.emptyList(); 45 | } 46 | 47 | @Override 48 | public DeclaredQuery deriveCountQuery( 49 | @Nullable String countQuery, @Nullable String countQueryProjection) { 50 | 51 | Assert.hasText(countQuery, "CountQuery must not be empty"); 52 | 53 | return DeclaredQuery.of(countQuery, false); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ExpressionBasedStringQuery.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.regex.Pattern; 4 | import org.springframework.data.jpa.repository.query.JpaEntityMetadata; 5 | import org.springframework.expression.Expression; 6 | import org.springframework.expression.ParserContext; 7 | import org.springframework.expression.spel.standard.SpelExpressionParser; 8 | import org.springframework.expression.spel.support.StandardEvaluationContext; 9 | import org.springframework.util.Assert; 10 | 11 | /** 12 | * @author Bao.Ngo 13 | */ 14 | public class ExpressionBasedStringQuery extends StringQuery { 15 | 16 | private static final String EXPRESSION_PARAMETER = "$1#{"; 17 | private static final String QUOTED_EXPRESSION_PARAMETER = "$1__HASH__{"; 18 | 19 | private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern.compile("([:?])#\\{"); 20 | private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = 21 | Pattern.compile("([:?])__HASH__\\{"); 22 | 23 | private static final String ENTITY_NAME = "entityName"; 24 | private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME; 25 | private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE; 26 | 27 | public ExpressionBasedStringQuery( 28 | String query, 29 | JpaEntityMetadata metadata, 30 | SpelExpressionParser parser, 31 | boolean nativeQuery) { 32 | super( 33 | renderQueryIfExpressionOrReturnQuery(query, metadata, parser), 34 | nativeQuery && !containsExpression(query)); 35 | } 36 | 37 | static ExpressionBasedStringQuery from( 38 | DeclaredQuery query, 39 | JpaEntityMetadata metadata, 40 | SpelExpressionParser parser, 41 | boolean nativeQuery) { 42 | return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery); 43 | } 44 | 45 | private static String renderQueryIfExpressionOrReturnQuery( 46 | String query, JpaEntityMetadata metadata, SpelExpressionParser parser) { 47 | 48 | Assert.notNull(query, "query must not be null"); 49 | Assert.notNull(metadata, "metadata must not be null"); 50 | Assert.notNull(parser, "parser must not be null"); 51 | 52 | if (!containsExpression(query)) { 53 | return query; 54 | } 55 | 56 | StandardEvaluationContext evalContext = new StandardEvaluationContext(); 57 | evalContext.setVariable(ENTITY_NAME, metadata.getEntityName()); 58 | 59 | query = potentiallyQuoteExpressionsParameter(query); 60 | 61 | Expression expr = parser.parseExpression(query, ParserContext.TEMPLATE_EXPRESSION); 62 | 63 | String result = expr.getValue(evalContext, String.class); 64 | 65 | if (result == null) { 66 | return query; 67 | } 68 | 69 | return potentiallyUnquoteParameterExpressions(result); 70 | } 71 | 72 | private static String potentiallyUnquoteParameterExpressions(String result) { 73 | return EXPRESSION_PARAMETER_UNQUOTING.matcher(result).replaceAll(EXPRESSION_PARAMETER); 74 | } 75 | 76 | private static String potentiallyQuoteExpressionsParameter(String query) { 77 | return EXPRESSION_PARAMETER_QUOTING.matcher(query).replaceAll(QUOTED_EXPRESSION_PARAMETER); 78 | } 79 | 80 | private static boolean containsExpression(String query) { 81 | return query.contains(ENTITY_NAME_VARIABLE_EXPRESSION); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/Jpa21Utils.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.*; 4 | import java.lang.reflect.Method; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import org.hibernate.reactive.stage.Stage; 9 | import org.springframework.data.jpa.repository.query.JpaEntityGraph; 10 | import org.springframework.data.jpa.repository.support.MutableQueryHints; 11 | import org.springframework.data.jpa.repository.support.QueryHints; 12 | import org.springframework.lang.Nullable; 13 | import org.springframework.util.*; 14 | 15 | /** 16 | * @author Bao.Ngo 17 | */ 18 | public class Jpa21Utils { 19 | 20 | private static final @Nullable Method GET_ENTITY_GRAPH_METHOD; 21 | private static final boolean JPA21_AVAILABLE = 22 | ClassUtils.isPresent( 23 | "jakarta.persistence.NamedEntityGraph", 24 | org.springframework.data.jpa.repository.query.Jpa21Utils.class.getClassLoader()); 25 | 26 | static { 27 | if (JPA21_AVAILABLE) { 28 | GET_ENTITY_GRAPH_METHOD = 29 | ReflectionUtils.findMethod(EntityManager.class, "getEntityGraph", String.class); 30 | } else { 31 | GET_ENTITY_GRAPH_METHOD = null; 32 | } 33 | } 34 | 35 | private Jpa21Utils() { 36 | // prevent instantiation 37 | } 38 | 39 | public static QueryHints getFetchGraphHint( 40 | Stage.Session em, @Nullable JpaEntityGraph entityGraph, Class entityType) { 41 | MutableQueryHints result = new MutableQueryHints(); 42 | 43 | if (entityGraph == null) { 44 | return result; 45 | } 46 | 47 | EntityGraph graph = tryGetFetchGraph(em, entityGraph, entityType); 48 | if (graph == null) { 49 | return result; 50 | } 51 | 52 | result.add(entityGraph.getType().getKey(), graph); 53 | return result; 54 | } 55 | 56 | @Nullable 57 | private static EntityGraph tryGetFetchGraph( 58 | Stage.Session session, JpaEntityGraph jpaEntityGraph, Class entityType) { 59 | Assert.notNull(session, "EntityManager must not be null"); 60 | Assert.notNull(jpaEntityGraph, "EntityGraph must not be null"); 61 | Assert.notNull(entityType, "EntityType must not be null"); 62 | 63 | Assert.isTrue( 64 | JPA21_AVAILABLE, 65 | "The EntityGraph-Feature requires at least a JPA 2.1 persistence provider"); 66 | Assert.isTrue( 67 | GET_ENTITY_GRAPH_METHOD != null, 68 | "It seems that you have the JPA 2.1 API but a JPA 2.0 implementation on the classpath"); 69 | 70 | try { 71 | // first check whether an entityGraph with that name is already registered. 72 | return session.getEntityGraph(entityType, jpaEntityGraph.getName()); 73 | } catch (Exception ex) { 74 | // try to create and dynamically register the entityGraph 75 | return createDynamicEntityGraph(session, jpaEntityGraph, entityType); 76 | } 77 | } 78 | 79 | private static EntityGraph createDynamicEntityGraph( 80 | Stage.Session em, JpaEntityGraph jpaEntityGraph, Class entityType) { 81 | Assert.notNull(em, "EntityManager must not be null"); 82 | Assert.notNull(jpaEntityGraph, "JpaEntityGraph must not be null"); 83 | Assert.notNull(entityType, "Entity type must not be null"); 84 | Assert.isTrue( 85 | jpaEntityGraph.isAdHocEntityGraph(), "The given " + jpaEntityGraph + " is not dynamic"); 86 | 87 | EntityGraph entityGraph = em.createEntityGraph(entityType); 88 | configureFetchGraphFrom(jpaEntityGraph, entityGraph); 89 | 90 | return entityGraph; 91 | } 92 | 93 | static void configureFetchGraphFrom(JpaEntityGraph jpaEntityGraph, EntityGraph entityGraph) { 94 | List attributePaths = new ArrayList<>(jpaEntityGraph.getAttributePaths()); 95 | 96 | // Sort to ensure that the intermediate entity subgraphs are created accordingly. 97 | Collections.sort(attributePaths); 98 | 99 | for (String path : attributePaths) { 100 | String[] pathComponents = StringUtils.delimitedListToStringArray(path, "."); 101 | createGraph(pathComponents, 0, entityGraph, null); 102 | } 103 | } 104 | 105 | private static void createGraph( 106 | String[] pathComponents, int offset, EntityGraph root, @Nullable Subgraph parent) { 107 | String attributeName = pathComponents[offset]; 108 | 109 | // we found our leaf property, now let's see if it already exists and add it if not 110 | if (pathComponents.length - 1 == offset) { 111 | if (parent == null && !exists(attributeName, root.getAttributeNodes())) { 112 | root.addAttributeNodes(attributeName); 113 | } else if (parent != null && !exists(attributeName, parent.getAttributeNodes())) { 114 | parent.addAttributeNodes(attributeName); 115 | } 116 | 117 | return; 118 | } 119 | 120 | AttributeNode node = findAttributeNode(attributeName, root, parent); 121 | if (node != null) { 122 | Subgraph subgraph = getSubgraph(node); 123 | if (subgraph == null) { 124 | subgraph = 125 | parent != null ? parent.addSubgraph(attributeName) : root.addSubgraph(attributeName); 126 | } 127 | 128 | createGraph(pathComponents, offset + 1, root, subgraph); 129 | return; 130 | } 131 | 132 | if (parent == null) { 133 | createGraph(pathComponents, offset + 1, root, root.addSubgraph(attributeName)); 134 | } else { 135 | createGraph(pathComponents, offset + 1, root, parent.addSubgraph(attributeName)); 136 | } 137 | } 138 | 139 | private static boolean exists(String attributeNodeName, List> nodes) { 140 | return findAttributeNode(attributeNodeName, nodes) != null; 141 | } 142 | 143 | @Nullable 144 | private static AttributeNode findAttributeNode( 145 | String attributeNodeName, EntityGraph entityGraph, @Nullable Subgraph parent) { 146 | return findAttributeNode( 147 | attributeNodeName, 148 | parent != null ? parent.getAttributeNodes() : entityGraph.getAttributeNodes()); 149 | } 150 | 151 | @Nullable 152 | private static AttributeNode findAttributeNode( 153 | String attributeNodeName, List> nodes) { 154 | for (AttributeNode node : nodes) { 155 | if (ObjectUtils.nullSafeEquals(node.getAttributeName(), attributeNodeName)) { 156 | return node; 157 | } 158 | } 159 | 160 | return null; 161 | } 162 | 163 | @Nullable 164 | private static Subgraph getSubgraph(AttributeNode node) { 165 | return node.getSubgraphs().isEmpty() ? null : node.getSubgraphs().values().iterator().next(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/NamedQuery.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import jakarta.persistence.EntityManagerFactory; 5 | import jakarta.persistence.Tuple; 6 | import java.util.Optional; 7 | import java.util.concurrent.locks.Lock; 8 | import java.util.concurrent.locks.ReentrantLock; 9 | import org.apache.commons.logging.Log; 10 | import org.apache.commons.logging.LogFactory; 11 | import org.hibernate.reactive.stage.Stage; 12 | import org.springframework.data.repository.query.*; 13 | import org.springframework.lang.Nullable; 14 | import reactor.core.publisher.Mono; 15 | 16 | /** 17 | * @author Bao.Ngo 18 | */ 19 | public class NamedQuery extends AbstractReactiveJpaQuery { 20 | 21 | private static final String CANNOT_EXTRACT_QUERY = 22 | "Your persistence provider does not support extracting the JPQL query from a " 23 | + "named query thus you can't use Pageable inside your query method; Make sure you " 24 | + "have a JpaDialect configured at your EntityManagerFactoryBean as this affects " 25 | + "discovering the concrete persistence provider"; 26 | 27 | private static final Log LOG = LogFactory.getLog(NamedQuery.class); 28 | 29 | private final EntityManagerFactory emf; 30 | private final String queryName; 31 | private final String countQueryName; 32 | private final @Nullable String countProjection; 33 | private final QueryParameterSetter.QueryMetadataCache metadataCache; 34 | 35 | private final Lock lock = new ReentrantLock(); 36 | 37 | private boolean namedCountQueryIsPresent; 38 | private DeclaredQuery declaredQuery; 39 | 40 | private boolean fullyInitialized; 41 | 42 | private NamedQuery( 43 | ReactiveJpaQueryMethod method, 44 | Stage.SessionFactory sessionFactory, 45 | EntityManagerFactory emf) { 46 | super(method, sessionFactory); 47 | 48 | this.emf = emf; 49 | this.queryName = method.getNamedQueryName(); 50 | this.countQueryName = method.getNamedCountQueryName(); 51 | this.countProjection = method.getCountQueryProjection(); 52 | this.metadataCache = new QueryParameterSetter.QueryMetadataCache(); 53 | 54 | Parameters parameters = method.getParameters(); 55 | if (parameters.hasSortParameter()) { 56 | throw new IllegalStateException( 57 | String.format( 58 | "Finder method %s is backed by a NamedQuery and must " 59 | + "not contain a sort parameter as we cannot modify the query; Use @Query instead", 60 | method)); 61 | } 62 | 63 | if (parameters.hasPageableParameter()) { 64 | LOG.warn( 65 | String.format( 66 | "Finder method %s is backed by a NamedQuery but contains a Pageable parameter; Sorting delivered via this Pageable will not be applied", 67 | method)); 68 | } 69 | 70 | this.namedCountQueryIsPresent = hasNamedQuery(emf, countQueryName); 71 | } 72 | 73 | // TODO 74 | /* static boolean hasNamedQuery(Stage.SessionFactory em, String queryName) { 75 | try { 76 | em.createNamedQuery(queryName); 77 | return true; 78 | } catch (IllegalArgumentException e) { 79 | 80 | if (LOG.isDebugEnabled()) { 81 | LOG.debug(String.format("Did not find named query %s", queryName)); 82 | } 83 | return false; 84 | } finally { 85 | } 86 | }*/ 87 | 88 | @Nullable 89 | public static RepositoryQuery lookupFrom( 90 | ReactiveJpaQueryMethod method, 91 | Stage.SessionFactory sessionFactory, 92 | EntityManagerFactory emf) { 93 | String queryName = method.getNamedQueryName(); 94 | 95 | if (LOG.isDebugEnabled()) { 96 | LOG.debug(String.format("Looking up named query %s", queryName)); 97 | } 98 | 99 | // TODO 100 | if (!hasNamedQuery(emf, queryName)) { 101 | return null; 102 | } 103 | 104 | if (method.isScrollQuery()) { 105 | throw QueryCreationException.create( 106 | method, "Scroll queries are not supported using String-based queries"); 107 | } 108 | 109 | try { 110 | 111 | RepositoryQuery query = new NamedQuery(method, sessionFactory, emf); 112 | if (LOG.isDebugEnabled()) { 113 | LOG.debug(String.format("Found named query %s", queryName)); 114 | } 115 | return query; 116 | } catch (IllegalArgumentException e) { 117 | return null; 118 | } 119 | } 120 | 121 | static boolean hasNamedQuery(EntityManagerFactory emf, String queryName) { 122 | try (EntityManager lookupEm = emf.createEntityManager()) { 123 | lookupEm.createNamedQuery(queryName); 124 | return true; 125 | } catch (IllegalArgumentException e) { 126 | if (LOG.isDebugEnabled()) { 127 | LOG.debug(String.format("Did not find named query %s", queryName)); 128 | } 129 | return false; 130 | } 131 | } 132 | 133 | @Override 134 | protected Mono doCreateQuery( 135 | Mono session, 136 | ReactiveJpaParametersParameterAccessor accessor, 137 | ReactiveJpaQueryMethod method) { 138 | ReactiveJpaQueryMethod queryMethod = getQueryMethod(); 139 | ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(accessor); 140 | // Class typeToRead = getTypeToRead(processor.getReturnedType()); 141 | 142 | return session 143 | .doOnNext(this::initialize) 144 | .zipWhen(__ -> Mono.fromSupplier(() -> getTypeToRead(processor.getReturnedType()))) 145 | .map( 146 | t -> { 147 | Stage.Session s = t.getT1(); 148 | Optional> typeToRead = t.getT2(); 149 | 150 | return typeToRead.isEmpty() 151 | ? s.createNamedQuery(queryName) 152 | : s.createNamedQuery(queryName, typeToRead.get()); 153 | }) 154 | .zipWhen(q -> Mono.fromSupplier(() -> metadataCache.getMetadata(queryName, q))) 155 | .flatMap(t -> parameterBinder.get().bindAndPrepare(t.getT1(), t.getT2(), accessor)); 156 | // Stage.AbstractQuery query = typeToRead == null // 157 | // ? em.createNamedQuery(queryName) // 158 | // : em.createNamedQuery(queryName, typeToRead); 159 | 160 | // QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryName, query); 161 | 162 | // return parameterBinder.get().bindAndPrepare(query, metadata, accessor); 163 | // TODO 164 | // return Mono.empty(); 165 | } 166 | 167 | @Override 168 | protected Mono doCreateCountQuery( 169 | Mono session, ReactiveJpaParametersParameterAccessor accessor) { 170 | // Mono countQuery; 171 | // String cacheKey = ""; 172 | return session 173 | .doOnNext(this::initialize) 174 | .flatMap( 175 | s -> { 176 | String cacheKey; 177 | Stage.AbstractQuery countQuery; 178 | if (namedCountQueryIsPresent) { 179 | cacheKey = countQueryName; 180 | countQuery = s.createNamedQuery(countQueryName, Long.class); 181 | } else { 182 | String countQueryString = 183 | declaredQuery.deriveCountQuery(null, countProjection).getQueryString(); 184 | cacheKey = countQueryString; 185 | countQuery = s.createQuery(countQueryString, Long.class); 186 | } 187 | 188 | QueryParameterSetter.QueryMetadata metadata = 189 | metadataCache.getMetadata(cacheKey, countQuery); 190 | 191 | return parameterBinder.get().bind(countQuery, metadata, accessor); 192 | }); 193 | 194 | // if (namedCountQueryIsPresent) { 195 | // cacheKey = countQueryName; 196 | // countQuery = session.map(s -> s.createNamedQuery(countQueryName, Long.class)); 197 | // } else { 198 | // String countQueryString = declaredQuery.deriveCountQuery(null, 199 | // countProjection).getQueryString(); 200 | // cacheKey = countQueryString; 201 | // countQuery = em.createQuery(countQueryString, Long.class); 202 | // } 203 | 204 | // QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(cacheKey, 205 | // countQuery); 206 | // 207 | // return parameterBinder.get().bind(countQuery, metadata, accessor); 208 | // // TODO 209 | // return null; 210 | } 211 | 212 | @Override 213 | protected Optional> getTypeToRead(ReturnedType returnedType) { 214 | if (getQueryMethod().isNativeQuery()) { 215 | Class type = returnedType.getReturnedType(); 216 | Class domainType = returnedType.getDomainType(); 217 | 218 | // Domain or subtype -> use return type 219 | if (domainType.isAssignableFrom(type)) { 220 | return Optional.of(type); 221 | } 222 | 223 | // Domain type supertype -> use domain type 224 | if (type.isAssignableFrom(domainType)) { 225 | return Optional.of(domainType); 226 | } 227 | 228 | // Tuples for projection interfaces or explicit SQL mappings for everything else 229 | return type.isInterface() ? Optional.of(Tuple.class) : Optional.empty(); 230 | } 231 | 232 | return declaredQuery.hasConstructorExpression() // 233 | ? Optional.empty() // 234 | : super.getTypeToRead(returnedType); 235 | } 236 | 237 | private void initialize(Stage.Session session) { 238 | if (fullyInitialized) { 239 | return; 240 | } 241 | 242 | try { 243 | lock.lock(); 244 | ReactiveJpaQueryExtractor extractor = method.getQueryExtractor(); 245 | // 246 | // Parameters parameters = method.getParameters(); 247 | // if (parameters.hasSortParameter()) { 248 | // throw new IllegalStateException( 249 | // String.format( 250 | // "Finder method %s is backed by a NamedQuery and must " 251 | // + "not contain a sort parameter as we cannot modify the query; Use 252 | // @Query instead", 253 | // method)); 254 | // } 255 | // 256 | // this.namedCountQueryIsPresent = hasNamedQuery(emf, countQueryName); 257 | 258 | Stage.Query query = session.createNamedQuery(queryName); 259 | String queryString = extractor.extractQueryString(query); 260 | this.declaredQuery = DeclaredQuery.of(queryString, false); 261 | 262 | boolean needToCreateCountQuery = 263 | !namedCountQueryIsPresent && method.getParameters().hasLimitingParameters(); 264 | 265 | if (needToCreateCountQuery && !extractor.canExtractQuery()) { 266 | throw QueryCreationException.create(method, CANNOT_EXTRACT_QUERY); 267 | } 268 | 269 | fullyInitialized = true; 270 | } finally { 271 | lock.unlock(); 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/NativeReactiveJpaQuery.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.Tuple; 4 | import org.hibernate.reactive.stage.Stage; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.domain.Sort; 7 | import org.springframework.data.jpa.repository.QueryRewriter; 8 | import org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException; 9 | import org.springframework.data.repository.query.Parameters; 10 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; 11 | import org.springframework.data.repository.query.ReturnedType; 12 | import org.springframework.expression.spel.standard.SpelExpressionParser; 13 | import org.springframework.lang.Nullable; 14 | import reactor.core.publisher.Mono; 15 | 16 | /** 17 | * @author Bao.Ngo 18 | */ 19 | public class NativeReactiveJpaQuery extends AbstractStringBasedReactiveJpaQuery { 20 | 21 | public NativeReactiveJpaQuery( 22 | ReactiveJpaQueryMethod method, 23 | Stage.SessionFactory sessionFactory, 24 | String queryString, 25 | @Nullable String countQueryString, 26 | QueryRewriter rewriter, 27 | ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, 28 | SpelExpressionParser parser) { 29 | 30 | super( 31 | method, 32 | sessionFactory, 33 | queryString, 34 | countQueryString, 35 | rewriter, 36 | evaluationContextProvider, 37 | parser); 38 | 39 | Parameters parameters = method.getParameters(); 40 | 41 | if (parameters.hasSortParameter() && !queryString.contains("#sort")) { 42 | throw new InvalidJpaQueryMethodException( 43 | "Cannot use native queries with dynamic sorting in method " + method); 44 | } 45 | } 46 | 47 | @Override 48 | protected Mono createReactiveJpaQuery( 49 | Mono session, 50 | String queryString, 51 | ReactiveJpaQueryMethod method, 52 | Sort sort, 53 | Pageable pageable, 54 | ReturnedType returnedType) { 55 | return session.map( 56 | s -> { 57 | Class type = getTypeToQueryFor(returnedType); 58 | return type == null 59 | ? s.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable)) 60 | : s.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable), type); 61 | }); 62 | // Class type = getTypeToQueryFor(returnedType); 63 | // 64 | // return type == null 65 | // ? session.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable)) 66 | // : session.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable), 67 | // type); 68 | } 69 | 70 | @Nullable 71 | private Class getTypeToQueryFor(ReturnedType returnedType) { 72 | Class result = getQueryMethod().isQueryForEntity() ? returnedType.getDomainType() : null; 73 | 74 | if (this.getQuery().hasConstructorExpression() || this.getQuery().isDefaultProjection()) { 75 | return result; 76 | } 77 | 78 | return returnedType.isProjecting() 79 | && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) // 80 | ? Tuple.class 81 | : result; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ParameterBinder.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import org.hibernate.reactive.stage.Stage; 4 | import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; 5 | import org.springframework.data.jpa.support.PageableUtils; 6 | import org.springframework.util.Assert; 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * @author Bao.Ngo 12 | */ 13 | public class ParameterBinder { 14 | 15 | static final String PARAMETER_NEEDS_TO_BE_NAMED = 16 | "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters"; 17 | 18 | private final ReactiveJpaParameters parameters; 19 | private final Iterable parameterSetters; 20 | private final boolean useJpaForPaging; 21 | 22 | ParameterBinder( 23 | ReactiveJpaParameters parameters, Iterable parameterSetters) { 24 | this(parameters, parameterSetters, true); 25 | } 26 | 27 | public ParameterBinder( 28 | ReactiveJpaParameters parameters, 29 | Iterable parameterSetters, 30 | boolean useJpaForPaging) { 31 | 32 | Assert.notNull(parameters, "ReactiveJpaParameters must not be null"); 33 | Assert.notNull(parameterSetters, "Parameter setters must not be null"); 34 | 35 | this.parameters = parameters; 36 | this.parameterSetters = parameterSetters; 37 | this.useJpaForPaging = useJpaForPaging; 38 | } 39 | 40 | public Mono bind( 41 | T jpaQuery, 42 | QueryParameterSetter.QueryMetadata metadata, 43 | JpaParametersParameterAccessor accessor) { 44 | 45 | return bind(metadata.withQuery(jpaQuery), accessor, QueryParameterSetter.ErrorHandling.STRICT) 46 | .thenReturn(jpaQuery); 47 | // return jpaQuery; 48 | } 49 | 50 | public Mono bind( 51 | QueryParameterSetter.BindableQuery query, 52 | JpaParametersParameterAccessor accessor, 53 | QueryParameterSetter.ErrorHandling errorHandling) { 54 | 55 | // for (QueryParameterSetter setter : parameterSetters) { 56 | // setter.setParameter(query, accessor, errorHandling); 57 | // } 58 | return Flux.fromIterable(parameterSetters) 59 | .concatMap(setter -> setter.setParameter(query, accessor, errorHandling)) 60 | .then(); 61 | } 62 | 63 | Mono bindAndPrepare( 64 | Stage.AbstractQuery query, 65 | QueryParameterSetter.QueryMetadata metadata, 66 | JpaParametersParameterAccessor accessor) { 67 | 68 | return bind(query, metadata, accessor) 69 | .then( 70 | Mono.defer( 71 | () -> { 72 | if (!useJpaForPaging 73 | || !parameters.hasLimitingParameters() 74 | || accessor.getPageable().isUnpaged()) { 75 | return Mono.just(query); 76 | } 77 | 78 | if (query instanceof Stage.SelectionQuery selectionQuery) { 79 | selectionQuery.setFirstResult( 80 | PageableUtils.getOffsetAsInteger(accessor.getPageable())); 81 | selectionQuery.setMaxResults(accessor.getPageable().getPageSize()); 82 | } 83 | 84 | return Mono.just(query); 85 | })); 86 | 87 | // if (!useJpaForPaging 88 | // || !parameters.hasLimitingParameters() 89 | // || accessor.getPageable().isUnpaged()) { 90 | // return query; 91 | // } 92 | 93 | // if (query instanceof Stage.SelectionQuery selectionQuery) { 94 | // selectionQuery.setFirstResult(PageableUtils.getOffsetAsInteger(accessor.getPageable())); 95 | // selectionQuery.setMaxResults(accessor.getPageable().getPageSize()); 96 | // } 97 | // 98 | // return query; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ParameterBinderFactory.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; 6 | import org.springframework.expression.spel.standard.SpelExpressionParser; 7 | import org.springframework.util.Assert; 8 | 9 | public class ParameterBinderFactory { 10 | 11 | static ParameterBinder createBinder(ReactiveJpaParameters parameters) { 12 | Assert.notNull(parameters, "ReactiveJpaParameters must not be null"); 13 | 14 | QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters); 15 | List bindings = getBindings(parameters); 16 | 17 | return new ParameterBinder(parameters, createSetters(bindings, setterFactory)); 18 | } 19 | 20 | static ParameterBinder createCriteriaBinder( 21 | ReactiveJpaParameters parameters, 22 | List> metadata) { 23 | 24 | Assert.notNull(parameters, "ReactiveJpaParameters must not be null"); 25 | Assert.notNull(metadata, "Parameter metadata must not be null"); 26 | 27 | QueryParameterSetterFactory setterFactory = 28 | QueryParameterSetterFactory.forCriteriaQuery(parameters, metadata); 29 | List bindings = getBindings(parameters); 30 | 31 | return new ParameterBinder(parameters, createSetters(bindings, setterFactory)); 32 | } 33 | 34 | static ParameterBinder createQueryAwareBinder( 35 | ReactiveJpaParameters parameters, 36 | DeclaredQuery query, 37 | SpelExpressionParser parser, 38 | ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { 39 | 40 | Assert.notNull(parameters, "ReactiveJpaParameters must not be null"); 41 | Assert.notNull(query, "StringQuery must not be null"); 42 | Assert.notNull(parser, "SpelExpressionParser must not be null"); 43 | Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null"); 44 | 45 | List bindings = query.getParameterBindings(); 46 | QueryParameterSetterFactory expressionSetterFactory = 47 | QueryParameterSetterFactory.parsing(parser, evaluationContextProvider, parameters); 48 | 49 | QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters); 50 | 51 | return new ParameterBinder( 52 | parameters, 53 | createSetters(bindings, query, expressionSetterFactory, basicSetterFactory), 54 | !query.usesPaging()); 55 | } 56 | 57 | private static List getBindings(ReactiveJpaParameters parameters) { 58 | 59 | List result = new ArrayList<>(); 60 | int bindableParameterIndex = 0; 61 | 62 | for (ReactiveJpaParameters.JpaParameter parameter : parameters) { 63 | 64 | if (parameter.isBindable()) { 65 | int index = ++bindableParameterIndex; 66 | ParameterBinding.BindingIdentifier bindingIdentifier = 67 | parameter 68 | .getName() 69 | .map(it -> ParameterBinding.BindingIdentifier.of(it, index)) 70 | .orElseGet(() -> ParameterBinding.BindingIdentifier.of(index)); 71 | 72 | result.add( 73 | new ParameterBinding( 74 | bindingIdentifier, 75 | ParameterBinding.ParameterOrigin.ofParameter(bindingIdentifier))); 76 | } 77 | } 78 | 79 | return result; 80 | } 81 | 82 | private static Iterable createSetters( 83 | List parameterBindings, QueryParameterSetterFactory... factories) { 84 | return createSetters(parameterBindings, EmptyDeclaredQuery.EMPTY_QUERY, factories); 85 | } 86 | 87 | private static Iterable createSetters( 88 | List parameterBindings, 89 | DeclaredQuery declaredQuery, 90 | QueryParameterSetterFactory... strategies) { 91 | List setters = new ArrayList<>(parameterBindings.size()); 92 | for (ParameterBinding parameterBinding : parameterBindings) { 93 | setters.add(createQueryParameterSetter(parameterBinding, strategies, declaredQuery)); 94 | } 95 | 96 | return setters; 97 | } 98 | 99 | private static QueryParameterSetter createQueryParameterSetter( 100 | ParameterBinding binding, 101 | QueryParameterSetterFactory[] strategies, 102 | DeclaredQuery declaredQuery) { 103 | for (QueryParameterSetterFactory strategy : strategies) { 104 | QueryParameterSetter setter = strategy.create(binding, declaredQuery); 105 | 106 | if (setter != null) { 107 | return setter; 108 | } 109 | } 110 | 111 | return QueryParameterSetter.NOOP; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ParameterMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.criteria.CriteriaBuilder; 4 | import jakarta.persistence.criteria.ParameterExpression; 5 | import java.util.*; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Collectors; 8 | import org.springframework.data.jpa.repository.query.EscapeCharacter; 9 | import org.springframework.data.repository.query.Parameter; 10 | import org.springframework.data.repository.query.Parameters; 11 | import org.springframework.data.repository.query.ParametersParameterAccessor; 12 | import org.springframework.data.repository.query.parser.Part; 13 | import org.springframework.expression.Expression; 14 | import org.springframework.lang.Nullable; 15 | import org.springframework.util.Assert; 16 | import org.springframework.util.ClassUtils; 17 | import org.springframework.util.CollectionUtils; 18 | import org.springframework.util.ObjectUtils; 19 | 20 | public class ParameterMetadataProvider { 21 | 22 | private final CriteriaBuilder builder; 23 | private final Iterator parameters; 24 | private final List> expressions; 25 | private final @Nullable Iterator bindableParameterValues; 26 | private final EscapeCharacter escape; 27 | 28 | public ParameterMetadataProvider( 29 | CriteriaBuilder builder, ParametersParameterAccessor accessor, EscapeCharacter escape) { 30 | this(builder, accessor.iterator(), accessor.getParameters(), escape); 31 | } 32 | 33 | public ParameterMetadataProvider( 34 | CriteriaBuilder builder, Parameters parameters, EscapeCharacter escape) { 35 | this(builder, null, parameters, escape); 36 | } 37 | 38 | private ParameterMetadataProvider( 39 | CriteriaBuilder builder, 40 | @Nullable Iterator bindableParameterValues, 41 | Parameters parameters, 42 | EscapeCharacter escape) { 43 | 44 | Assert.notNull(builder, "CriteriaBuilder must not be null"); 45 | Assert.notNull(parameters, "Parameters must not be null"); 46 | Assert.notNull(escape, "EscapeCharacter must not be null"); 47 | 48 | this.builder = builder; 49 | this.parameters = parameters.getBindableParameters().iterator(); 50 | this.expressions = new ArrayList<>(); 51 | this.bindableParameterValues = bindableParameterValues; 52 | this.escape = escape; 53 | } 54 | 55 | public List> getExpressions() { 56 | return expressions; 57 | } 58 | 59 | @SuppressWarnings("unchecked") 60 | public ParameterMetadataProvider.ParameterMetadata next(Part part) { 61 | Assert.isTrue( 62 | parameters.hasNext(), () -> String.format("No parameter available for part %s", part)); 63 | 64 | Parameter parameter = parameters.next(); 65 | return (ParameterMetadataProvider.ParameterMetadata) 66 | next(part, parameter.getType(), parameter); 67 | } 68 | 69 | @SuppressWarnings("unchecked") 70 | public ParameterMetadataProvider.ParameterMetadata next( 71 | Part part, Class type) { 72 | Parameter parameter = parameters.next(); 73 | Class typeToUse = 74 | ClassUtils.isAssignable(type, parameter.getType()) ? parameter.getType() : type; 75 | return (ParameterMetadataProvider.ParameterMetadata) 76 | next(part, typeToUse, parameter); 77 | } 78 | 79 | private ParameterMetadataProvider.ParameterMetadata next( 80 | Part part, Class type, Parameter parameter) { 81 | Assert.notNull(type, "Type must not be null"); 82 | /* 83 | * We treat Expression types as Object vales since the real value to be bound as a parameter is determined at query time. 84 | */ 85 | @SuppressWarnings("unchecked") 86 | Class reifiedType = Expression.class.equals(type) ? (Class) Object.class : type; 87 | 88 | Supplier name = 89 | () -> 90 | parameter 91 | .getName() 92 | .orElseThrow(() -> new IllegalArgumentException("o_O Parameter needs to be named")); 93 | 94 | ParameterExpression expression = 95 | parameter.isExplicitlyNamed() // 96 | ? builder.parameter(reifiedType, name.get()) // 97 | : builder.parameter(reifiedType); 98 | 99 | Object value = 100 | bindableParameterValues == null 101 | ? ParameterMetadataProvider.ParameterMetadata.PLACEHOLDER 102 | : bindableParameterValues.next(); 103 | 104 | ParameterMetadataProvider.ParameterMetadata metadata = 105 | new ParameterMetadataProvider.ParameterMetadata<>(expression, part, value, escape); 106 | expressions.add(metadata); 107 | 108 | return metadata; 109 | } 110 | 111 | EscapeCharacter getEscape() { 112 | return escape; 113 | } 114 | 115 | static class ParameterMetadata { 116 | 117 | static final Object PLACEHOLDER = new Object(); 118 | 119 | private final Part.Type type; 120 | private final ParameterExpression expression; 121 | private final EscapeCharacter escape; 122 | private final boolean ignoreCase; 123 | private final boolean noWildcards; 124 | 125 | public ParameterMetadata( 126 | ParameterExpression expression, 127 | Part part, 128 | @Nullable Object value, 129 | EscapeCharacter escape) { 130 | 131 | this.expression = expression; 132 | this.type = 133 | value == null && Part.Type.SIMPLE_PROPERTY.equals(part.getType()) 134 | ? Part.Type.IS_NULL 135 | : part.getType(); 136 | this.ignoreCase = Part.IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase()); 137 | this.noWildcards = part.getProperty().getLeafProperty().isCollection(); 138 | this.escape = escape; 139 | } 140 | 141 | public ParameterExpression getExpression() { 142 | return expression; 143 | } 144 | 145 | public boolean isIsNullParameter() { 146 | return Part.Type.IS_NULL.equals(type); 147 | } 148 | 149 | @Nullable 150 | public Object prepare(@Nullable Object value) { 151 | if (value == null || expression.getJavaType() == null) { 152 | return value; 153 | } 154 | 155 | if (String.class.equals(expression.getJavaType()) && !noWildcards) { 156 | switch (type) { 157 | case STARTING_WITH: 158 | return String.format("%s%%", escape.escape(value.toString())); 159 | case ENDING_WITH: 160 | return String.format("%%%s", escape.escape(value.toString())); 161 | case CONTAINING: 162 | case NOT_CONTAINING: 163 | return String.format("%%%s%%", escape.escape(value.toString())); 164 | default: 165 | return value; 166 | } 167 | } 168 | 169 | return Collection.class.isAssignableFrom(expression.getJavaType()) // 170 | ? upperIfIgnoreCase(ignoreCase, toCollection(value)) // 171 | : value; 172 | } 173 | 174 | @Nullable 175 | private static Collection toCollection(@Nullable Object value) { 176 | if (value == null) { 177 | return null; 178 | } 179 | 180 | if (value instanceof Collection collection) { 181 | return collection.isEmpty() ? null : collection; 182 | } 183 | 184 | if (ObjectUtils.isArray(value)) { 185 | List collection = Arrays.asList(ObjectUtils.toObjectArray(value)); 186 | return collection.isEmpty() ? null : collection; 187 | } 188 | 189 | return Collections.singleton(value); 190 | } 191 | 192 | @Nullable 193 | @SuppressWarnings("unchecked") 194 | private static Collection upperIfIgnoreCase( 195 | boolean ignoreCase, @Nullable Collection collection) { 196 | if (!ignoreCase || CollectionUtils.isEmpty(collection)) { 197 | return collection; 198 | } 199 | 200 | return ((Collection) collection) 201 | .stream() // 202 | .map( 203 | it -> 204 | it == null // 205 | ? null // 206 | : it.toUpperCase()) // 207 | .collect(Collectors.toList()); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ParameterValueEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.Optional; 4 | import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; 5 | import reactor.core.publisher.Mono; 6 | 7 | /** 8 | * @author Bao.Ngo 9 | */ 10 | public interface ParameterValueEvaluator { 11 | 12 | Mono> evaluate(JpaParametersParameterAccessor accessor); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/PredicateBuilder.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import static com.htech.data.jpa.reactive.repository.query.QueryUtils.toExpressionRecursively; 4 | import static org.springframework.data.repository.query.parser.Part.Type.*; 5 | 6 | import jakarta.persistence.criteria.*; 7 | import java.util.Collection; 8 | import org.springframework.data.jpa.repository.query.EscapeCharacter; 9 | import org.springframework.data.mapping.PropertyPath; 10 | import org.springframework.data.repository.query.parser.Part; 11 | import org.springframework.util.Assert; 12 | 13 | @SuppressWarnings({"unchecked", "rawtypes"}) 14 | public class PredicateBuilder { 15 | 16 | private final ParameterMetadataProvider provider; 17 | private final CriteriaBuilder builder; 18 | private final Part part; 19 | private final Root root; 20 | private final EscapeCharacter escape; 21 | 22 | public PredicateBuilder( 23 | ParameterMetadataProvider provider, 24 | CriteriaBuilder builder, 25 | Part part, 26 | Root root, 27 | EscapeCharacter escape) { 28 | this.provider = provider; 29 | this.builder = builder; 30 | this.part = part; 31 | this.root = root; 32 | this.escape = escape; 33 | } 34 | 35 | public Predicate build() { 36 | PropertyPath property = part.getProperty(); 37 | Part.Type type = part.getType(); 38 | 39 | switch (type) { 40 | case BETWEEN: 41 | ParameterMetadataProvider.ParameterMetadata first = provider.next(part); 42 | ParameterMetadataProvider.ParameterMetadata second = provider.next(part); 43 | return builder.between( 44 | getComparablePath(root, part), first.getExpression(), second.getExpression()); 45 | case AFTER: 46 | case GREATER_THAN: 47 | return builder.greaterThan( 48 | getComparablePath(root, part), provider.next(part, Comparable.class).getExpression()); 49 | case GREATER_THAN_EQUAL: 50 | return builder.greaterThanOrEqualTo( 51 | getComparablePath(root, part), provider.next(part, Comparable.class).getExpression()); 52 | case BEFORE: 53 | case LESS_THAN: 54 | return builder.lessThan( 55 | getComparablePath(root, part), provider.next(part, Comparable.class).getExpression()); 56 | case LESS_THAN_EQUAL: 57 | return builder.lessThanOrEqualTo( 58 | getComparablePath(root, part), provider.next(part, Comparable.class).getExpression()); 59 | case IS_NULL: 60 | return getTypedPath(root, part).isNull(); 61 | case IS_NOT_NULL: 62 | return getTypedPath(root, part).isNotNull(); 63 | case NOT_IN: 64 | // cast required for eclipselink workaround, see DATAJPA-433 65 | return upperIfIgnoreCase(getTypedPath(root, part)) 66 | .in((Expression>) provider.next(part, Collection.class).getExpression()) 67 | .not(); 68 | case IN: 69 | // cast required for eclipselink workaround, see DATAJPA-433 70 | return upperIfIgnoreCase(getTypedPath(root, part)) 71 | .in((Expression>) provider.next(part, Collection.class).getExpression()); 72 | case STARTING_WITH: 73 | case ENDING_WITH: 74 | case CONTAINING: 75 | case NOT_CONTAINING: 76 | if (property.getLeafProperty().isCollection()) { 77 | 78 | Expression> propertyExpression = traversePath(root, property); 79 | ParameterExpression parameterExpression = provider.next(part).getExpression(); 80 | 81 | // Can't just call .not() in case of negation as EclipseLink chokes on that. 82 | return type.equals(NOT_CONTAINING) // 83 | ? isNotMember(builder, parameterExpression, propertyExpression) // 84 | : isMember(builder, parameterExpression, propertyExpression); 85 | } 86 | 87 | case LIKE: 88 | case NOT_LIKE: 89 | Expression stringPath = getTypedPath(root, part); 90 | Expression propertyExpression = upperIfIgnoreCase(stringPath); 91 | Expression parameterExpression = 92 | upperIfIgnoreCase(provider.next(part, String.class).getExpression()); 93 | Predicate like = 94 | builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter()); 95 | return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? like.not() : like; 96 | case TRUE: 97 | Expression truePath = getTypedPath(root, part); 98 | return builder.isTrue(truePath); 99 | case FALSE: 100 | Expression falsePath = getTypedPath(root, part); 101 | return builder.isFalse(falsePath); 102 | case SIMPLE_PROPERTY: 103 | ParameterMetadataProvider.ParameterMetadata expression = provider.next(part); 104 | Expression path = getTypedPath(root, part); 105 | return expression.isIsNullParameter() 106 | ? path.isNull() 107 | : builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression())); 108 | case NEGATING_SIMPLE_PROPERTY: 109 | return builder.notEqual( 110 | upperIfIgnoreCase(getTypedPath(root, part)), 111 | upperIfIgnoreCase(provider.next(part).getExpression())); 112 | case IS_EMPTY: 113 | case IS_NOT_EMPTY: 114 | if (!property.getLeafProperty().isCollection()) { 115 | throw new IllegalArgumentException( 116 | "IsEmpty / IsNotEmpty can only be used on collection properties"); 117 | } 118 | 119 | Expression> collectionPath = traversePath(root, property); 120 | return type.equals(IS_NOT_EMPTY) 121 | ? builder.isNotEmpty(collectionPath) 122 | : builder.isEmpty(collectionPath); 123 | 124 | default: 125 | throw new IllegalArgumentException("Unsupported keyword " + type); 126 | } 127 | } 128 | 129 | private Predicate isMember( 130 | CriteriaBuilder builder, Expression parameter, Expression> property) { 131 | return builder.isMember(parameter, property); 132 | } 133 | 134 | private Predicate isNotMember( 135 | CriteriaBuilder builder, Expression parameter, Expression> property) { 136 | return builder.isNotMember(parameter, property); 137 | } 138 | 139 | private Expression upperIfIgnoreCase(Expression expression) { 140 | switch (part.shouldIgnoreCase()) { 141 | case ALWAYS: 142 | Assert.state( 143 | canUpperCase(expression), 144 | "Unable to ignore case of " 145 | + expression.getJavaType().getName() 146 | + " types, the property '" 147 | + part.getProperty().getSegment() 148 | + "' must reference a String"); 149 | return (Expression) builder.upper((Expression) expression); 150 | 151 | case WHEN_POSSIBLE: 152 | if (canUpperCase(expression)) { 153 | return (Expression) builder.upper((Expression) expression); 154 | } 155 | 156 | case NEVER: 157 | default: 158 | return (Expression) expression; 159 | } 160 | } 161 | 162 | private boolean canUpperCase(Expression expression) { 163 | return String.class.equals(expression.getJavaType()); 164 | } 165 | 166 | private Expression getComparablePath(Root root, Part part) { 167 | return getTypedPath(root, part); 168 | } 169 | 170 | private Expression getTypedPath(Root root, Part part) { 171 | return toExpressionRecursively(root, part.getProperty()); 172 | } 173 | 174 | private Expression traversePath(Path root, PropertyPath path) { 175 | Path result = root.get(path.getSegment()); 176 | return (Expression) (path.hasNext() ? traversePath(result, path.next()) : result); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ProcedureParameter.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.ParameterMode; 4 | import java.util.Objects; 5 | import org.springframework.lang.Nullable; 6 | 7 | public class ProcedureParameter { 8 | 9 | private final String name; 10 | private final ParameterMode mode; 11 | private final Class type; 12 | 13 | ProcedureParameter(@Nullable String name, ParameterMode mode, Class type) { 14 | 15 | this.name = name; 16 | this.mode = mode; 17 | this.type = type; 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | public ParameterMode getMode() { 25 | return mode; 26 | } 27 | 28 | public Class getType() { 29 | return type; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | 35 | if (this == o) { 36 | return true; 37 | } 38 | 39 | if (!(o instanceof ProcedureParameter)) { 40 | return false; 41 | } 42 | 43 | ProcedureParameter that = (ProcedureParameter) o; 44 | return Objects.equals(name, that.name) && mode == that.mode && Objects.equals(type, that.type); 45 | } 46 | 47 | @Override 48 | public int hashCode() { 49 | return Objects.hash(name, mode, type); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "ProcedureParameter{" 55 | + "name='" 56 | + name 57 | + '\'' 58 | + ", mode=" 59 | + mode 60 | + ", type=" 61 | + type 62 | + '}'; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/QueryEnhancer.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.Set; 4 | import org.springframework.data.domain.Sort; 5 | import org.springframework.data.jpa.repository.query.QueryUtils; 6 | import org.springframework.lang.Nullable; 7 | 8 | /** 9 | * @author Bao.Ngo 10 | */ 11 | public interface QueryEnhancer { 12 | 13 | default String applySorting(Sort sort) { 14 | return applySorting(sort, detectAlias()); 15 | } 16 | 17 | String applySorting(Sort sort, @Nullable String alias); 18 | 19 | @Nullable 20 | String detectAlias(); 21 | 22 | default String createCountQueryFor() { 23 | return createCountQueryFor(null); 24 | } 25 | 26 | String createCountQueryFor(@Nullable String countProjection); 27 | 28 | default boolean hasConstructorExpression() { 29 | return QueryUtils.hasConstructorExpression(getQuery().getQueryString()); 30 | } 31 | 32 | String getProjection(); 33 | 34 | Set getJoinAliases(); 35 | 36 | DeclaredQuery getQuery(); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/QueryEnhancerFactory.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import org.apache.commons.logging.Log; 4 | import org.apache.commons.logging.LogFactory; 5 | import org.springframework.util.ClassUtils; 6 | 7 | public class QueryEnhancerFactory { 8 | 9 | private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class); 10 | 11 | private static final boolean jSqlParserPresent = 12 | ClassUtils.isPresent( 13 | "net.sf.jsqlparser.parser.JSqlParser", 14 | org.springframework.data.jpa.repository.query.QueryEnhancerFactory.class 15 | .getClassLoader()); 16 | 17 | static { 18 | if (jSqlParserPresent) { 19 | LOG.info("JSqlParser is in classpath; If applicable, JSqlParser will be used"); 20 | } 21 | } 22 | 23 | private QueryEnhancerFactory() {} 24 | 25 | public static QueryEnhancer forQuery(DeclaredQuery query) { 26 | // TODO 27 | if (query.isNativeQuery()) { 28 | return new DefaultQueryEnhancer(query); 29 | } 30 | 31 | try { 32 | return ReactiveJpaQueryEnhancer.forHql(query); 33 | } catch (Exception e) { 34 | throw new RuntimeException(e.getMessage(), e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/QueryParameterSetter.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.Parameter; 4 | import jakarta.persistence.Query; 5 | import jakarta.persistence.TemporalType; 6 | import jakarta.persistence.criteria.ParameterExpression; 7 | import java.lang.reflect.Proxy; 8 | import java.util.*; 9 | import org.apache.commons.logging.Log; 10 | import org.apache.commons.logging.LogFactory; 11 | import org.hibernate.reactive.stage.Stage; 12 | import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; 13 | import org.springframework.data.util.NullableWrapperConverters; 14 | import org.springframework.lang.Nullable; 15 | import org.springframework.util.Assert; 16 | import reactor.core.publisher.Mono; 17 | 18 | /** 19 | * @author Bao.Ngo 20 | */ 21 | public interface QueryParameterSetter { 22 | 23 | Mono setParameter( 24 | QueryParameterSetter.BindableQuery query, 25 | JpaParametersParameterAccessor accessor, 26 | QueryParameterSetter.ErrorHandling errorHandling); 27 | 28 | QueryParameterSetter NOOP = (query, values, errorHandling) -> Mono.empty(); 29 | 30 | class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter { 31 | 32 | // protected final boolean useIndexBasedParams; 33 | // protected final Function valueExtractor; 34 | protected final ParameterValueEvaluator valueEvaluator; 35 | protected final ParameterBinding binding; 36 | protected final Parameter parameter; 37 | protected final @Nullable TemporalType temporalType; 38 | 39 | NamedOrIndexedQueryParameterSetter( 40 | // Function valueExtractor, 41 | ParameterValueEvaluator valueEvaluator, 42 | ParameterBinding binding, 43 | Parameter parameter, 44 | @Nullable TemporalType temporalType) { 45 | Assert.notNull(valueEvaluator, "ValueEvaluator must not be null"); 46 | 47 | this.valueEvaluator = valueEvaluator; 48 | this.binding = binding; 49 | this.parameter = parameter; 50 | this.temporalType = temporalType; 51 | } 52 | 53 | @SuppressWarnings("unchecked") 54 | @Override 55 | public Mono setParameter( 56 | BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling) { 57 | if (temporalType != null) { 58 | return Mono.empty(); 59 | // TODO 60 | } else { 61 | 62 | return valueEvaluator 63 | .evaluate(accessor) 64 | .map(binding::prepare) 65 | .doOnNext( 66 | value -> { 67 | Object unwrapped; 68 | if (NullableWrapperConverters.supports(value.getClass())) { 69 | unwrapped = NullableWrapperConverters.unwrap(value); 70 | } else { 71 | unwrapped = value; 72 | } 73 | 74 | if (parameter instanceof ParameterExpression) { 75 | errorHandling.execute( 76 | () -> query.setParameter((Parameter) parameter, unwrapped)); 77 | } else if (parameter.getName() != null) { 78 | errorHandling.execute(() -> query.setParameter(parameter.getName(), unwrapped)); 79 | 80 | } else { 81 | Integer position = parameter.getPosition(); 82 | 83 | if (position != null) { 84 | errorHandling.execute(() -> query.setParameter(position, unwrapped)); 85 | } 86 | } 87 | }) 88 | .then(); 89 | 90 | // if (parameter instanceof ParameterExpression) { 91 | // return errorHandling.execute( 92 | // () -> query.setParameter((Parameter) parameter, value)); 93 | // } else if (parameter.getName() != null) { 94 | // return errorHandling.execute(() -> query.setParameter(parameter.getName(), 95 | // value)); 96 | // 97 | // } else { 98 | // Integer position = parameter.getPosition(); 99 | // 100 | // if (position != null) { 101 | // return errorHandling.execute(() -> query.setParameter(position, value)); 102 | // } 103 | // } 104 | 105 | // return Mono.error( 106 | // () -> new IllegalStateException("Illegal parameter " + parameter.getClass())); 107 | } 108 | } 109 | } 110 | 111 | enum ErrorHandling { 112 | STRICT { 113 | 114 | @Override 115 | public void execute(Runnable block) { 116 | block.run(); 117 | } 118 | }, 119 | 120 | LENIENT { 121 | 122 | @Override 123 | public void execute(Runnable block) { 124 | // return Mono.fromRunnable(block) 125 | // .onErrorResume( 126 | // RuntimeException.class, 127 | // rex -> { 128 | // LOG.info("Silently ignoring", rex); 129 | // return Mono.empty(); 130 | // }) 131 | // .then(); 132 | try { 133 | block.run(); 134 | } catch (RuntimeException rex) { 135 | LOG.info("Silently ignoring", rex); 136 | } 137 | } 138 | }; 139 | 140 | private static final Log LOG = LogFactory.getLog(QueryParameterSetter.ErrorHandling.class); 141 | 142 | abstract void execute(Runnable block); 143 | } 144 | 145 | class QueryMetadataCache { 146 | 147 | private Map cache = Collections.emptyMap(); 148 | 149 | public QueryParameterSetter.QueryMetadata getMetadata( 150 | String cacheKey, Stage.AbstractQuery query) { 151 | QueryParameterSetter.QueryMetadata queryMetadata = cache.get(cacheKey); 152 | 153 | if (queryMetadata == null) { 154 | queryMetadata = new QueryParameterSetter.QueryMetadata(query); 155 | 156 | Map cache; 157 | 158 | if (this.cache.isEmpty()) { 159 | cache = Collections.singletonMap(cacheKey, queryMetadata); 160 | } else { 161 | cache = new HashMap<>(this.cache); 162 | cache.put(cacheKey, queryMetadata); 163 | } 164 | 165 | synchronized (this) { 166 | this.cache = cache; 167 | } 168 | } 169 | 170 | return queryMetadata; 171 | } 172 | } 173 | 174 | class QueryMetadata { 175 | 176 | private final boolean namedParameters = false; 177 | private final Set> parameters = new HashSet<>(); 178 | private final boolean registerExcessParameters = false; 179 | 180 | QueryMetadata(Stage.AbstractQuery query) { 181 | 182 | /* this.namedParameters = QueryUtils.hasNamedParameter(query); 183 | this.parameters = query.getParameters(); 184 | 185 | // DATAJPA-1172 186 | // Since EclipseLink doesn't reliably report whether a query has parameters 187 | // we simply try to set the parameters and ignore possible failures. 188 | // this is relevant for native queries with SpEL expressions, where the method parameters don't have to match the 189 | // parameters in the query. 190 | // https://bugs.eclipse.org/bugs/show_bug.cgi?id=521915 191 | 192 | this.registerExcessParameters = query.getParameters().size() == 0 193 | && unwrapClass(query).getName().startsWith("org.eclipse");*/ 194 | } 195 | 196 | QueryMetadata(QueryParameterSetter.QueryMetadata metadata) { 197 | 198 | // this.namedParameters = metadata.namedParameters; 199 | // this.parameters = metadata.parameters; 200 | // this.registerExcessParameters = metadata.registerExcessParameters; 201 | } 202 | 203 | public QueryParameterSetter.BindableQuery withQuery(Stage.AbstractQuery query) { 204 | return new QueryParameterSetter.BindableQuery(this, query); 205 | } 206 | 207 | public Set> getParameters() { 208 | return parameters; 209 | } 210 | 211 | public boolean hasNamedParameters() { 212 | return this.namedParameters; 213 | } 214 | 215 | public boolean registerExcessParameters() { 216 | return this.registerExcessParameters; 217 | } 218 | 219 | private static Class unwrapClass(Query query) { 220 | 221 | Class queryType = query.getClass(); 222 | 223 | try { 224 | 225 | return Proxy.isProxyClass(queryType) // 226 | ? query.unwrap(null).getClass() // 227 | : queryType; 228 | 229 | } catch (RuntimeException e) { 230 | 231 | LogFactory.getLog(QueryParameterSetter.QueryMetadata.class) 232 | .warn("Failed to unwrap actual class for Query proxy", e); 233 | 234 | return queryType; 235 | } 236 | } 237 | } 238 | 239 | class BindableQuery extends QueryMetadata { 240 | 241 | protected final Stage.AbstractQuery query; 242 | 243 | // protected final Stage.AbstractQuery unwrapped; 244 | 245 | BindableQuery(QueryParameterSetter.QueryMetadata metadata, Stage.AbstractQuery query) { 246 | super(metadata); 247 | this.query = query; 248 | // this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query; 249 | } 250 | 251 | private BindableQuery(Stage.AbstractQuery query) { 252 | super(query); 253 | this.query = query; 254 | // this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query; 255 | } 256 | 257 | public static QueryParameterSetter.BindableQuery from(Stage.AbstractQuery query) { 258 | return new QueryParameterSetter.BindableQuery(query); 259 | } 260 | 261 | public Stage.AbstractQuery getQuery() { 262 | return query; 263 | } 264 | 265 | public Stage.AbstractQuery setParameter(Parameter param, T value) { 266 | return query.setParameter(param, value); 267 | } 268 | 269 | public Stage.AbstractQuery setParameter( 270 | Parameter param, Date value, TemporalType temporalType) { 271 | return query.setParameter(param, value /*, temporalType*/); 272 | } 273 | 274 | public Stage.AbstractQuery setParameter(String name, Object value) { 275 | return query.setParameter(name, value); 276 | } 277 | 278 | public Stage.AbstractQuery setParameter(String name, Date value, TemporalType temporalType) { 279 | return query.setParameter(name, value /*, temporalType*/); 280 | } 281 | 282 | public Stage.AbstractQuery setParameter(int position, Object value) { 283 | return query.setParameter(position, value); 284 | } 285 | 286 | public Stage.AbstractQuery setParameter(int position, Date value, TemporalType temporalType) { 287 | return query.setParameter(position, value /*, temporalType*/); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaCountQueryCreator.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.criteria.*; 4 | import org.springframework.data.domain.Sort; 5 | import org.springframework.data.repository.query.ReturnedType; 6 | import org.springframework.data.repository.query.parser.PartTree; 7 | import org.springframework.lang.Nullable; 8 | 9 | /** 10 | * @author Bao.Ngo 11 | */ 12 | public class ReactiveJpaCountQueryCreator extends ReactiveJpaCriteriaQueryCreator { 13 | 14 | protected final boolean distinct; 15 | 16 | public ReactiveJpaCountQueryCreator( 17 | PartTree tree, 18 | ReturnedType type, 19 | CriteriaBuilder builder, 20 | ParameterMetadataProvider provider) { 21 | super(tree, type, builder, provider); 22 | distinct = tree.isDistinct(); 23 | } 24 | 25 | @Override 26 | protected CriteriaQuery createCriteriaQuery( 27 | CriteriaBuilder builder, ReturnedType type) { 28 | return builder.createQuery(Long.class); 29 | } 30 | 31 | @Override 32 | @SuppressWarnings("unchecked") 33 | protected CriteriaQuery complete( 34 | @Nullable Predicate predicate, 35 | Sort sort, 36 | CriteriaQuery query, 37 | CriteriaBuilder builder, 38 | Root root) { 39 | 40 | CriteriaQuery select = query.select(getCountQuery(query, builder, root)); 41 | return predicate == null ? select : select.where(predicate); 42 | } 43 | 44 | @SuppressWarnings("rawtypes") 45 | private Expression getCountQuery(CriteriaQuery query, CriteriaBuilder builder, Root root) { 46 | return distinct ? builder.countDistinct(root) : builder.count(root); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaCriteriaDeleteQueryCreator.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import static org.springframework.data.repository.query.parser.Part.Type.*; 4 | 5 | import jakarta.persistence.criteria.*; 6 | import java.util.Collection; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import org.springframework.data.domain.Sort; 10 | import org.springframework.data.jpa.repository.query.EscapeCharacter; 11 | import org.springframework.data.repository.query.ReturnedType; 12 | import org.springframework.data.repository.query.parser.Part; 13 | import org.springframework.data.repository.query.parser.PartTree; 14 | import org.springframework.lang.Nullable; 15 | 16 | /** 17 | * @author Bao.Ngo 18 | */ 19 | public class ReactiveJpaCriteriaDeleteQueryCreator 20 | extends AbstractQueryCreator, Predicate> { 21 | 22 | protected final CriteriaBuilder builder; 23 | protected final Root root; 24 | protected final CriteriaDelete query; 25 | protected final ParameterMetadataProvider provider; 26 | protected final ReturnedType returnedType; 27 | protected final PartTree tree; 28 | protected final EscapeCharacter escape; 29 | 30 | public ReactiveJpaCriteriaDeleteQueryCreator( 31 | PartTree tree, 32 | ReturnedType type, 33 | CriteriaBuilder builder, 34 | ParameterMetadataProvider provider) { 35 | 36 | super(tree); 37 | this.tree = tree; 38 | 39 | CriteriaDelete criteriaDelete = createCriteriaQuery(builder, type); 40 | 41 | this.builder = builder; 42 | this.query = criteriaDelete; 43 | this.root = query.from((Class) type.getDomainType()); 44 | this.provider = provider; 45 | this.returnedType = type; 46 | this.escape = provider.getEscape(); 47 | } 48 | 49 | protected CriteriaDelete createCriteriaQuery( 50 | CriteriaBuilder builder, ReturnedType type) { 51 | Class typeToRead = type.getDomainType(); 52 | 53 | return builder.createCriteriaDelete(typeToRead); 54 | } 55 | 56 | public List> getParameterExpressions() { 57 | return provider.getExpressions(); 58 | } 59 | 60 | @Override 61 | protected Predicate create(Part part, Iterator iterator) { 62 | return toPredicate(part, root); 63 | } 64 | 65 | @Override 66 | protected Predicate and(Part part, Predicate base, Iterator iterator) { 67 | return builder.and(base, toPredicate(part, root)); 68 | } 69 | 70 | @Override 71 | protected Predicate or(Predicate base, Predicate predicate) { 72 | return builder.or(base, predicate); 73 | } 74 | 75 | @Override 76 | protected final CriteriaDelete complete(Predicate predicate, Sort sort) { 77 | return complete(predicate, sort, query, builder, root); 78 | } 79 | 80 | @SuppressWarnings({"unchecked", "rawtypes"}) 81 | protected CriteriaDelete complete( 82 | @Nullable Predicate predicate, 83 | Sort sort, 84 | CriteriaDelete query, 85 | CriteriaBuilder builder, 86 | Root root) { 87 | 88 | CriteriaDelete delete = query; 89 | return predicate == null ? delete : delete.where(predicate); 90 | } 91 | 92 | Collection getRequiredSelection(Sort sort, ReturnedType returnedType) { 93 | return returnedType.getInputProperties(); 94 | } 95 | 96 | private Predicate toPredicate(Part part, Root root) { 97 | return new PredicateBuilder(provider, builder, part, root, escape).build(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaCriteriaQueryCreator.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import static com.htech.data.jpa.reactive.repository.query.QueryUtils.toExpressionRecursively; 4 | import static org.springframework.data.repository.query.parser.Part.Type.*; 5 | 6 | import jakarta.persistence.criteria.*; 7 | import jakarta.persistence.metamodel.SingularAttribute; 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.Iterator; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | import org.springframework.data.domain.Sort; 14 | import org.springframework.data.jpa.repository.query.EscapeCharacter; 15 | import org.springframework.data.jpa.repository.query.QueryUtils; 16 | import org.springframework.data.mapping.PropertyPath; 17 | import org.springframework.data.repository.query.ReturnedType; 18 | import org.springframework.data.repository.query.parser.Part; 19 | import org.springframework.data.repository.query.parser.PartTree; 20 | import org.springframework.lang.Nullable; 21 | 22 | /** 23 | * @author Bao.Ngo 24 | */ 25 | public class ReactiveJpaCriteriaQueryCreator 26 | extends AbstractQueryCreator, Predicate> { 27 | 28 | protected final CriteriaBuilder builder; 29 | protected final Root root; 30 | protected final CriteriaQuery query; 31 | protected final ParameterMetadataProvider provider; 32 | protected final ReturnedType returnedType; 33 | protected final PartTree tree; 34 | protected final EscapeCharacter escape; 35 | 36 | public ReactiveJpaCriteriaQueryCreator( 37 | PartTree tree, 38 | ReturnedType type, 39 | CriteriaBuilder builder, 40 | ParameterMetadataProvider provider) { 41 | super(tree); 42 | this.tree = tree; 43 | 44 | CriteriaQuery criteriaQuery = createCriteriaQuery(builder, type); 45 | 46 | this.builder = builder; 47 | this.query = criteriaQuery.distinct(tree.isDistinct() && !tree.isCountProjection()); 48 | this.root = query.from(type.getDomainType()); 49 | this.provider = provider; 50 | this.returnedType = type; 51 | this.escape = provider.getEscape(); 52 | } 53 | 54 | protected CriteriaQuery createCriteriaQuery( 55 | CriteriaBuilder builder, ReturnedType type) { 56 | Class typeToRead = tree.isDelete() ? type.getDomainType() : type.getTypeToRead(); 57 | 58 | return (typeToRead == null) || tree.isExistsProjection() // 59 | ? builder.createTupleQuery() // 60 | : builder.createQuery(typeToRead); 61 | } 62 | 63 | public List> getParameterExpressions() { 64 | return provider.getExpressions(); 65 | } 66 | 67 | @Override 68 | protected Predicate create(Part part, Iterator iterator) { 69 | return toPredicate(part, root); 70 | } 71 | 72 | @Override 73 | protected Predicate and(Part part, Predicate base, Iterator iterator) { 74 | return builder.and(base, toPredicate(part, root)); 75 | } 76 | 77 | @Override 78 | protected Predicate or(Predicate base, Predicate predicate) { 79 | return builder.or(base, predicate); 80 | } 81 | 82 | @Override 83 | protected final CriteriaQuery complete(Predicate predicate, Sort sort) { 84 | return complete(predicate, sort, query, builder, root); 85 | } 86 | 87 | @SuppressWarnings({"unchecked", "rawtypes"}) 88 | protected CriteriaQuery complete( 89 | @Nullable Predicate predicate, 90 | Sort sort, 91 | CriteriaQuery query, 92 | CriteriaBuilder builder, 93 | Root root) { 94 | if (returnedType.needsCustomConstruction()) { 95 | Collection requiredSelection = getRequiredSelection(sort, returnedType); 96 | List> selections = new ArrayList<>(); 97 | 98 | for (String property : requiredSelection) { 99 | PropertyPath path = PropertyPath.from(property, returnedType.getDomainType()); 100 | selections.add(toExpressionRecursively(root, path, true).alias(property)); 101 | } 102 | 103 | Class typeToRead = returnedType.getReturnedType(); 104 | query = 105 | typeToRead.isInterface() // 106 | ? query.multiselect(selections) // 107 | : query.select( 108 | (Selection) 109 | builder.construct( 110 | typeToRead, // 111 | selections.toArray(new Selection[0]))); 112 | 113 | } else if (tree.isExistsProjection()) { 114 | if (root.getModel().hasSingleIdAttribute()) { 115 | SingularAttribute id = 116 | root.getModel().getId(root.getModel().getIdType().getJavaType()); 117 | query = query.multiselect(root.get((SingularAttribute) id).alias(id.getName())); 118 | 119 | } else { 120 | query = 121 | query.multiselect( 122 | root.getModel().getIdClassAttributes().stream() // 123 | .map(it -> (Selection) root.get((SingularAttribute) it).alias(it.getName())) 124 | .collect(Collectors.toList())); 125 | } 126 | 127 | } else { 128 | query = query.select((Root) root); 129 | } 130 | 131 | CriteriaQuery select = 132 | query.orderBy(QueryUtils.toOrders(sort, root, builder)); 133 | 134 | return predicate == null ? select : select.where(predicate); 135 | } 136 | 137 | Collection getRequiredSelection(Sort sort, ReturnedType returnedType) { 138 | return returnedType.getInputProperties(); 139 | } 140 | 141 | private Predicate toPredicate(Part part, Root root) { 142 | return new PredicateBuilder(provider, builder, part, root, escape).build(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaParameters.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.TemporalType; 4 | import java.lang.reflect.Method; 5 | import java.util.Date; 6 | import java.util.List; 7 | import org.springframework.core.MethodParameter; 8 | import org.springframework.data.jpa.repository.Temporal; 9 | import org.springframework.data.repository.query.Parameter; 10 | import org.springframework.data.repository.query.Parameters; 11 | import org.springframework.data.util.TypeInformation; 12 | import org.springframework.lang.Nullable; 13 | 14 | public class ReactiveJpaParameters 15 | extends Parameters { 16 | 17 | public ReactiveJpaParameters(Method method) { 18 | super(method, null); 19 | } 20 | 21 | private ReactiveJpaParameters(List parameters) { 22 | super(parameters); 23 | } 24 | 25 | @Override 26 | protected ReactiveJpaParameters.JpaParameter createParameter(MethodParameter parameter) { 27 | return new ReactiveJpaParameters.JpaParameter(parameter); 28 | } 29 | 30 | @Override 31 | protected ReactiveJpaParameters createFrom(List parameters) { 32 | return new ReactiveJpaParameters(parameters); 33 | } 34 | 35 | public boolean hasLimitingParameters() { 36 | return hasLimitParameter() || hasPageableParameter(); 37 | } 38 | 39 | public static class JpaParameter extends Parameter { 40 | 41 | private final @Nullable Temporal annotation; 42 | private @Nullable TemporalType temporalType; 43 | 44 | protected JpaParameter(MethodParameter parameter) { 45 | super(parameter, TypeInformation.of(Parameter.class)); 46 | 47 | this.annotation = parameter.getParameterAnnotation(Temporal.class); 48 | this.temporalType = null; 49 | 50 | if (!isDateParameter() && hasTemporalParamAnnotation()) { 51 | throw new IllegalArgumentException( 52 | Temporal.class.getSimpleName() + " annotation is only allowed on Date parameter"); 53 | } 54 | } 55 | 56 | @Override 57 | public boolean isBindable() { 58 | return super.isBindable() || isTemporalParameter(); 59 | } 60 | 61 | boolean isTemporalParameter() { 62 | return isDateParameter() && hasTemporalParamAnnotation(); 63 | } 64 | 65 | @Nullable 66 | TemporalType getTemporalType() { 67 | if (temporalType == null) { 68 | this.temporalType = annotation == null ? null : annotation.value(); 69 | } 70 | 71 | return this.temporalType; 72 | } 73 | 74 | TemporalType getRequiredTemporalType() throws IllegalStateException { 75 | TemporalType temporalType = getTemporalType(); 76 | 77 | if (temporalType != null) { 78 | return temporalType; 79 | } 80 | 81 | throw new IllegalStateException( 82 | String.format("Required temporal type not found for %s", getType())); 83 | } 84 | 85 | private boolean hasTemporalParamAnnotation() { 86 | return annotation != null; 87 | } 88 | 89 | private boolean isDateParameter() { 90 | return getType().equals(Date.class); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaParametersParameterAccessor.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import org.hibernate.reactive.stage.Stage; 9 | import org.reactivestreams.Publisher; 10 | import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; 11 | import org.springframework.data.repository.query.Parameters; 12 | import org.springframework.data.repository.util.ReactiveWrapperConverters; 13 | import org.springframework.data.util.ReactiveWrappers; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | /** 18 | * @author Bao.Ngo 19 | */ 20 | public class ReactiveJpaParametersParameterAccessor extends JpaParametersParameterAccessor { 21 | 22 | protected final ReactiveJpaQueryMethod method; 23 | protected final Object[] values; 24 | protected final Stage.SessionFactory sessionFactory; 25 | 26 | public ReactiveJpaParametersParameterAccessor( 27 | ReactiveJpaQueryMethod method, 28 | Parameters parameters, 29 | Object[] values, 30 | Stage.SessionFactory sessionFactory /*, 31 | Mono session, 32 | Stage.Session session, 33 | Stage.Transaction transaction*/) { 34 | super(parameters, values); 35 | this.method = method; 36 | this.values = values; 37 | this.sessionFactory = sessionFactory; 38 | } 39 | 40 | public Stage.SessionFactory getSessionFactory() { 41 | return sessionFactory; 42 | } 43 | 44 | // public Mono getSession() { 45 | // return session; 46 | // } 47 | 48 | // public Stage.Transaction getTransaction() { 49 | // return transaction; 50 | // } 51 | @SuppressWarnings("unchecked") 52 | public Mono resolveParameters() { 53 | 54 | boolean hasReactiveWrapper = false; 55 | 56 | for (Object value : values) { 57 | if (value == null || !ReactiveWrappers.supports(value.getClass())) { 58 | continue; 59 | } 60 | 61 | hasReactiveWrapper = true; 62 | break; 63 | } 64 | 65 | if (!hasReactiveWrapper) { 66 | return Mono.just(this); 67 | } 68 | 69 | Object[] resolved = new Object[values.length]; 70 | Map> holder = new ConcurrentHashMap<>(); 71 | List> publishers = new ArrayList<>(); 72 | 73 | for (int i = 0; i < values.length; i++) { 74 | 75 | Object value = resolved[i] = values[i]; 76 | if (value == null || !ReactiveWrappers.supports(value.getClass())) { 77 | continue; 78 | } 79 | 80 | if (ReactiveWrappers.isSingleValueType(value.getClass())) { 81 | 82 | int index = i; 83 | publishers.add( 84 | ReactiveWrapperConverters.toWrapper(value, Mono.class) // 85 | .map(Optional::of) // 86 | .defaultIfEmpty(Optional.empty()) // 87 | .doOnNext(it -> holder.put(index, (Optional) it))); 88 | } else { 89 | 90 | int index = i; 91 | publishers.add( 92 | ReactiveWrapperConverters.toWrapper(value, Flux.class) // 93 | .collectList() // 94 | .doOnNext(it -> holder.put(index, Optional.of(it)))); 95 | } 96 | } 97 | 98 | return Flux.merge(publishers) 99 | .then() 100 | .thenReturn(resolved) 101 | .map( 102 | values -> { 103 | holder.forEach((index, v) -> values[index] = v.orElse(null)); 104 | return new ReactiveJpaParametersParameterAccessor( 105 | method, getParameters(), values, sessionFactory); 106 | }); 107 | } 108 | 109 | public Parameters getBindableParameters() { 110 | return getParameters().getBindableParameters(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaQueryEnhancer.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Method; 5 | import java.util.Set; 6 | import org.springframework.data.domain.Sort; 7 | import org.springframework.lang.Nullable; 8 | import org.springframework.util.Assert; 9 | import org.springframework.util.ReflectionUtils; 10 | 11 | /** 12 | * @author Bao.Ngo 13 | */ 14 | public class ReactiveJpaQueryEnhancer implements QueryEnhancer { 15 | 16 | protected final DeclaredQuery query; 17 | protected final Object queryParser; 18 | 19 | private ReactiveJpaQueryEnhancer(DeclaredQuery query, Object queryParser) { 20 | this.query = query; 21 | this.queryParser = queryParser; 22 | } 23 | 24 | public static ReactiveJpaQueryEnhancer forJpql(DeclaredQuery query) throws Exception { 25 | Assert.notNull(query, "DeclaredQuery must not be null!"); 26 | 27 | Class clazz = Class.forName("org.springframework.data.jpa.repository.query.JpqlQueryParser"); 28 | Constructor constructor = ReflectionUtils.accessibleConstructor(clazz, String.class); 29 | return new ReactiveJpaQueryEnhancer(query, constructor.newInstance(query.getQueryString())); 30 | } 31 | 32 | public static ReactiveJpaQueryEnhancer forHql(DeclaredQuery query) throws Exception { 33 | Assert.notNull(query, "DeclaredQuery must not be null!"); 34 | 35 | Class clazz = Class.forName("org.springframework.data.jpa.repository.query.HqlQueryParser"); 36 | Constructor constructor = ReflectionUtils.accessibleConstructor(clazz, String.class); 37 | return new ReactiveJpaQueryEnhancer(query, constructor.newInstance(query.getQueryString())); 38 | } 39 | 40 | /*public static ReactiveJpaQueryEnhancer forEql(DeclaredQuery query) { 41 | 42 | Assert.notNull(query, "DeclaredQuery must not be null!"); 43 | 44 | return new ReactiveJpaQueryEnhancer(query, new EqlQueryParser(query.getQueryString())); 45 | }*/ 46 | 47 | protected Object getQueryParsingStrategy() { 48 | return queryParser; 49 | } 50 | 51 | @Override 52 | public String applySorting(Sort sort) { 53 | return invokeQueryParser("renderSortedQuery", new Class[] {Sort.class}, sort); 54 | } 55 | 56 | private T invokeQueryParser( 57 | String methodName, Class[] methodParamTypes, Object... params) { 58 | try { 59 | Method method; 60 | if (methodParamTypes == null || methodParamTypes.length == 0) { 61 | method = ReflectionUtils.findMethod(queryParser.getClass(), methodName); 62 | } else { 63 | method = ReflectionUtils.findMethod(queryParser.getClass(), methodName, methodParamTypes); 64 | } 65 | ReflectionUtils.makeAccessible(method); 66 | 67 | if (params == null || params.length == 0) { 68 | return (T) method.invoke(queryParser); 69 | } 70 | 71 | return (T) method.invoke(queryParser, params); 72 | } catch (Exception e) { 73 | throw new RuntimeException(e.getMessage(), e); 74 | } 75 | } 76 | 77 | @Override 78 | public String applySorting(Sort sort, String alias) { 79 | return applySorting(sort); 80 | } 81 | 82 | @Override 83 | public String detectAlias() { 84 | return invokeQueryParser("findAlias", null); 85 | // try { 86 | // Method method = ReflectionUtils.findMethod(queryParser.getClass(), "findAlias"); 87 | // ReflectionUtils.makeAccessible(method); 88 | // return (String) method.invoke(queryParser); 89 | // } catch (Exception e) { 90 | // throw new RuntimeException(e.getMessage(), e); 91 | // } 92 | } 93 | 94 | @Override 95 | public String createCountQueryFor() { 96 | return createCountQueryFor(null); 97 | } 98 | 99 | @Override 100 | public String createCountQueryFor(@Nullable String countProjection) { 101 | return invokeQueryParser("createCountQuery", new Class[] {String.class}, countProjection); 102 | } 103 | 104 | @Override 105 | public boolean hasConstructorExpression() { 106 | return invokeQueryParser("hasConstructorExpression", null); 107 | } 108 | 109 | @Override 110 | public String getProjection() { 111 | return invokeQueryParser("projection", null); 112 | } 113 | 114 | @Override 115 | public Set getJoinAliases() { 116 | return Set.of(); 117 | } 118 | 119 | @Override 120 | public DeclaredQuery getQuery() { 121 | return query; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaQueryExecutionConverters.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import io.smallrye.mutiny.Multi; 4 | import io.smallrye.mutiny.Uni; 5 | import java.util.*; 6 | import java.util.function.Function; 7 | import java.util.stream.Stream; 8 | import org.springframework.core.convert.TypeDescriptor; 9 | import org.springframework.core.convert.converter.GenericConverter; 10 | import org.springframework.core.convert.support.GenericConversionService; 11 | import org.springframework.data.repository.util.QueryExecutionConverters; 12 | import org.springframework.data.util.NullableWrapperConverters; 13 | import org.springframework.data.util.TypeInformation; 14 | import org.springframework.lang.Nullable; 15 | import org.springframework.util.Assert; 16 | import org.springframework.util.ConcurrentReferenceHashMap; 17 | import org.springframework.util.ObjectUtils; 18 | import reactor.core.publisher.Flux; 19 | import reactor.core.publisher.Mono; 20 | 21 | /** 22 | * @author Bao.Ngo 23 | */ 24 | public class ReactiveJpaQueryExecutionConverters { 25 | 26 | private static final Set WRAPPER_TYPES = new HashSet<>(); 27 | private static final Set UN_WRAPPER_TYPES = new HashSet<>(); 28 | private static final Set> UNWRAPPERS = new HashSet<>(); 29 | private static final Set> ALLOWED_PAGEABLE_TYPES = new HashSet<>(); 30 | private static final Map, QueryExecutionConverters.ExecutionAdapter> EXECUTION_ADAPTER = 31 | new HashMap<>(); 32 | private static final Map, Boolean> supportsCache = new ConcurrentReferenceHashMap<>(); 33 | private static final TypeInformation VOID_INFORMATION = TypeInformation.of(Void.class); 34 | 35 | private static final GenericConversionService CONVERSION_SERVICE = new GenericConversionService(); 36 | 37 | static { 38 | CONVERSION_SERVICE.addConverter(new ReactiveFlatteningConverter()); 39 | WRAPPER_TYPES.add(WrapperType.singleValue(Mono.class)); 40 | WRAPPER_TYPES.add(WrapperType.singleValue(Uni.class)); 41 | // 42 | // WRAPPER_TYPES.add(QueryExecutionConverters.WrapperType.singleValue(CompletableFuture.class)); 43 | 44 | UN_WRAPPER_TYPES.add(WrapperType.multiValue(Flux.class)); 45 | UN_WRAPPER_TYPES.add(WrapperType.multiValue(Uni.class)); 46 | // 47 | // UNWRAPPER_TYPES.add(QueryExecutionConverters.WrapperType.singleValue(CompletableFuture.class)); 48 | 49 | // ALLOWED_PAGEABLE_TYPES.add(Slice.class); 50 | // ALLOWED_PAGEABLE_TYPES.add(Page.class); 51 | // ALLOWED_PAGEABLE_TYPES.add(List.class); 52 | // ALLOWED_PAGEABLE_TYPES.add(Window.class); 53 | 54 | // 55 | // WRAPPER_TYPES.add(QueryExecutionConverters.NullableWrapperToCompletableFutureConverter.getWrapperType()); 56 | 57 | // UNWRAPPERS.addAll(CustomCollections.getUnwrappers()); 58 | 59 | // 60 | // CustomCollections.getCustomTypes().stream().map(QueryExecutionConverters.WrapperType::multiValue).forEach(WRAPPER_TYPES::add); 61 | 62 | // ALLOWED_PAGEABLE_TYPES.addAll(CustomCollections.getPaginationReturnTypes()); 63 | 64 | } 65 | 66 | private ReactiveJpaQueryExecutionConverters() {} 67 | 68 | public static boolean supports(Class type) { 69 | 70 | Assert.notNull(type, "Type must not be null"); 71 | 72 | return supportsCache.computeIfAbsent( 73 | type, 74 | key -> { 75 | for (WrapperType candidate : WRAPPER_TYPES) { 76 | if (candidate.getType().isAssignableFrom(key)) { 77 | return true; 78 | } 79 | } 80 | 81 | return NullableWrapperConverters.supports(type); 82 | }); 83 | } 84 | 85 | public static boolean isSingleValue(Class type) { 86 | // if (NullableWrapperConverters.supports(type)) { 87 | // return NullableWrapperConverters.isSingleValue(type); 88 | // } 89 | 90 | for (WrapperType candidate : WRAPPER_TYPES) { 91 | if (candidate.getType().isAssignableFrom(type)) { 92 | return candidate.isSingleValue(); 93 | } 94 | } 95 | 96 | return false; 97 | } 98 | 99 | public static GenericConversionService getDefaultConversionService() { 100 | return CONVERSION_SERVICE; 101 | } 102 | 103 | public static final class WrapperType { 104 | 105 | private WrapperType(Class type, WrapperType.Cardinality cardinality) { 106 | this.type = type; 107 | this.cardinality = cardinality; 108 | } 109 | 110 | public Class getType() { 111 | return this.type; 112 | } 113 | 114 | public WrapperType.Cardinality getCardinality() { 115 | return cardinality; 116 | } 117 | 118 | @Override 119 | public boolean equals(@Nullable Object o) { 120 | 121 | if (this == o) { 122 | return true; 123 | } 124 | 125 | if (!(o instanceof WrapperType that)) { 126 | return false; 127 | } 128 | 129 | if (!ObjectUtils.nullSafeEquals(type, that.type)) { 130 | return false; 131 | } 132 | 133 | return cardinality == that.cardinality; 134 | } 135 | 136 | @Override 137 | public int hashCode() { 138 | int result = ObjectUtils.nullSafeHashCode(type); 139 | result = 31 * result + ObjectUtils.nullSafeHashCode(cardinality); 140 | return result; 141 | } 142 | 143 | @Override 144 | public String toString() { 145 | return "QueryExecutionConverters.WrapperType(type=" 146 | + this.getType() 147 | + ", cardinality=" 148 | + this.getCardinality() 149 | + ")"; 150 | } 151 | 152 | enum Cardinality { 153 | NONE, 154 | SINGLE, 155 | MULTI; 156 | } 157 | 158 | private final Class type; 159 | private final WrapperType.Cardinality cardinality; 160 | 161 | public static WrapperType singleValue(Class type) { 162 | return new WrapperType(type, WrapperType.Cardinality.SINGLE); 163 | } 164 | 165 | public static WrapperType multiValue(Class type) { 166 | return new WrapperType(type, WrapperType.Cardinality.MULTI); 167 | } 168 | 169 | public static WrapperType noValue(Class type) { 170 | return new WrapperType(type, WrapperType.Cardinality.NONE); 171 | } 172 | 173 | boolean isSingleValue() { 174 | return cardinality.equals(WrapperType.Cardinality.SINGLE); 175 | } 176 | } 177 | 178 | static class ReactiveFlatteningConverter implements GenericConverter { 179 | 180 | @Override 181 | public Set getConvertibleTypes() { 182 | return Set.of( 183 | /*new ConvertiblePair(Mono.class, Mono.class), */ new ConvertiblePair( 184 | Flux.class, Flux.class), 185 | /*new ConvertiblePair(Uni.class, Uni.class), */ new ConvertiblePair( 186 | Multi.class, Multi.class)); 187 | } 188 | 189 | @Override 190 | public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { 191 | if (source instanceof Flux flux) { 192 | return flux.flatMap( 193 | o -> { 194 | if (o instanceof Iterable i) { 195 | return Flux.fromIterable(i); 196 | } 197 | if (o instanceof Stream stream) { 198 | return Flux.fromStream(stream); 199 | } 200 | 201 | return Mono.just(o); 202 | }); 203 | } 204 | 205 | if (source instanceof Multi multi) { 206 | return multi.flatMap( 207 | o -> { 208 | if (o instanceof Iterable i) { 209 | return Multi.createFrom().iterable(i); 210 | } 211 | if (o instanceof Stream stream) { 212 | return Multi.createFrom().items(stream); 213 | } 214 | 215 | return Multi.createFrom().items(o); 216 | }); 217 | } 218 | 219 | return source; 220 | } 221 | } 222 | 223 | // static class UniFlatteningConverter extends ReactiveFlatteningConverter { 224 | // 225 | // } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaQueryExtractor.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import org.hibernate.reactive.stage.Stage; 4 | 5 | public interface ReactiveJpaQueryExtractor { 6 | String extractQueryString(Stage.AbstractQuery query); 7 | 8 | boolean canExtractQuery(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaQueryFactory.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import org.hibernate.reactive.stage.Stage; 4 | import org.springframework.data.jpa.repository.QueryRewriter; 5 | import org.springframework.data.repository.query.QueryCreationException; 6 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; 7 | import org.springframework.expression.spel.standard.SpelExpressionParser; 8 | import org.springframework.lang.Nullable; 9 | 10 | public enum ReactiveJpaQueryFactory { 11 | INSTANCE; 12 | 13 | private static final SpelExpressionParser PARSER = new SpelExpressionParser(); 14 | 15 | AbstractReactiveJpaQuery fromMethodWithQueryString( 16 | ReactiveJpaQueryMethod method, 17 | Stage.SessionFactory sessionFactory, 18 | String queryString, 19 | @Nullable String countQueryString, 20 | QueryRewriter queryRewriter, 21 | ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { 22 | 23 | if (method.isScrollQuery()) { 24 | throw QueryCreationException.create( 25 | method, "Scroll queries are not supported using String-based queries"); 26 | } 27 | 28 | return method.isNativeQuery() 29 | ? new NativeReactiveJpaQuery( 30 | method, 31 | sessionFactory, 32 | queryString, 33 | countQueryString, 34 | queryRewriter, 35 | evaluationContextProvider, 36 | PARSER) 37 | : new SimpleReactiveJpaQuery( 38 | method, 39 | sessionFactory, 40 | queryString, 41 | countQueryString, 42 | queryRewriter, 43 | evaluationContextProvider, 44 | PARSER); 45 | } 46 | 47 | // TODO 48 | /* public StoredProcedureJpaQuery fromProcedureAnnotation(JpaQueryMethod method, EntityManager em) { 49 | 50 | if (method.isScrollQuery()) { 51 | throw QueryCreationException.create(method, "Scroll queries are not supported using stored procedures"); 52 | } 53 | 54 | return new StoredProcedureJpaQuery(method, em); 55 | }*/ 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveJpaQueryMethodFactory.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.lang.reflect.Method; 4 | import org.springframework.data.jpa.repository.query.JpaQueryMethod; 5 | import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory; 6 | import org.springframework.data.projection.ProjectionFactory; 7 | import org.springframework.data.repository.core.RepositoryMetadata; 8 | 9 | /** 10 | * @author Bao.Ngo 11 | */ 12 | public class ReactiveJpaQueryMethodFactory implements JpaQueryMethodFactory { 13 | 14 | private final ReactiveJpaQueryExtractor extractor; 15 | 16 | public ReactiveJpaQueryMethodFactory(ReactiveJpaQueryExtractor extractor) { 17 | // Assert.notNull(extractor, "QueryExtractor must not be null"); 18 | this.extractor = extractor; 19 | } 20 | 21 | public ReactiveJpaQueryMethod build0( 22 | Method method, RepositoryMetadata metadata, ProjectionFactory factory) { 23 | return new ReactiveJpaQueryMethod(method, metadata, factory, extractor); 24 | } 25 | 26 | @Override 27 | public JpaQueryMethod build( 28 | Method method, RepositoryMetadata metadata, ProjectionFactory factory) { 29 | throw new UnsupportedOperationException(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/ReactiveQueryRewriterProvider.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import org.springframework.beans.BeanUtils; 4 | import org.springframework.data.jpa.repository.QueryRewriter; 5 | 6 | public interface ReactiveQueryRewriterProvider { 7 | 8 | static ReactiveQueryRewriterProvider simple() { 9 | 10 | return method -> { 11 | Class queryRewriter = method.getQueryRewriter(); 12 | 13 | if (queryRewriter == QueryRewriter.IdentityQueryRewriter.class) { 14 | return QueryRewriter.IdentityQueryRewriter.INSTANCE; 15 | } 16 | 17 | return BeanUtils.instantiateClass(queryRewriter); 18 | }; 19 | } 20 | 21 | QueryRewriter getQueryRewriter(ReactiveJpaQueryMethod method); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/SimpleReactiveJpaQuery.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import org.hibernate.reactive.stage.Stage; 4 | import org.springframework.data.jpa.repository.QueryRewriter; 5 | import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; 6 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; 7 | import org.springframework.expression.spel.standard.SpelExpressionParser; 8 | import org.springframework.lang.Nullable; 9 | 10 | public class SimpleReactiveJpaQuery extends AbstractStringBasedReactiveJpaQuery { 11 | 12 | public SimpleReactiveJpaQuery( 13 | ReactiveJpaQueryMethod method, 14 | Stage.SessionFactory sessionFactory, 15 | @Nullable String countQueryString, 16 | QueryRewriter queryRewriter, 17 | ReactiveExtensionAwareQueryMethodEvaluationContextProvider evaluationContextProvider, 18 | SpelExpressionParser parser) { 19 | this( 20 | method, 21 | sessionFactory, 22 | method.getRequiredAnnotatedQuery(), 23 | countQueryString, 24 | queryRewriter, 25 | evaluationContextProvider, 26 | parser); 27 | } 28 | 29 | public SimpleReactiveJpaQuery( 30 | ReactiveJpaQueryMethod method, 31 | Stage.SessionFactory sessionFactory, 32 | String queryString, 33 | @Nullable String countQueryString, 34 | QueryRewriter queryRewriter, 35 | ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, 36 | SpelExpressionParser parser) { 37 | 38 | super( 39 | method, 40 | sessionFactory, 41 | queryString, 42 | countQueryString, 43 | queryRewriter, 44 | evaluationContextProvider, 45 | parser); 46 | 47 | // TODO 48 | // validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s", 49 | // method); 50 | 51 | /*if (method.isPageQuery()) { 52 | validateQuery(getCountQuery().getQueryString(), 53 | String.format("Count query validation failed for method %s", method)); 54 | }*/ 55 | } 56 | 57 | private void validateQuery(String query, String errorMessage, Object... arguments) { 58 | 59 | if (getQueryMethod().isProcedureQuery()) { 60 | return; 61 | } 62 | 63 | try { 64 | 65 | } catch (RuntimeException e) { 66 | // Needed as there's ambiguities in how an invalid query string shall be expressed by the 67 | // persistence provider 68 | // https://java.net/projects/jpa-spec/lists/jsr338-experts/archive/2012-07/message/17 69 | throw new IllegalArgumentException(String.format(errorMessage, arguments), e); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/SpELParameterValueEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.Optional; 4 | import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; 5 | import org.springframework.data.repository.query.Parameters; 6 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; 7 | import org.springframework.expression.Expression; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * @author Bao.Ngo 12 | */ 13 | public class SpELParameterValueEvaluator implements ParameterValueEvaluator { 14 | 15 | private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider; 16 | private final Parameters parameters; 17 | private final Expression expression; 18 | 19 | public SpELParameterValueEvaluator( 20 | ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, 21 | Parameters parameters, 22 | Expression expression) { 23 | this.evaluationContextProvider = evaluationContextProvider; 24 | this.parameters = parameters; 25 | this.expression = expression; 26 | } 27 | 28 | @Override 29 | public Mono> evaluate(JpaParametersParameterAccessor accessor) { 30 | return evaluationContextProvider 31 | .getEvaluationContextLater(parameters, accessor.getValues()) 32 | .map(context -> Optional.ofNullable(expression.getValue(context, Object.class))); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/StoredProcedureAttributeSource.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import jakarta.persistence.NamedStoredProcedureQueries; 4 | import jakarta.persistence.NamedStoredProcedureQuery; 5 | import jakarta.persistence.ParameterMode; 6 | import jakarta.persistence.StoredProcedureParameter; 7 | import java.lang.reflect.Method; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | import org.springframework.core.annotation.AnnotatedElementUtils; 14 | import org.springframework.data.jpa.repository.query.JpaEntityMetadata; 15 | import org.springframework.data.jpa.repository.query.Procedure; 16 | import org.springframework.lang.Nullable; 17 | import org.springframework.util.Assert; 18 | import org.springframework.util.ObjectUtils; 19 | import org.springframework.util.StringUtils; 20 | 21 | public enum StoredProcedureAttributeSource { 22 | INSTANCE; 23 | 24 | public StoredProcedureAttributes createFrom(Method method, JpaEntityMetadata entityMetadata) { 25 | 26 | Assert.notNull(method, "Method must not be null"); 27 | Assert.notNull(entityMetadata, "EntityMetadata must not be null"); 28 | 29 | Procedure procedure = AnnotatedElementUtils.findMergedAnnotation(method, Procedure.class); 30 | Assert.notNull(procedure, "Method must have an @Procedure annotation"); 31 | 32 | NamedStoredProcedureQuery namedStoredProc = 33 | tryFindAnnotatedNamedStoredProcedureQuery(method, entityMetadata, procedure); 34 | 35 | if (namedStoredProc != null) { 36 | return newProcedureAttributesFrom(method, namedStoredProc, procedure); 37 | } 38 | 39 | String procedureName = deriveProcedureNameFrom(method, procedure); 40 | if (ObjectUtils.isEmpty(procedureName)) { 41 | throw new IllegalArgumentException( 42 | "Could not determine name of procedure for @Procedure annotated method: " + method); 43 | } 44 | 45 | return new StoredProcedureAttributes( 46 | procedureName, createOutputProcedureParameterFrom(method, procedure)); 47 | } 48 | 49 | private String deriveProcedureNameFrom(Method method, Procedure procedure) { 50 | 51 | if (StringUtils.hasText(procedure.value())) { 52 | return procedure.value(); 53 | } 54 | 55 | String procedureName = procedure.procedureName(); 56 | return StringUtils.hasText(procedureName) ? procedureName : method.getName(); 57 | } 58 | 59 | private StoredProcedureAttributes newProcedureAttributesFrom( 60 | Method method, NamedStoredProcedureQuery namedStoredProc, Procedure procedure) { 61 | 62 | List outputParameters; 63 | 64 | if (!procedure.outputParameterName().isEmpty()) { 65 | 66 | // we give the output parameter definition from the @Procedure annotation precedence 67 | outputParameters = 68 | Collections.singletonList(createOutputProcedureParameterFrom(method, procedure)); 69 | } else { 70 | 71 | // try to discover the output parameter 72 | outputParameters = 73 | extractOutputParametersFrom(namedStoredProc).stream() // 74 | .map( 75 | namedParameter -> 76 | new ProcedureParameter( 77 | namedParameter.name(), namedParameter.mode(), namedParameter.type())) // 78 | .collect(Collectors.toList()); 79 | } 80 | 81 | return new StoredProcedureAttributes(namedStoredProc.name(), outputParameters, true); 82 | } 83 | 84 | private ProcedureParameter createOutputProcedureParameterFrom( 85 | Method method, Procedure procedure) { 86 | 87 | return new ProcedureParameter( 88 | procedure.outputParameterName(), 89 | procedure.refCursor() ? ParameterMode.REF_CURSOR : ParameterMode.OUT, 90 | method.getReturnType()); 91 | } 92 | 93 | private List extractOutputParametersFrom( 94 | NamedStoredProcedureQuery namedStoredProc) { 95 | 96 | List outputParameters = new ArrayList<>(); 97 | 98 | for (StoredProcedureParameter param : namedStoredProc.parameters()) { 99 | 100 | switch (param.mode()) { 101 | case OUT: 102 | case INOUT: 103 | case REF_CURSOR: 104 | outputParameters.add(param); 105 | break; 106 | case IN: 107 | default: 108 | continue; 109 | } 110 | } 111 | 112 | return outputParameters; 113 | } 114 | 115 | @Nullable 116 | private NamedStoredProcedureQuery tryFindAnnotatedNamedStoredProcedureQuery( 117 | Method method, JpaEntityMetadata entityMetadata, Procedure procedure) { 118 | 119 | Assert.notNull(method, "Method must not be null"); 120 | Assert.notNull(entityMetadata, "EntityMetadata must not be null"); 121 | Assert.notNull(procedure, "Procedure must not be null"); 122 | 123 | Class entityType = entityMetadata.getJavaType(); 124 | 125 | List queries = collectNamedStoredProcedureQueriesFrom(entityType); 126 | 127 | if (queries.isEmpty()) { 128 | return null; 129 | } 130 | 131 | String namedProcedureName = derivedNamedProcedureNameFrom(method, entityMetadata, procedure); 132 | 133 | for (NamedStoredProcedureQuery query : queries) { 134 | 135 | if (query.name().equals(namedProcedureName)) { 136 | return query; 137 | } 138 | } 139 | 140 | return null; 141 | } 142 | 143 | private String derivedNamedProcedureNameFrom( 144 | Method method, JpaEntityMetadata entityMetadata, Procedure procedure) { 145 | 146 | return StringUtils.hasText(procedure.name()) // 147 | ? procedure.name() // 148 | : entityMetadata.getEntityName() + "." + method.getName(); 149 | } 150 | 151 | private List collectNamedStoredProcedureQueriesFrom( 152 | Class entityType) { 153 | 154 | List queries = new ArrayList<>(); 155 | 156 | NamedStoredProcedureQueries namedQueriesAnnotation = 157 | AnnotatedElementUtils.findMergedAnnotation(entityType, NamedStoredProcedureQueries.class); 158 | if (namedQueriesAnnotation != null) { 159 | queries.addAll(Arrays.asList(namedQueriesAnnotation.value())); 160 | } 161 | 162 | NamedStoredProcedureQuery namedQueryAnnotation = 163 | AnnotatedElementUtils.findMergedAnnotation(entityType, NamedStoredProcedureQuery.class); 164 | if (namedQueryAnnotation != null) { 165 | queries.add(namedQueryAnnotation); 166 | } 167 | 168 | return queries; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/query/StoredProcedureAttributes.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.query; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.IntStream; 7 | import org.springframework.util.Assert; 8 | import org.springframework.util.StringUtils; 9 | 10 | public class StoredProcedureAttributes { 11 | 12 | static final String SYNTHETIC_OUTPUT_PARAMETER_NAME = "out"; 13 | 14 | private final boolean namedStoredProcedure; 15 | private final String procedureName; 16 | private final List outputProcedureParameters; 17 | 18 | StoredProcedureAttributes(String procedureName, ProcedureParameter parameter) { 19 | this(procedureName, Collections.singletonList(parameter), false); 20 | } 21 | 22 | StoredProcedureAttributes( 23 | String procedureName, 24 | List outputProcedureParameters, 25 | boolean namedStoredProcedure) { 26 | 27 | Assert.notNull(procedureName, "ProcedureName must not be null"); 28 | Assert.notNull(outputProcedureParameters, "OutputProcedureParameters must not be null"); 29 | Assert.isTrue( 30 | outputProcedureParameters.size() != 1 || outputProcedureParameters.get(0) != null, 31 | "ProcedureParameters must not have size 1 with a null value"); 32 | 33 | this.procedureName = procedureName; 34 | this.namedStoredProcedure = namedStoredProcedure; 35 | 36 | if (namedStoredProcedure) { 37 | this.outputProcedureParameters = outputProcedureParameters; 38 | } else { 39 | this.outputProcedureParameters = getParametersWithCompletedNames(outputProcedureParameters); 40 | } 41 | } 42 | 43 | private List getParametersWithCompletedNames( 44 | List procedureParameters) { 45 | 46 | return IntStream.range(0, procedureParameters.size()) // 47 | .mapToObj(i -> getParameterWithCompletedName(procedureParameters.get(i), i)) // 48 | .collect(Collectors.toList()); 49 | } 50 | 51 | private ProcedureParameter getParameterWithCompletedName(ProcedureParameter parameter, int i) { 52 | 53 | return new ProcedureParameter( 54 | completeOutputParameterName(i, parameter.getName()), 55 | parameter.getMode(), 56 | parameter.getType()); 57 | } 58 | 59 | private String completeOutputParameterName(int i, String paramName) { 60 | 61 | return StringUtils.hasText(paramName) // 62 | ? paramName // 63 | : createSyntheticParameterName(i); 64 | } 65 | 66 | private String createSyntheticParameterName(int i) { 67 | return SYNTHETIC_OUTPUT_PARAMETER_NAME + (i == 0 ? "" : i); 68 | } 69 | 70 | public String getProcedureName() { 71 | return procedureName; 72 | } 73 | 74 | public boolean isNamedStoredProcedure() { 75 | return namedStoredProcedure; 76 | } 77 | 78 | public List getOutputProcedureParameters() { 79 | return outputProcedureParameters; 80 | } 81 | 82 | public boolean hasReturnValue() { 83 | 84 | if (getOutputProcedureParameters().isEmpty()) return false; 85 | 86 | Class outputType = getOutputProcedureParameters().get(0).getType(); 87 | return !(void.class.equals(outputType) || Void.class.equals(outputType)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/support/CrudMethodMetadataContextHolder.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.support; 2 | 3 | import org.springframework.data.jpa.repository.support.CrudMethodMetadata; 4 | import reactor.core.publisher.Mono; 5 | import reactor.util.context.Context; 6 | 7 | /** 8 | * @author Bao.Ngo 9 | */ 10 | public class CrudMethodMetadataContextHolder { 11 | 12 | private static final Object KEY = CrudMethodMetadataContextHolder.class; 13 | 14 | private CrudMethodMetadataContextHolder() {} 15 | 16 | public static Mono currentCrudMethodMetadata() { 17 | return Mono.deferContextual( 18 | c -> { 19 | if (c.hasKey(KEY)) { 20 | return c.get(KEY); 21 | } 22 | return Mono.error( 23 | () -> new IllegalStateException("CrudMethodMetadata may not be correctly setup")); 24 | }); 25 | } 26 | 27 | public static Context set(Mono metadata) { 28 | return Context.of(KEY, metadata); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/support/CrudMethodMetadataPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.support; 2 | 3 | import jakarta.persistence.LockModeType; 4 | import jakarta.persistence.QueryHint; 5 | import java.lang.reflect.Method; 6 | import java.util.HashSet; 7 | import java.util.Optional; 8 | import java.util.Set; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.concurrent.ConcurrentMap; 11 | import java.util.function.Predicate; 12 | import java.util.function.Supplier; 13 | import org.aopalliance.intercept.MethodInterceptor; 14 | import org.aopalliance.intercept.MethodInvocation; 15 | import org.springframework.aop.framework.ProxyFactory; 16 | import org.springframework.core.annotation.AnnotatedElementUtils; 17 | import org.springframework.core.annotation.AnnotationUtils; 18 | import org.springframework.data.jpa.repository.EntityGraph; 19 | import org.springframework.data.jpa.repository.Lock; 20 | import org.springframework.data.jpa.repository.Meta; 21 | import org.springframework.data.jpa.repository.QueryHints; 22 | import org.springframework.data.jpa.repository.support.CrudMethodMetadata; 23 | import org.springframework.data.jpa.repository.support.MutableQueryHints; 24 | import org.springframework.data.repository.core.RepositoryInformation; 25 | import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; 26 | import org.springframework.lang.Nullable; 27 | import org.springframework.util.Assert; 28 | import org.springframework.util.ReflectionUtils; 29 | import reactor.core.publisher.Flux; 30 | import reactor.core.publisher.Mono; 31 | 32 | /** 33 | * @author Bao.Ngo 34 | */ 35 | public class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor { 36 | 37 | @Override 38 | public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { 39 | factory.addAdvice( 40 | new CrudMethodMetadataPostProcessor.CrudMethodMetadataPopulatingMethodInterceptor( 41 | repositoryInformation)); 42 | } 43 | 44 | static class CrudMethodMetadataPopulatingMethodInterceptor implements MethodInterceptor { 45 | 46 | private static final ConcurrentMap> METADATA_CACHE = 47 | new ConcurrentHashMap<>(); 48 | 49 | private final Set implementations = new HashSet<>(); 50 | 51 | CrudMethodMetadataPopulatingMethodInterceptor(RepositoryInformation repositoryInformation) { 52 | ReflectionUtils.doWithMethods( 53 | repositoryInformation.getRepositoryInterface(), 54 | implementations::add, 55 | method -> !repositoryInformation.isQueryMethod(method)); 56 | } 57 | 58 | @Override 59 | public Object invoke(MethodInvocation invocation) throws Throwable { 60 | Method method = invocation.getMethod(); 61 | Object proceeded = invocation.proceed(); 62 | 63 | if (!implementations.contains(method)) { 64 | return proceeded; 65 | } 66 | 67 | Mono methodMetadata = METADATA_CACHE.get(method); 68 | 69 | if (methodMetadata == null) { 70 | Supplier supplier = () -> new DefaultCrudMethodMetadata(method); 71 | methodMetadata = Mono.fromSupplier(supplier).cache(); 72 | METADATA_CACHE.putIfAbsent(method, methodMetadata); 73 | } 74 | 75 | if (proceeded instanceof Mono mono) { 76 | return mono.contextWrite(CrudMethodMetadataContextHolder.set(methodMetadata)); 77 | } 78 | 79 | if (proceeded instanceof Flux flux) { 80 | return flux.contextWrite(CrudMethodMetadataContextHolder.set(methodMetadata)); 81 | } 82 | 83 | return proceeded; 84 | } 85 | } 86 | 87 | private static class DefaultCrudMethodMetadata implements CrudMethodMetadata { 88 | 89 | private final @Nullable LockModeType lockModeType; 90 | private final org.springframework.data.jpa.repository.support.QueryHints queryHints; 91 | private final org.springframework.data.jpa.repository.support.QueryHints queryHintsForCount; 92 | private final @Nullable String comment; 93 | private final Optional entityGraph; 94 | private final Method method; 95 | 96 | DefaultCrudMethodMetadata(Method method) { 97 | Assert.notNull(method, "Method must not be null"); 98 | 99 | this.lockModeType = findLockModeType(method); 100 | this.queryHints = findQueryHints(method, it -> true); 101 | this.queryHintsForCount = findQueryHints(method, QueryHints::forCounting); 102 | this.comment = findComment(method); 103 | this.entityGraph = findEntityGraph(method); 104 | this.method = method; 105 | } 106 | 107 | private static Optional findEntityGraph(Method method) { 108 | return Optional.ofNullable( 109 | AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class)); 110 | } 111 | 112 | @Nullable 113 | private static LockModeType findLockModeType(Method method) { 114 | Lock annotation = AnnotatedElementUtils.findMergedAnnotation(method, Lock.class); 115 | return annotation == null ? null : (LockModeType) AnnotationUtils.getValue(annotation); 116 | } 117 | 118 | private static org.springframework.data.jpa.repository.support.QueryHints findQueryHints( 119 | Method method, Predicate annotationFilter) { 120 | MutableQueryHints queryHints = new MutableQueryHints(); 121 | QueryHints queryHintsAnnotation = 122 | AnnotatedElementUtils.findMergedAnnotation(method, QueryHints.class); 123 | if (queryHintsAnnotation != null && annotationFilter.test(queryHintsAnnotation)) { 124 | for (QueryHint hint : queryHintsAnnotation.value()) { 125 | queryHints.add(hint.name(), hint.value()); 126 | } 127 | } 128 | 129 | QueryHint queryHintAnnotation = AnnotationUtils.findAnnotation(method, QueryHint.class); 130 | if (queryHintAnnotation != null) { 131 | queryHints.add(queryHintAnnotation.name(), queryHintAnnotation.value()); 132 | } 133 | 134 | return queryHints; 135 | } 136 | 137 | @Nullable 138 | private static String findComment(Method method) { 139 | Meta annotation = AnnotatedElementUtils.findMergedAnnotation(method, Meta.class); 140 | return annotation == null ? null : (String) AnnotationUtils.getValue(annotation, "comment"); 141 | } 142 | 143 | @Nullable 144 | @Override 145 | public LockModeType getLockModeType() { 146 | return lockModeType; 147 | } 148 | 149 | @Override 150 | public org.springframework.data.jpa.repository.support.QueryHints getQueryHints() { 151 | return queryHints; 152 | } 153 | 154 | @Override 155 | public org.springframework.data.jpa.repository.support.QueryHints getQueryHintsForCount() { 156 | return queryHintsForCount; 157 | } 158 | 159 | @Override 160 | public String getComment() { 161 | return comment; 162 | } 163 | 164 | @Override 165 | public Optional getEntityGraph() { 166 | return entityGraph; 167 | } 168 | 169 | @Override 170 | public Method getMethod() { 171 | return method; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/support/PersistenceExceptionHandlerPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.support; 2 | 3 | import jakarta.persistence.NoResultException; 4 | import org.aopalliance.intercept.MethodInterceptor; 5 | import org.aopalliance.intercept.MethodInvocation; 6 | import org.hibernate.reactive.stage.Stage; 7 | import org.springframework.aop.framework.ProxyFactory; 8 | import org.springframework.data.repository.core.RepositoryInformation; 9 | import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | 13 | /** 14 | * @author Bao.Ngo 15 | */ 16 | public class PersistenceExceptionHandlerPostProcessor implements RepositoryProxyPostProcessor { 17 | 18 | private final Stage.SessionFactory sessionFactory; 19 | 20 | public PersistenceExceptionHandlerPostProcessor(Stage.SessionFactory sessionFactory) { 21 | this.sessionFactory = sessionFactory; 22 | } 23 | 24 | @Override 25 | public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { 26 | factory.addAdvice(new PersistenceExceptionHandlerInterceptor(sessionFactory)); 27 | } 28 | 29 | static class PersistenceExceptionHandlerInterceptor implements MethodInterceptor { 30 | 31 | private final Stage.SessionFactory sessionFactory; 32 | 33 | public PersistenceExceptionHandlerInterceptor(Stage.SessionFactory sessionFactory) { 34 | this.sessionFactory = sessionFactory; 35 | } 36 | 37 | @Override 38 | public Object invoke(MethodInvocation invocation) throws Throwable { 39 | Object proceed = invocation.proceed(); 40 | if (proceed instanceof Mono mono) { 41 | return mono.onErrorResume(NoResultException.class, e -> Mono.empty()); 42 | } else if (proceed instanceof Flux flux) { 43 | return flux.onErrorResume(NoResultException.class, e -> Mono.empty()); 44 | } 45 | 46 | return proceed; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/support/ReactiveJpaRepositoryFactory.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.support; 2 | 3 | import com.htech.data.jpa.reactive.core.StageReactiveJpaEntityOperations; 4 | import com.htech.data.jpa.reactive.repository.query.ReactiveJpaQueryLookupStrategy; 5 | import com.htech.data.jpa.reactive.repository.query.ReactiveJpaQueryMethodFactory; 6 | import com.htech.data.jpa.reactive.repository.query.ReactiveQueryRewriterProvider; 7 | import jakarta.persistence.EntityManagerFactory; 8 | import jakarta.persistence.metamodel.Metamodel; 9 | import java.util.Optional; 10 | import org.hibernate.reactive.stage.Stage; 11 | import org.springframework.beans.factory.BeanClassLoaderAware; 12 | import org.springframework.data.domain.Persistable; 13 | import org.springframework.data.jpa.repository.query.EscapeCharacter; 14 | import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation; 15 | import org.springframework.data.jpa.repository.support.JpaPersistableEntityInformation; 16 | import org.springframework.data.repository.core.EntityInformation; 17 | import org.springframework.data.repository.core.RepositoryInformation; 18 | import org.springframework.data.repository.core.RepositoryMetadata; 19 | import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; 20 | import org.springframework.data.repository.query.QueryLookupStrategy; 21 | import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; 22 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; 23 | 24 | /** 25 | * @author Bao.Ngo 26 | */ 27 | public class ReactiveJpaRepositoryFactory extends ReactiveRepositoryFactorySupport 28 | implements BeanClassLoaderAware { 29 | 30 | protected final StageReactiveJpaEntityOperations entityOperations; 31 | protected final Stage.SessionFactory sessionFactory; 32 | protected final EntityManagerFactory entityManagerFactory; 33 | protected ClassLoader classLoader; 34 | protected EscapeCharacter escapeCharacter; 35 | 36 | protected ReactiveJpaQueryMethodFactory queryMethodFactory; 37 | protected ReactiveQueryRewriterProvider queryRewriterProvider; 38 | 39 | public ReactiveJpaRepositoryFactory( 40 | StageReactiveJpaEntityOperations entityOperations, 41 | Stage.SessionFactory sessionFactory, 42 | EntityManagerFactory entityManagerFactory) { 43 | this.entityOperations = entityOperations; 44 | this.sessionFactory = sessionFactory; 45 | this.entityManagerFactory = entityManagerFactory; 46 | } 47 | 48 | @Override 49 | @SuppressWarnings({"rawtypes"}) 50 | public EntityInformation getEntityInformation(Class domainClass) { 51 | Metamodel metamodel = sessionFactory.getMetamodel(); 52 | if (Persistable.class.isAssignableFrom(domainClass)) { 53 | return new JpaPersistableEntityInformation( 54 | domainClass, metamodel, entityManagerFactory.getPersistenceUnitUtil()); 55 | } else { 56 | return new JpaMetamodelEntityInformation( 57 | domainClass, metamodel, entityManagerFactory.getPersistenceUnitUtil()); 58 | } 59 | } 60 | 61 | @Override 62 | protected Object getTargetRepository(RepositoryInformation repositoryInformation) { 63 | EntityInformation entityInformation = 64 | getEntityInformation(repositoryInformation.getDomainType()); 65 | ReactiveJpaRepositoryImplementation repository = 66 | getTargetRepositoryViaReflection( 67 | repositoryInformation, entityInformation, sessionFactory, entityOperations); 68 | // 69 | // repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata()); 70 | repository.setEscapeCharacter(escapeCharacter); 71 | 72 | return repository; 73 | } 74 | 75 | // protected ReactiveJpaRepositoryImplementation getTargetRepositoryViaReflection1( 76 | // RepositoryInformation repositoryInformation, EntityInformation 77 | // entityInformation) { 78 | // Class repositoryBaseClass = repositoryInformation.getRepositoryBaseClass(); 79 | // 80 | // return Optional.ofNullable(ReflectionUtils.findMethod( 81 | // repositoryBaseClass, 82 | // "createInstance", 83 | // JpaEntityInformation.class, 84 | // MutinyReactiveJpaEntityOperations.class, 85 | // Stage.SessionFactory.class, 86 | // ClassLoader.class)) 87 | // .map(m -> { 88 | // ReflectionUtils.makeAccessible(m); 89 | // try { 90 | // return (ReactiveJpaRepositoryImplementation) 91 | // m.invoke(null, entityInformation, entityOperations, sessionFactory, 92 | // classLoader); 93 | // } catch (IllegalAccessException | InvocationTargetException e) { 94 | // throw new RuntimeException(e.getMessage(), e); 95 | // } 96 | // }).orElseThrow(() -> new RuntimeException("Method createInstance is not found")); 97 | // 98 | //// return (ReactiveJpaRepositoryImplementation) 99 | //// method.invoke(null, entityInformation, sessionFactory, classLoader); 100 | // } 101 | 102 | @Override 103 | protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { 104 | // return SimpleReactiveJpaRepository.class; 105 | return SimpleReactiveJpaRepository.class; 106 | } 107 | 108 | public void setEscapeCharacter(EscapeCharacter escapeCharacter) { 109 | this.escapeCharacter = escapeCharacter; 110 | } 111 | 112 | @Override 113 | protected Optional getQueryLookupStrategy( 114 | QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { 115 | return Optional.of( 116 | ReactiveJpaQueryLookupStrategy.create( 117 | entityManagerFactory, 118 | sessionFactory, 119 | queryMethodFactory, 120 | key, 121 | (ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider, 122 | queryRewriterProvider, 123 | escapeCharacter)); 124 | } 125 | 126 | @Override 127 | public void setBeanClassLoader(ClassLoader classLoader) { 128 | super.setBeanClassLoader(classLoader); 129 | this.classLoader = classLoader; 130 | } 131 | 132 | @Override 133 | public RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { 134 | return super.getRepositoryMetadata(repositoryInterface); 135 | } 136 | 137 | public ReactiveJpaQueryMethodFactory getQueryMethodFactory() { 138 | return queryMethodFactory; 139 | } 140 | 141 | public void setQueryMethodFactory(ReactiveJpaQueryMethodFactory queryMethodFactory) { 142 | this.queryMethodFactory = queryMethodFactory; 143 | } 144 | 145 | public ReactiveQueryRewriterProvider getQueryRewriterProvider() { 146 | return queryRewriterProvider; 147 | } 148 | 149 | public void setQueryRewriterProvider(ReactiveQueryRewriterProvider queryRewriterProvider) { 150 | this.queryRewriterProvider = queryRewriterProvider; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/support/ReactiveJpaRepositoryFactoryBean.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.support; 2 | 3 | import com.htech.data.jpa.reactive.core.StageReactiveJpaEntityOperations; 4 | import com.htech.data.jpa.reactive.repository.query.DefaultReactiveJpaQueryExtractor; 5 | import com.htech.data.jpa.reactive.repository.query.ReactiveJpaQueryMethodFactory; 6 | import com.htech.data.jpa.reactive.repository.query.ReactiveQueryRewriterProvider; 7 | import jakarta.persistence.EntityManagerFactory; 8 | import java.io.Serializable; 9 | import java.util.*; 10 | import org.springframework.beans.BeansException; 11 | import org.springframework.beans.factory.BeanClassLoaderAware; 12 | import org.springframework.beans.factory.ListableBeanFactory; 13 | import org.springframework.beans.factory.ObjectProvider; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.context.ApplicationContext; 16 | import org.springframework.context.ApplicationContextAware; 17 | import org.springframework.data.jpa.repository.query.EscapeCharacter; 18 | import org.springframework.data.querydsl.EntityPathResolver; 19 | import org.springframework.data.querydsl.SimpleEntityPathResolver; 20 | import org.springframework.data.repository.Repository; 21 | import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; 22 | import org.springframework.data.repository.core.support.RepositoryFactorySupport; 23 | import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; 24 | import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; 25 | import org.springframework.lang.Nullable; 26 | 27 | /** 28 | * @author Bao.Ngo 29 | */ 30 | public class ReactiveJpaRepositoryFactoryBean< 31 | T extends Repository, S, ID extends Serializable> 32 | extends RepositoryFactoryBeanSupport 33 | implements ApplicationContextAware, BeanClassLoaderAware { 34 | 35 | private @Nullable ApplicationContext applicationContext; 36 | private StageReactiveJpaEntityOperations entityOperations; 37 | 38 | private EntityPathResolver entityPathResolver; 39 | 40 | private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; 41 | 42 | protected ReactiveJpaRepositoryFactoryBean(Class repositoryInterface) { 43 | super(repositoryInterface); 44 | } 45 | 46 | @Override 47 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 48 | this.applicationContext = applicationContext; 49 | } 50 | 51 | @Override 52 | protected RepositoryFactorySupport createRepositoryFactory() { 53 | ReactiveJpaRepositoryFactory factory = 54 | new ReactiveJpaRepositoryFactory( 55 | entityOperations, 56 | entityOperations.sessionFactory(), 57 | applicationContext.getBean("entityManagerFactory", EntityManagerFactory.class)); 58 | factory.setEscapeCharacter(escapeCharacter); 59 | // TODO 60 | factory.setQueryMethodFactory( 61 | new ReactiveJpaQueryMethodFactory(new DefaultReactiveJpaQueryExtractor())); 62 | factory.setQueryRewriterProvider(ReactiveQueryRewriterProvider.simple()); 63 | 64 | // RepositoryMetadata repositoryMetadata = factory.getRepositoryMetadata(getObjectType()); 65 | // factory.addRepositoryProxyPostProcessor(new ValueAdapterInterceptorProxyPostProcessor()); 66 | // factory.addRepositoryProxyPostProcessor(new SessionAwareProxyPostProcessor()); 67 | factory.addRepositoryProxyPostProcessor(new CrudMethodMetadataPostProcessor()); 68 | factory.addRepositoryProxyPostProcessor( 69 | new SessionAwarePostProcessor(entityOperations.sessionFactory())); 70 | factory.addRepositoryProxyPostProcessor( 71 | new PersistenceExceptionHandlerPostProcessor(entityOperations.sessionFactory())); 72 | 73 | return factory; 74 | } 75 | 76 | @Override 77 | protected Optional 78 | createDefaultQueryMethodEvaluationContextProvider(ListableBeanFactory beanFactory) { 79 | return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory)); 80 | } 81 | 82 | // @Autowired 83 | public void setEntityOperations(@Nullable StageReactiveJpaEntityOperations entityOperations) { 84 | this.entityOperations = entityOperations; 85 | } 86 | 87 | @Autowired 88 | public void setEntityPathResolver(ObjectProvider resolver) { 89 | this.entityPathResolver = resolver.getIfAvailable(() -> SimpleEntityPathResolver.INSTANCE); 90 | } 91 | 92 | public void setEscapeCharacter(char escapeCharacter) { 93 | this.escapeCharacter = EscapeCharacter.of(escapeCharacter); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/support/ReactiveJpaRepositoryImplementation.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.support; 2 | 3 | import com.htech.data.jpa.reactive.repository.ReactiveJpaRepository; 4 | import com.htech.data.jpa.reactive.repository.ReactiveJpaSpecificationExecutor; 5 | import org.springframework.data.jpa.repository.query.EscapeCharacter; 6 | import org.springframework.data.repository.NoRepositoryBean; 7 | 8 | /** 9 | * @author Bao.Ngo 10 | */ 11 | @NoRepositoryBean 12 | public interface ReactiveJpaRepositoryImplementation 13 | extends ReactiveJpaRepository, ReactiveJpaSpecificationExecutor { 14 | 15 | default void setEscapeCharacter(EscapeCharacter escapeCharacter) {} 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/htech/data/jpa/reactive/repository/support/SessionAwarePostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.htech.data.jpa.reactive.repository.support; 2 | 3 | import static org.springframework.transaction.reactive.TransactionSynchronizationManager.forCurrentTransaction; 4 | 5 | import com.htech.jpa.reactive.connection.ConnectionHolder; 6 | import com.htech.jpa.reactive.connection.SessionContextHolder; 7 | import com.htech.jpa.reactive.connection.TransactionUtils; 8 | import org.aopalliance.intercept.MethodInterceptor; 9 | import org.aopalliance.intercept.MethodInvocation; 10 | import org.hibernate.reactive.stage.Stage; 11 | import org.springframework.aop.framework.ProxyFactory; 12 | import org.springframework.data.repository.core.RepositoryInformation; 13 | import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | /** 18 | * @author Bao.Ngo 19 | */ 20 | public class SessionAwarePostProcessor implements RepositoryProxyPostProcessor { 21 | 22 | private final Stage.SessionFactory sessionFactory; 23 | 24 | public SessionAwarePostProcessor(Stage.SessionFactory sessionFactory) { 25 | this.sessionFactory = sessionFactory; 26 | } 27 | 28 | @Override 29 | public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { 30 | factory.addAdvice(new SessionAwareInterceptor(sessionFactory)); 31 | } 32 | 33 | static class SessionAwareInterceptor implements MethodInterceptor { 34 | 35 | private final Stage.SessionFactory sessionFactory; 36 | 37 | public SessionAwareInterceptor(Stage.SessionFactory sessionFactory) { 38 | this.sessionFactory = sessionFactory; 39 | } 40 | 41 | @Override 42 | public Object invoke(MethodInvocation invocation) throws Throwable { 43 | Object proceed = invocation.proceed(); 44 | Mono session = currentSession(sessionFactory); 45 | Mono transactionAvailable = TransactionUtils.isTransactionAvailable(sessionFactory); 46 | if (proceed instanceof Mono mono) { 47 | return Mono.usingWhen( 48 | Mono.just("dummy"), 49 | str -> mono.contextWrite(SessionContextHolder.set(session)), 50 | str -> closeNormally(transactionAvailable, session), 51 | (str, t) -> closeExceptionally(t, transactionAvailable, session), 52 | str -> closeNormally(transactionAvailable, session)); 53 | } else if (proceed instanceof Flux flux) { 54 | return Flux.usingWhen( 55 | Mono.just("dummy"), 56 | str -> flux.contextWrite(SessionContextHolder.set(session)), 57 | str -> closeNormally(transactionAvailable, session), 58 | (str, t) -> closeExceptionally(t, transactionAvailable, session), 59 | str -> closeNormally(transactionAvailable, session)); 60 | } 61 | 62 | return proceed; 63 | } 64 | 65 | private static Mono currentSession(Stage.SessionFactory sessionFactory) { 66 | return forCurrentTransaction() 67 | .mapNotNull(tsm -> tsm.getResource(sessionFactory)) 68 | .filter(ConnectionHolder.class::isInstance) 69 | .onErrorResume(e -> Mono.empty()) 70 | .map(ConnectionHolder.class::cast) 71 | .map(ConnectionHolder::getConnection) 72 | .map(Stage.Session.class::cast) 73 | .switchIfEmpty(Mono.defer(() -> Mono.fromCompletionStage(sessionFactory.openSession()))) 74 | .cache(); 75 | } 76 | 77 | private static Mono closeExceptionally( 78 | Throwable t, Mono transactionAvailable, Mono session) { 79 | return transactionAvailable.flatMap( 80 | b -> { 81 | if (b) { 82 | return Mono.error(t); 83 | } 84 | return session 85 | .flatMap(s -> Mono.defer(() -> Mono.fromCompletionStage(s.close()))) 86 | .then(Mono.error(t)); 87 | }); 88 | } 89 | 90 | private static Mono closeNormally( 91 | Mono transactionAvailable, Mono session) { 92 | return transactionAvailable.flatMap( 93 | b -> { 94 | if (b) { 95 | return Mono.empty(); 96 | } 97 | return session.flatMap(s -> Mono.defer(() -> Mono.fromCompletionStage(s.close()))); 98 | }); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/pu/CustomPersistenceUnitManager.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.pu; 2 | 3 | import jakarta.persistence.spi.PersistenceUnitInfo; 4 | import java.util.Map; 5 | import java.util.Properties; 6 | import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; 7 | import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; 8 | 9 | /** 10 | * @author Bao.Ngo 11 | */ 12 | public class CustomPersistenceUnitManager extends DefaultPersistenceUnitManager { 13 | 14 | private final Map vendorProperties; 15 | 16 | public CustomPersistenceUnitManager(Map vendorProperties) { 17 | this.vendorProperties = vendorProperties; 18 | } 19 | 20 | @Override 21 | public PersistenceUnitInfo obtainDefaultPersistenceUnitInfo() { 22 | PersistenceUnitInfo persistenceUnitInfo = super.obtainDefaultPersistenceUnitInfo(); 23 | if (persistenceUnitInfo instanceof MutablePersistenceUnitInfo m) { 24 | Properties properties = new Properties(); 25 | properties.putAll(vendorProperties); 26 | m.setProperties(properties); 27 | } 28 | 29 | return persistenceUnitInfo; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/EntityManagerFactoryBuilder.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive; 2 | 3 | import java.net.URL; 4 | import java.util.HashMap; 5 | import java.util.HashSet; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import org.springframework.core.task.AsyncTaskExecutor; 10 | import org.springframework.orm.jpa.JpaVendorAdapter; 11 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 12 | import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; 13 | import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; 14 | import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; 15 | import org.springframework.util.ClassUtils; 16 | import org.springframework.util.ObjectUtils; 17 | import org.springframework.util.StringUtils; 18 | 19 | /** 20 | * @author Bao.Ngo 21 | */ 22 | public class EntityManagerFactoryBuilder { 23 | 24 | private final JpaVendorAdapter jpaVendorAdapter; 25 | 26 | private final PersistenceUnitManager persistenceUnitManager; 27 | 28 | private final Map jpaProperties; 29 | 30 | private final URL persistenceUnitRootLocation; 31 | 32 | private AsyncTaskExecutor bootstrapExecutor; 33 | 34 | private PersistenceUnitPostProcessor[] persistenceUnitPostProcessors; 35 | 36 | public EntityManagerFactoryBuilder( 37 | JpaVendorAdapter jpaVendorAdapter, 38 | Map jpaProperties, 39 | PersistenceUnitManager persistenceUnitManager) { 40 | this(jpaVendorAdapter, jpaProperties, persistenceUnitManager, null); 41 | } 42 | 43 | public EntityManagerFactoryBuilder( 44 | JpaVendorAdapter jpaVendorAdapter, 45 | Map jpaProperties, 46 | PersistenceUnitManager persistenceUnitManager, 47 | URL persistenceUnitRootLocation) { 48 | this.jpaVendorAdapter = jpaVendorAdapter; 49 | this.persistenceUnitManager = persistenceUnitManager; 50 | this.jpaProperties = new LinkedHashMap<>(jpaProperties); 51 | this.persistenceUnitRootLocation = persistenceUnitRootLocation; 52 | } 53 | 54 | public Builder builder() { 55 | return new Builder(); 56 | } 57 | 58 | public void setBootstrapExecutor(AsyncTaskExecutor bootstrapExecutor) { 59 | this.bootstrapExecutor = bootstrapExecutor; 60 | } 61 | 62 | public void setPersistenceUnitPostProcessors( 63 | PersistenceUnitPostProcessor... persistenceUnitPostProcessors) { 64 | this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; 65 | } 66 | 67 | public class Builder { 68 | 69 | // private DataSource dataSource; 70 | 71 | private PersistenceManagedTypes managedTypes; 72 | 73 | private String[] packagesToScan; 74 | 75 | private String persistenceUnit; 76 | 77 | private Map properties = new HashMap<>(); 78 | 79 | private String[] mappingResources; 80 | 81 | // private boolean jta; 82 | 83 | protected Builder() {} 84 | 85 | public Builder managedTypes(PersistenceManagedTypes managedTypes) { 86 | this.managedTypes = managedTypes; 87 | return this; 88 | } 89 | 90 | public Builder packages(String... packagesToScan) { 91 | this.packagesToScan = packagesToScan; 92 | return this; 93 | } 94 | 95 | public Builder packages(Class... basePackageClasses) { 96 | Set packages = new HashSet<>(); 97 | for (Class type : basePackageClasses) { 98 | packages.add(ClassUtils.getPackageName(type)); 99 | } 100 | this.packagesToScan = StringUtils.toStringArray(packages); 101 | return this; 102 | } 103 | 104 | public Builder persistenceUnit(String persistenceUnit) { 105 | this.persistenceUnit = persistenceUnit; 106 | return this; 107 | } 108 | 109 | public Builder properties(Map properties) { 110 | this.properties.putAll(properties); 111 | return this; 112 | } 113 | 114 | public Builder mappingResources(String... mappingResources) { 115 | this.mappingResources = mappingResources; 116 | return this; 117 | } 118 | 119 | // public Builder jta(boolean jta) { 120 | // this.jta = jta; 121 | // return this; 122 | // } 123 | 124 | public LocalContainerEntityManagerFactoryBean build() { 125 | LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = 126 | new LocalContainerEntityManagerFactoryBean(); 127 | if (persistenceUnitManager != null) { 128 | entityManagerFactoryBean.setPersistenceUnitManager(persistenceUnitManager); 129 | } 130 | if (this.persistenceUnit != null) { 131 | entityManagerFactoryBean.setPersistenceUnitName(this.persistenceUnit); 132 | } 133 | entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter); 134 | 135 | // if (this.jta) { 136 | // entityManagerFactoryBean.setJtaDataSource(this.dataSource); 137 | // } 138 | // else { 139 | // entityManagerFactoryBean.setDataSource(this.dataSource); 140 | // } 141 | if (this.managedTypes != null) { 142 | entityManagerFactoryBean.setManagedTypes(this.managedTypes); 143 | } else { 144 | entityManagerFactoryBean.setPackagesToScan(this.packagesToScan); 145 | } 146 | entityManagerFactoryBean.getJpaPropertyMap().putAll(jpaProperties); 147 | entityManagerFactoryBean.getJpaPropertyMap().putAll(this.properties); 148 | if (!ObjectUtils.isEmpty(this.mappingResources)) { 149 | entityManagerFactoryBean.setMappingResources(this.mappingResources); 150 | } 151 | URL rootLocation = persistenceUnitRootLocation; 152 | if (rootLocation != null) { 153 | entityManagerFactoryBean.setPersistenceUnitRootLocation(rootLocation.toString()); 154 | } 155 | if (bootstrapExecutor != null) { 156 | entityManagerFactoryBean.setBootstrapExecutor(bootstrapExecutor); 157 | } 158 | if (persistenceUnitPostProcessors != null) { 159 | entityManagerFactoryBean.setPersistenceUnitPostProcessors(persistenceUnitPostProcessors); 160 | } 161 | return entityManagerFactoryBean; 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/EntityManagerFactoryBuilderCustomizer.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive; 2 | 3 | /** 4 | * @author Bao.Ngo 5 | */ 6 | @FunctionalInterface 7 | public interface EntityManagerFactoryBuilderCustomizer { 8 | 9 | void customize(EntityManagerFactoryBuilder builder); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/ReactiveHibernateJpaAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive; 2 | 3 | import org.hibernate.reactive.provider.ReactivePersistenceProvider; 4 | import org.springframework.boot.autoconfigure.AutoConfiguration; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 6 | import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; 7 | import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Import; 11 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 12 | 13 | /** 14 | * @author Bao.Ngo 15 | */ 16 | @Configuration(proxyBeanMethods = false) 17 | @AutoConfiguration(before = TransactionAutoConfiguration.class) 18 | @ConditionalOnClass({ 19 | LocalContainerEntityManagerFactoryBean.class, 20 | ReactivePersistenceProvider.class 21 | }) 22 | @EnableConfigurationProperties(JpaProperties.class) 23 | @Import(ReactiveHibernateJpaConfiguration.class) 24 | public class ReactiveHibernateJpaAutoConfiguration {} 25 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/connection/ConnectionFactoryUtils.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive.connection; 2 | 3 | import static org.springframework.transaction.reactive.TransactionSynchronizationManager.forCurrentTransaction; 4 | 5 | import org.hibernate.reactive.stage.Stage; 6 | import org.hibernate.reactive.stage.impl.StageSessionImpl; 7 | import org.springframework.dao.DataAccessResourceFailureException; 8 | import org.springframework.transaction.NoTransactionException; 9 | import reactor.core.publisher.Mono; 10 | 11 | public class ConnectionFactoryUtils { 12 | 13 | private ConnectionFactoryUtils() {} 14 | 15 | public static Mono releaseConnection( 16 | StageSessionImpl con, Stage.SessionFactory connectionFactory) { 17 | return doReleaseConnection(con, connectionFactory) 18 | .onErrorMap( 19 | ex -> new DataAccessResourceFailureException("Failed to close R2DBC Connection", ex)); 20 | } 21 | 22 | public static Mono doReleaseConnection( 23 | StageSessionImpl connection, Stage.SessionFactory connectionFactory) { 24 | return forCurrentTransaction() 25 | .flatMap( 26 | synchronizationManager -> { 27 | ConnectionHolder conHolder = 28 | (ConnectionHolder) synchronizationManager.getResource(connectionFactory); 29 | if (conHolder != null && connectionEquals(conHolder, connection)) { 30 | // It's the transactional Connection: Don't close it. 31 | conHolder.released(); 32 | return Mono.empty(); 33 | } 34 | return Mono.defer(() -> Mono.fromCompletionStage(connection.close())); 35 | }) 36 | .onErrorResume( 37 | NoTransactionException.class, 38 | ex -> Mono.defer(() -> Mono.fromCompletionStage(connection.close()))); 39 | } 40 | 41 | private static boolean connectionEquals( 42 | ConnectionHolder conHolder, StageSessionImpl passedInCon) { 43 | if (!conHolder.hasConnection()) { 44 | return false; 45 | } 46 | StageSessionImpl heldCon = conHolder.getConnection(); 47 | // Explicitly check for identity too: for Connection handles that do not implement 48 | // "equals" properly). 49 | return (heldCon == passedInCon 50 | || heldCon.equals(passedInCon) 51 | || getTargetConnection(heldCon).equals(passedInCon)); 52 | } 53 | 54 | public static StageSessionImpl getTargetConnection(StageSessionImpl con) { 55 | StageSessionImpl conToUse = con; 56 | // while (conToUse instanceof Wrapped) { 57 | // conToUse = ((Wrapped) conToUse).unwrap(); 58 | // } 59 | return conToUse; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/connection/ConnectionHolder.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive.connection; 2 | 3 | import org.hibernate.reactive.stage.impl.StageSessionImpl; 4 | import org.springframework.lang.Nullable; 5 | import org.springframework.transaction.support.ResourceHolderSupport; 6 | import org.springframework.util.Assert; 7 | 8 | /** 9 | * @author Bao.Ngo 10 | */ 11 | public class ConnectionHolder extends ResourceHolderSupport { 12 | 13 | static final String SAVEPOINT_NAME_PREFIX = "SAVEPOINT_"; 14 | 15 | @Nullable private StageSessionImpl currentConnection; 16 | 17 | private boolean transactionActive; 18 | 19 | private int savepointCounter = 0; 20 | 21 | public ConnectionHolder(StageSessionImpl connection) { 22 | this(connection, false); 23 | } 24 | 25 | public ConnectionHolder(StageSessionImpl connection, boolean transactionActive) { 26 | this.currentConnection = connection; 27 | this.transactionActive = transactionActive; 28 | } 29 | 30 | protected boolean hasConnection() { 31 | return (this.currentConnection != null); 32 | } 33 | 34 | protected void setTransactionActive(boolean transactionActive) { 35 | this.transactionActive = transactionActive; 36 | } 37 | 38 | protected boolean isTransactionActive() { 39 | return this.transactionActive; 40 | } 41 | 42 | protected void setConnection(@Nullable StageSessionImpl connection) { 43 | this.currentConnection = connection; 44 | } 45 | 46 | public StageSessionImpl getConnection() { 47 | Assert.state(this.currentConnection != null, "Active ReactiveConnection is required"); 48 | return this.currentConnection; 49 | } 50 | 51 | String nextSavepoint() { 52 | this.savepointCounter++; 53 | return SAVEPOINT_NAME_PREFIX + this.savepointCounter; 54 | } 55 | 56 | @Override 57 | public void released() { 58 | super.released(); 59 | if (!isOpen() && this.currentConnection != null) { 60 | this.currentConnection = null; 61 | } 62 | } 63 | 64 | @Override 65 | public void clear() { 66 | super.clear(); 67 | this.transactionActive = false; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/connection/ConstantPool.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive.connection; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | import java.util.concurrent.ConcurrentMap; 5 | 6 | public abstract class ConstantPool { 7 | 8 | private final ConcurrentMap constants = new ConcurrentHashMap<>(); 9 | 10 | @Override 11 | public String toString() { 12 | return "ConstantPool{" + "constants=" + this.constants + '}'; 13 | } 14 | 15 | abstract T createConstant(String name, boolean sensitive); 16 | 17 | final T valueOf(String name, boolean sensitive) { 18 | return this.constants.computeIfAbsent(name, n -> createConstant(n, sensitive)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/connection/IsolationLevel.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive.connection; 2 | 3 | public class IsolationLevel implements TransactionDefinition { 4 | 5 | private static final ConstantPool CONSTANTS = 6 | new ConstantPool() { 7 | 8 | @Override 9 | IsolationLevel createConstant(String name, boolean sensitive) { 10 | return new IsolationLevel(name); 11 | } 12 | }; 13 | 14 | public static final IsolationLevel READ_COMMITTED = IsolationLevel.valueOf("READ COMMITTED"); 15 | 16 | public static final IsolationLevel READ_UNCOMMITTED = IsolationLevel.valueOf("READ UNCOMMITTED"); 17 | 18 | public static final IsolationLevel REPEATABLE_READ = IsolationLevel.valueOf("REPEATABLE READ"); 19 | 20 | public static final IsolationLevel SERIALIZABLE = IsolationLevel.valueOf("SERIALIZABLE"); 21 | 22 | private final String sql; 23 | 24 | private IsolationLevel(String sql) { 25 | this.sql = sql; 26 | } 27 | 28 | public static IsolationLevel valueOf(String sql) { 29 | return CONSTANTS.valueOf(sql, false); 30 | } 31 | 32 | @Override 33 | public T getAttribute(Option option) { 34 | if (option.equals(TransactionDefinition.ISOLATION_LEVEL)) { 35 | return option.cast(this); 36 | } 37 | 38 | return null; 39 | } 40 | 41 | public String asSql() { 42 | return this.sql; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "IsolationLevel{" + "sql='" + this.sql + '\'' + '}'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/connection/Option.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive.connection; 2 | 3 | import java.util.Objects; 4 | import org.springframework.lang.Nullable; 5 | 6 | public class Option { 7 | 8 | private static final ConstantPool> CONSTANTS = 9 | new ConstantPool>() { 10 | 11 | @Override 12 | Option createConstant(String name, boolean sensitive) { 13 | return new Option<>(name, sensitive); 14 | } 15 | }; 16 | 17 | private final String name; 18 | 19 | private final boolean sensitive; 20 | 21 | private Option(String name, boolean sensitive) { 22 | this.name = name; 23 | this.sensitive = sensitive; 24 | } 25 | 26 | @SuppressWarnings("unchecked") 27 | public static Option sensitiveValueOf(String name) { 28 | return (Option) CONSTANTS.valueOf(name, true); 29 | } 30 | 31 | @SuppressWarnings("unchecked") 32 | public static Option valueOf(String name) { 33 | return (Option) CONSTANTS.valueOf(name, false); 34 | } 35 | 36 | @Nullable 37 | @SuppressWarnings("unchecked") 38 | public T cast(@Nullable Object obj) { 39 | if (obj == null) { 40 | return null; 41 | } 42 | 43 | return (T) obj; 44 | } 45 | 46 | public String name() { 47 | return this.name; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "Option{" + "name='" + this.name + '\'' + ", sensitive=" + this.sensitive + '}'; 53 | } 54 | 55 | @Override 56 | public boolean equals(Object o) { 57 | if (this == o) { 58 | return true; 59 | } 60 | if (o == null || getClass() != o.getClass()) { 61 | return false; 62 | } 63 | Option option = (Option) o; 64 | return this.sensitive == option.sensitive && this.name.equals(option.name); 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | return Objects.hash(this.name, this.sensitive); 70 | } 71 | 72 | boolean sensitive() { 73 | return this.sensitive; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/connection/SessionContextHolder.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive.connection; 2 | 3 | import org.hibernate.reactive.stage.Stage; 4 | import reactor.core.publisher.Mono; 5 | import reactor.util.context.Context; 6 | 7 | /** 8 | * @author Bao.Ngo 9 | */ 10 | public class SessionContextHolder { 11 | 12 | private static final Object KEY = SessionContextHolder.class; 13 | 14 | private SessionContextHolder() {} 15 | 16 | public static Mono currentSession() { 17 | return Mono.deferContextual( 18 | c -> { 19 | if (c.hasKey(KEY)) { 20 | return c.get(KEY); 21 | } 22 | return Mono.error(() -> new IllegalStateException("Session may not be correctly setup")); 23 | }); 24 | } 25 | 26 | public static Context set(Mono session) { 27 | return Context.of(KEY, session); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/connection/TransactionDefinition.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive.connection; 2 | 3 | import java.time.Duration; 4 | import org.springframework.lang.Nullable; 5 | 6 | /** 7 | * @author Bao.Ngo 8 | */ 9 | public interface TransactionDefinition { 10 | Option ISOLATION_LEVEL = Option.valueOf("isolationLevel"); 11 | 12 | Option READ_ONLY = Option.valueOf("readOnly"); 13 | 14 | Option NAME = Option.valueOf("name"); 15 | 16 | Option LOCK_WAIT_TIMEOUT = Option.valueOf("lockWaitTimeout"); 17 | 18 | @Nullable 19 | T getAttribute(Option option); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/reactive/connection/TransactionUtils.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.reactive.connection; 2 | 3 | import static org.springframework.transaction.reactive.TransactionSynchronizationManager.forCurrentTransaction; 4 | 5 | import java.util.Objects; 6 | import org.hibernate.reactive.stage.Stage; 7 | import reactor.core.publisher.Mono; 8 | 9 | public class TransactionUtils { 10 | 11 | private TransactionUtils() {} 12 | 13 | public static Mono isTransactionAvailable(Stage.SessionFactory sessionFactory) { 14 | return forCurrentTransaction() 15 | .mapNotNull(tsm -> tsm.getResource(sessionFactory)) 16 | .filter(ConnectionHolder.class::isInstance) 17 | .onErrorResume(e -> Mono.empty()) 18 | .map(Objects::nonNull) 19 | .defaultIfEmpty(Boolean.FALSE); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/htech/jpa/support/EntityManagerFactoryBeanCreationExceptionFailureAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.htech.jpa.support; 2 | 3 | import com.htech.jpa.reactive.ReactiveHibernateJpaConfiguration; 4 | import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; 5 | import org.springframework.boot.diagnostics.FailureAnalysis; 6 | import org.springframework.context.EnvironmentAware; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.util.ObjectUtils; 9 | import org.springframework.util.StringUtils; 10 | 11 | /** 12 | * @author Bao.Ngo 13 | */ 14 | public class EntityManagerFactoryBeanCreationExceptionFailureAnalyzer 15 | extends AbstractFailureAnalyzer< 16 | ReactiveHibernateJpaConfiguration.EntityManagerFactoryBeanCreationException> 17 | implements EnvironmentAware { 18 | 19 | private Environment environment; 20 | 21 | @Override 22 | protected FailureAnalysis analyze( 23 | Throwable rootFailure, 24 | ReactiveHibernateJpaConfiguration.EntityManagerFactoryBeanCreationException cause) { 25 | String description = getDescription(cause); 26 | String action = getAction(cause); 27 | return new FailureAnalysis(description, action, cause); 28 | } 29 | 30 | private String getDescription( 31 | ReactiveHibernateJpaConfiguration.EntityManagerFactoryBeanCreationException cause) { 32 | StringBuilder description = new StringBuilder(); 33 | description.append("Failed to configure a EntityManagerFactoryBean: "); 34 | if (!StringUtils.hasText(cause.getJpaProps().get("jakarta.persistence.jdbc.url"))) { 35 | description.append("'url' attribute is not specified."); 36 | } 37 | // description.append(String.format("no embedded database could be configured.%n")); 38 | description.append(String.format("%nReason: %s%n", cause.getMessage())); 39 | return description.toString(); 40 | } 41 | 42 | private String getAction( 43 | ReactiveHibernateJpaConfiguration.EntityManagerFactoryBeanCreationException cause) { 44 | StringBuilder action = new StringBuilder(); 45 | action.append(String.format("Consider the following:%n")); 46 | 47 | action.append( 48 | String.format( 49 | "\tReview the configuration of %s.\n", 50 | "spring.jpa.properties.jakarta.persistence.jdbc.url")); 51 | action 52 | .append( 53 | "\tIf you have database settings to be loaded from a particular " 54 | + "profile you may need to activate it") 55 | .append(getActiveProfiles()); 56 | return action.toString(); 57 | } 58 | 59 | private String getActiveProfiles() { 60 | StringBuilder message = new StringBuilder(); 61 | String[] profiles = this.environment.getActiveProfiles(); 62 | if (ObjectUtils.isEmpty(profiles)) { 63 | message.append(" (no profiles are currently active)."); 64 | } else { 65 | message.append(" (the profiles "); 66 | message.append(StringUtils.arrayToCommaDelimitedString(profiles)); 67 | message.append(" are currently active)."); 68 | } 69 | return message.toString(); 70 | } 71 | 72 | @Override 73 | public void setEnvironment(Environment environment) { 74 | this.environment = environment; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.htech.jpa.reactive.ReactiveHibernateJpaAutoConfiguration\ 3 | com.htech.data.jpa.reactive.core.ReactiveJpaDataAutoConfiguration\ 4 | com.htech.data.jpa.reactive.repository.auto.ReactiveJpaRepositoriesAutoConfiguration 5 | 6 | org.springframework.boot.diagnostics.FailureAnalyzer=\ 7 | com.htech.jpa.support.EntityManagerFactoryBeanCreationExceptionFailureAnalyzer 8 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.htech.jpa.reactive.ReactiveHibernateJpaAutoConfiguration 2 | com.htech.data.jpa.reactive.core.ReactiveJpaDataAutoConfiguration 3 | com.htech.data.jpa.reactive.repository.auto.ReactiveJpaRepositoriesAutoConfiguration --------------------------------------------------------------------------------