├── .github
└── workflows
│ └── maven-publish.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
└── java
│ ├── com
│ └── joutvhu
│ │ └── dynamic
│ │ └── r2dbc
│ │ ├── DynamicQuery.java
│ │ ├── query
│ │ ├── DynamicR2dbcQueryLookupStrategy.java
│ │ ├── DynamicR2dbcQueryMethod.java
│ │ └── DynamicR2dbcRepositoryQuery.java
│ │ └── support
│ │ ├── DynamicR2dbcRepositoryFactory.java
│ │ └── DynamicR2dbcRepositoryFactoryBean.java
│ └── org
│ └── springframework
│ └── data
│ └── r2dbc
│ └── repository
│ ├── query
│ ├── DynamicR2dbcParameterAccessor.java
│ └── DynamicStringBasedR2dbcQuery.java
│ └── support
│ └── DynamicCachingExpressionParser.java
└── test
├── java
└── com
│ └── joutvhu
│ └── dynamic
│ └── r2dbc
│ ├── R2dbcDynamicApplication.java
│ ├── R2dbcDynamicApplicationTest.java
│ ├── entity
│ ├── TableA.java
│ ├── TableB.java
│ └── TableC.java
│ ├── model
│ ├── ModelC.java
│ └── TableAB.java
│ └── repository
│ ├── TableARepository.java
│ ├── TableBRepository.java
│ └── TableCRepository.java
└── resources
├── application.properties
├── query
└── query.dsql
└── sql
└── table.sql
/.github/workflows/maven-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle
3 |
4 | name: Gradle Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: read
16 | packages: write
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | - name: Set up JDK 17
22 | uses: actions/setup-java@v2
23 | with:
24 | java-version: '17'
25 | distribution: 'adopt'
26 |
27 | - name: Write Secret Key Ring File
28 | uses: joutvhu/write-file@v1
29 | with:
30 | path: secrets/secret-key.gpg
31 | contents: ${{ secrets.GPG_SECRET_KEY_BASE64 }}
32 | write_mode: overwrite
33 | encoding: base64
34 |
35 | - name: Write Gradle Properties File
36 | uses: joutvhu/write-file@v1
37 | with:
38 | path: gradle.properties
39 | contents: ${{ secrets.GRADLE_SECRET_PROPERTIES }}
40 | write_mode: overwrite
41 |
42 | - name: Make gradlew executable
43 | run: chmod +x ./gradlew
44 |
45 | - name: Build with Gradle
46 | run: ./gradlew clean build
47 |
48 | - name: Get Current Release
49 | id: get_current_release
50 | uses: joutvhu/get-release@v1
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 |
54 | - name: Publish to Sonatype Packages
55 | run: ./gradlew publishMavenPublicationToSonatypeRepository
56 |
57 | - name: Publish to GitHub Packages
58 | if: ${{ steps.get_current_release.outputs.prerelease == 'false' && steps.get_current_release.outputs.draft == 'false' }}
59 | run: ./gradlew publishMavenPublicationToGithubRepository
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## OS X ##
2 | .DS_Store
3 |
4 | ## Java ##
5 | *.jar
6 | *.war
7 | *.nar
8 | *.ear
9 | *.zip
10 | *.tar.gz
11 | *.rar
12 | hs_err_pid*
13 |
14 | # Compiled class file
15 | *.class
16 |
17 | # Log file
18 | *.log
19 |
20 | # BlueJ files
21 | *.ctxt
22 |
23 | # Mobile Tools for Java (J2ME)
24 | .mtj.tmp/
25 |
26 | ## Gradle ##
27 | .gradle
28 | /build/
29 | !gradle-wrapper.jar
30 | .gradletasknamecache
31 | gradle-app.setting
32 |
33 | ## Maven ##
34 | target/
35 | pom.xml.tag
36 | pom.xml.releaseBackup
37 | pom.xml.versionsBackup
38 | pom.xml.next
39 | pom.xml.bak
40 | release.properties
41 | dependency-reduced-pom.xml
42 | buildNumber.properties
43 | .mvn/timing.properties
44 | .mvn/wrapper/maven-wrapper.jar
45 |
46 | ### STS ###
47 | .apt_generated
48 | .springBeans
49 | .sts4-cache
50 |
51 | ### IntelliJ IDEA ###
52 | .idea
53 | .idea_modules
54 | .log
55 | .build
56 | *.build
57 | *.iws
58 | *.iml
59 | *.ipr
60 | /out/
61 | /logs/
62 |
63 | ## Eclipse ##
64 | tmp/
65 | .metadata
66 | .classpath
67 | .project
68 | *.tmp
69 | *.bak
70 | *.swp
71 | *~.nib
72 | local.properties
73 | .loadpath
74 | .factorypath
75 |
76 | ### NetBeans ###
77 | /nbproject/private/
78 | /nbbuild/
79 | /dist/
80 | /nbdist/
81 | /.nb-gradle/
82 | .settings
83 | .settings/
84 | build/
85 | bin/
86 | nbactions.xml
87 | nb-configuration.xml
88 |
89 | ## Visual Studio Code ##
90 | .vscode/
91 | .code-workspace
92 |
93 | # Secrets
94 | gradle.properties
95 | secrets
96 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | joutvhu@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Giáo Hồ
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spring Dynamic R2DBC
2 |
3 | The Spring Dynamic R2DBC will make it easy to implement dynamic queries with R2DBC.
4 |
5 | ## How to use?
6 |
7 | ### Install dependency
8 |
9 | - Add dependency
10 |
11 | ```groovy
12 | implementation 'com.github.joutvhu:spring-dynamic-r2dbc:3.0.2'
13 | ```
14 |
15 | ```xml
16 |
17 | com.github.joutvhu
18 | spring-dynamic-r2dbc
19 | 3.0.2
20 |
21 | ```
22 |
23 | - Please choose the _Spring Dynamic R2DBC_ version appropriate with your spring version.
24 |
25 | | Spring Boot version | Spring Dynamic R2DBC version |
26 | |:----------:|:-------------:|
27 | | 2.6.x | 1.4.2 |
28 | | 2.7.x | 1.5.2 |
29 | | 3.0.x | 3.0.2 |
30 |
31 | Also, you have to choose a [Dynamic Query Template Provider](https://github.com/joutvhu/spring-dynamic-commons#dynamic-query-template-provider) to use,
32 | the Dynamic Query Template Provider will decide the style you write dynamic query template.
33 |
34 | In this document, I will use [Spring Dynamic Freemarker](https://github.com/joutvhu/spring-dynamic-freemarker).
35 | If you migrated from a lower version, you should use it.
36 |
37 | ```groovy
38 | implementation 'com.github.joutvhu:spring-dynamic-freemarker:1.0.0'
39 | ```
40 |
41 | ```xml
42 |
43 | com.github.joutvhu
44 | spring-dynamic-freemarker
45 | 1.0.0
46 |
47 | ```
48 |
49 | ### Configuration
50 |
51 | - First you need to create a bean of `DynamicQueryTemplateProvider`, that depending on which the Dynamic Query Template Provider you are using.
52 |
53 | ```java
54 | @Bean
55 | public DynamicQueryTemplateProvider dynamicQueryTemplateProvider() {
56 | FreemarkerQueryTemplateProvider provider = new FreemarkerQueryTemplateProvider();
57 | provider.setTemplateLocation("classpath:/query");
58 | provider.setSuffix(".dsql");
59 | return provider;
60 | }
61 | ```
62 |
63 | - Next, you need to set the r2dbc repository's `repositoryFactoryBeanClass` property to `DynamicR2dbcRepositoryFactoryBean.class`.
64 |
65 | ```java
66 | @EnableR2dbcRepositories(repositoryFactoryBeanClass = DynamicR2dbcRepositoryFactoryBean.class)
67 | ```
68 |
69 | ### Dynamic query
70 |
71 | - Use annotation `@DynamicQuery` to define dynamic queries.
72 |
73 | ```java
74 | public interface UserRepository extends R2dbcRepository {
75 | @DynamicQuery(
76 | value = "select * from USER where FIRST_NAME = :firstName\n" +
77 | "<#if lastName?has_content>\n" +
78 | " and LAST_NAME = :lastName\n" +
79 | "#if>"
80 | )
81 | Flux findUserByNames(Long firstName, String lastName);
82 |
83 | @Query(value = "select * from USER where FIRST_NAME = :firstName")
84 | Flux findByFirstName(String firstName);
85 |
86 | @DynamicQuery(
87 | value = "select USER_ID from USER\n" +
88 | "<#if name??>\n" +
89 | " where concat(FIRST_NAME, ' ', LAST_NAME) like %:name%\n" +
90 | "#if>"
91 | )
92 | Flux searchIdsByName(String name);
93 |
94 | @DynamicQuery(
95 | value = "select * from USER\n" +
96 | "<#if role??>\n" +
97 | " where ROLE = :role\n" +
98 | "#if>"
99 | )
100 | Flux findByRole(String role);
101 | }
102 | ```
103 |
104 | ### Load query template files
105 |
106 | - If you do not specify the query template on the `@DynamicQuery` annotation.
107 | The `DynamicQueryTemplateProvider` will find them from external template files based on the `TemplateLocation` and `Suffix` that you specify in the provider.
108 |
109 | - If you don't want to load the template from external template files you can use the following code `provider.setSuffix(null);`.
110 |
111 | - Each template will start with a template name definition line. The template name definition line must be start with two dash characters (`--`). The template name will have the following syntax.
112 |
113 | ```
114 | queryMethodName
115 | ```
116 |
117 | - `queryMethodName` can be provided through field `@DynamicQuery.name`. If `@DynamicQuery.name` is not provided, `queryMethodName` will be `entityName:methodName` where `entityName` is entity class name, `methodName` is query method name
118 |
119 | - Query templates (Ex: `resoucers/query/user-query.dsql`)
120 |
121 | ```sql
122 | --User:findUserByNames
123 | select * from USER where FIRST_NAME = :firstName
124 | <#if lastName?has_content>
125 | and LAST_NAME = :lastName
126 | #if>
127 |
128 | -- User:searchIdsByName
129 | select USER_ID from USER
130 | <#if name??>
131 | where concat(FIRST_NAME, ' ', LAST_NAME) like %:name%
132 | #if>
133 |
134 | -- get_user_by_username_and_email
135 | select * from USER
136 | <@where>
137 | <#if username??>
138 | and USERNAME = :username
139 | #if>
140 | <#if email??>
141 | and EMAIL = :email
142 | #if>
143 | @where>
144 | ```
145 |
146 | - Now you don't need to specify the query template on `@DynamicQuery` annotation.
147 |
148 | ```java
149 | public interface UserRepository extends ReactiveCrudRepository {
150 | @DynamicQuery
151 | Flux findUserByNames(Long firstName, String lastName);
152 |
153 | @Query(value = "select * from USER where FIRST_NAME = :firstName")
154 | Flux findByFirstName(String firstName);
155 |
156 | @DynamicQuery
157 | Flux searchIdsByName(String name);
158 |
159 | @DynamicQuery(name = "get_user_by_username_and_email")
160 | Flux getUserWithUsernameAndEmail(String username, String email);
161 | }
162 | ```
163 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'idea'
4 | id 'groovy'
5 | id 'signing'
6 | id 'java-library'
7 | id 'maven-publish'
8 | id 'org.springframework.boot' version '3.0.0'
9 | id 'io.spring.dependency-management' version '1.1.0'
10 | }
11 |
12 | group = 'com.github.joutvhu'
13 | version = '3.0.2'
14 |
15 | def snapshotVersion = version.endsWith('-SNAPSHOT') || version.endsWith('.SNAPSHOT')
16 |
17 | configurations {
18 | compileOnly {
19 | extendsFrom annotationProcessor
20 | }
21 | }
22 |
23 | repositories {
24 | mavenLocal()
25 | mavenCentral()
26 | }
27 |
28 | ext {
29 | versions = [
30 | 'spring-boot.version' : '3.0.0',
31 | 'spring-framework.version' : '6.0.2',
32 | 'spring-data-r2dbc.version' : '3.0.0',
33 | 'spring-dynamic-commons.version': '2.0.0'
34 | ]
35 | managedVersions = [
36 | 'org.springframework:spring-aspects' : 'spring-framework.version',
37 | 'org.springframework:spring-context' : 'spring-framework.version',
38 | 'org.springframework:spring-jcl' : 'spring-framework.version',
39 | 'org.springframework:spring-context-support' : 'spring-framework.version',
40 | 'org.springframework.data:spring-data-commons': 'spring-boot.version',
41 | 'org.springframework.data:spring-data-r2dbc' : 'spring-data-r2dbc.version',
42 | 'com.github.joutvhu:spring-dynamic-commons' : 'spring-dynamic-commons.version'
43 | ]
44 | otherVersions = [
45 | 'org.springframework.boot:spring-boot-dependencies': 'spring-boot.version'
46 | ]
47 | optionalDependencies = [
48 | ]
49 | }
50 |
51 | dependencyManagement {
52 | dependencies {
53 | ext.managedVersions.each {
54 | dependency "${it.key}:${ext.versions[it.value]}"
55 | }
56 | }
57 | }
58 |
59 | dependencies {
60 | implementation 'org.springframework:spring-context-support'
61 |
62 | implementation 'org.springframework.data:spring-data-r2dbc'
63 | implementation 'org.springframework.data:spring-data-commons'
64 |
65 | implementation 'com.github.joutvhu:spring-dynamic-commons'
66 |
67 | compileOnly 'org.projectlombok:lombok:1.18.24'
68 | annotationProcessor 'org.projectlombok:lombok:1.18.24'
69 |
70 | testImplementation(platform('org.junit:junit-bom:5.8.2'))
71 | testImplementation 'org.junit.jupiter:junit-jupiter-api'
72 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
73 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
74 |
75 | testImplementation('org.springframework.boot:spring-boot-starter-test') {
76 | exclude group: 'junit', module: 'junit'
77 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
78 | exclude group: 'org.apache.logging.log4j', module: 'log4j-api'
79 | exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j'
80 | }
81 | testImplementation 'com.h2database:h2'
82 | testImplementation 'io.r2dbc:r2dbc-h2'
83 | testImplementation 'io.projectreactor:reactor-test'
84 | testImplementation 'com.github.joutvhu:spring-dynamic-freemarker:1.0.0'
85 |
86 | testImplementation 'org.springframework:spring-context-support'
87 |
88 | testCompileOnly 'org.projectlombok:lombok:1.18.24'
89 | testAnnotationProcessor 'org.projectlombok:lombok:1.18.24'
90 | }
91 |
92 | jar {
93 | enabled = true
94 | manifest {
95 | attributes 'Built-By': 'joutvhu (Giao Ho)'
96 | attributes 'Build-Date': java.time.Instant.now().toString()
97 | attributes 'Bundle-Name': 'Spring Dynamic R2DBC'
98 | attributes 'Bundle-Vendor': project.group
99 | attributes 'Bundle-SymbolicName': project.name
100 | attributes 'Bundle-Version': project.version
101 | }
102 | into("META-INF/maven/${project.group}/${project.name}") {
103 | from {
104 | generatePomFileForMavenPublication
105 | }
106 | rename {
107 | it.replace('pom-default.xml', 'pom.xml')
108 | }
109 | }
110 | }
111 |
112 | bootJar {
113 | enabled = false
114 | }
115 |
116 | task fatJar(type: Jar) {
117 | manifest.from jar.manifest
118 | with jar
119 | }
120 |
121 | task sourcesJar(type: Jar, dependsOn: classes) {
122 | archiveClassifier = 'sources'
123 | from sourceSets.main.allSource
124 | }
125 |
126 | task javadocJar(type: Jar, dependsOn: javadoc) {
127 | archiveClassifier = 'javadoc'
128 | from javadoc.destinationDir
129 | }
130 |
131 | artifacts {
132 | archives fatJar, sourcesJar, javadocJar
133 | }
134 |
135 | test {
136 | useJUnitPlatform()
137 | filter {
138 | includeTestsMatching 'com.joutvhu.dynamic.r2dbc.*'
139 | }
140 | }
141 |
142 | publishing {
143 | publications {
144 | maven(MavenPublication) {
145 | groupId = group
146 | artifactId = project.name
147 | artifacts = [fatJar, sourcesJar, javadocJar]
148 | version = version
149 |
150 | pom {
151 | name = project.name
152 | description = 'The Spring Dynamic R2DBC will make it easy to implement dynamic queries with R2DBC'
153 | url = 'https://github.com/joutvhu/spring-dynamic-r2dbc'
154 | licenses {
155 | license {
156 | name = 'MIT License'
157 | url = 'https://github.com/joutvhu/spring-dynamic-r2dbc/blob/main/LICENSE'
158 | }
159 | }
160 | developers {
161 | developer {
162 | id = 'joutvhu'
163 | name = 'Giao Ho'
164 | email = 'joutvhu@gmail.com'
165 | }
166 | }
167 | scm {
168 | connection = 'scm:git:git@github.com:joutvhu/spring-dynamic-r2dbc.git'
169 | developerConnection = 'scm:git:git@github.com:joutvhu/spring-dynamic-r2dbc.git'
170 | url = 'https://github.com/joutvhu/spring-dynamic-r2dbc'
171 | }
172 | issueManagement {
173 | system = 'Github Issue'
174 | url = 'https://github.com/joutvhu/spring-dynamic-r2dbc/issues'
175 | }
176 | organization {
177 | name = 'Giao Ho'
178 | url = 'https://github.com/joutvhu'
179 | }
180 | properties = project.ext.versions instanceof Map ? project.ext.versions : new HashMap<>()
181 | withXml {
182 | def pomNode = asNode();
183 | def dependencyManagement = pomNode.get('dependencyManagement')
184 | if (dependencyManagement.dependencies.dependency.isEmpty()) {
185 | pomNode.remove(dependencyManagement)
186 | } else {
187 | dependencyManagement.dependencies.dependency.each {
188 | def key = "${it.groupId.text()}:${it.artifactId.text()}"
189 | def versionProperty = project.ext.managedVersions[key]
190 | if (versionProperty == null) {
191 | versionProperty = project.ext.otherVersions[key]
192 | }
193 | if (versionProperty) {
194 | it.version[0].setValue("\${$versionProperty}")
195 | }
196 | }
197 | }
198 |
199 | def dependenciesNode = pomNode.appendNode('dependencies')
200 | if (configurations.implementation.allDependencies.isEmpty()) {
201 | pomNode.remove(dependenciesNode)
202 | } else {
203 | configurations.implementation.allDependencies.each {
204 | def dependencyNode = dependenciesNode.appendNode('dependency')
205 | dependencyNode.appendNode('groupId', it.group)
206 | dependencyNode.appendNode('artifactId', it.name)
207 | if (it.version != null) {
208 | dependencyNode.appendNode('version', it.version)
209 | }
210 | def key = "${it.group}:${it.name}"
211 | if (project.ext.optionalDependencies.any { el -> el.contains(key) }) {
212 | dependencyNode.appendNode('optional', true)
213 | }
214 | }
215 | }
216 | }
217 | }
218 | }
219 | }
220 | repositories {
221 | maven {
222 | name = 'sonatype'
223 | if (snapshotVersion) {
224 | url = 'https://oss.sonatype.org/content/repositories/snapshots'
225 | } else {
226 | url = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
227 | }
228 | credentials {
229 | username = project.ossrhUsername
230 | password = project.ossrhPassword
231 | }
232 | }
233 | maven {
234 | name = 'github'
235 | url = "https://maven.pkg.github.com/joutvhu/spring-dynamic-r2dbc"
236 | credentials {
237 | username = project.githubUsername
238 | password = project.githubPassword
239 | }
240 | }
241 | }
242 | }
243 |
244 | signing {
245 | sign publishing.publications.maven
246 | }
247 |
248 | tasks.publishMavenPublicationToGithubRepository.configure {
249 | onlyIf { !snapshotVersion }
250 | }
251 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joutvhu/spring-dynamic-r2dbc/e6f38f517ca47674844443fcd7d67216f35a6799/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
4 | networkTimeout=10000
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 | # Collect all arguments for the java command;
201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
202 | # shell script including quotes and variable substitutions, so put them in
203 | # double quotes to make sure that they get re-expanded; and
204 | # * put everything else in single quotes, so that it's not re-expanded.
205 |
206 | set -- \
207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
208 | -classpath "$CLASSPATH" \
209 | org.gradle.wrapper.GradleWrapperMain \
210 | "$@"
211 |
212 | # Stop when "xargs" is not available.
213 | if ! command -v xargs >/dev/null 2>&1
214 | then
215 | die "xargs is not available"
216 | fi
217 |
218 | # Use "xargs" to parse quoted args.
219 | #
220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
221 | #
222 | # In Bash we could simply go:
223 | #
224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
225 | # set -- "${ARGS[@]}" "$@"
226 | #
227 | # but POSIX shell has neither arrays nor command substitution, so instead we
228 | # post-process each arg (as a line of input to sed) to backslash-escape any
229 | # character that might be a shell metacharacter, then use eval to reverse
230 | # that process (while maintaining the separation between arguments), and wrap
231 | # the whole thing up as a single "set" statement.
232 | #
233 | # This will of course break if any of these variables contains a newline or
234 | # an unmatched quote.
235 | #
236 |
237 | eval "set -- $(
238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
239 | xargs -n1 |
240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
241 | tr '\n' ' '
242 | )" '"$@"'
243 |
244 | exec "$JAVACMD" "$@"
245 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'spring-dynamic-r2dbc'
2 |
--------------------------------------------------------------------------------
/src/main/java/com/joutvhu/dynamic/r2dbc/DynamicQuery.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc;
2 |
3 | import org.springframework.data.annotation.QueryAnnotation;
4 |
5 | import java.lang.annotation.*;
6 |
7 | /**
8 | * Annotation to declare finder dynamic queries directly on repository methods.
9 | *
10 | * @author Giao Ho
11 | * @since 1.5.0
12 | */
13 | @Retention(RetentionPolicy.RUNTIME)
14 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
15 | @QueryAnnotation
16 | @Documented
17 | public @interface DynamicQuery {
18 | /**
19 | * Provides a query template method name, which is used to find external query templates.
20 | * The default is {@code entityName:methodName}, entityName is entity class name, methodName is query method name.
21 | *
22 | * @return the query template method name
23 | * @since x.x.8
24 | */
25 | String name() default "";
26 |
27 | /**
28 | * The SQL statement to execute when the annotated method gets invoked.
29 | */
30 | String value() default "";
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/joutvhu/dynamic/r2dbc/query/DynamicR2dbcQueryLookupStrategy.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.query;
2 |
3 | import com.joutvhu.dynamic.r2dbc.DynamicQuery;
4 | import org.springframework.data.projection.ProjectionFactory;
5 | import org.springframework.data.r2dbc.convert.R2dbcConverter;
6 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
7 | import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
8 | import org.springframework.data.r2dbc.repository.support.DynamicCachingExpressionParser;
9 | import org.springframework.data.repository.core.NamedQueries;
10 | import org.springframework.data.repository.core.RepositoryMetadata;
11 | import org.springframework.data.repository.query.QueryLookupStrategy;
12 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
13 | import org.springframework.data.repository.query.RepositoryQuery;
14 | import org.springframework.expression.ExpressionParser;
15 | import org.springframework.expression.spel.standard.SpelExpressionParser;
16 |
17 | import java.lang.reflect.Method;
18 |
19 | /**
20 | * {@link QueryLookupStrategy} that tries to detect a dynamic query declared via {@link DynamicQuery} annotation.
21 | *
22 | * @author Giao Ho
23 | * @since 1.5.0
24 | */
25 | public class DynamicR2dbcQueryLookupStrategy implements QueryLookupStrategy {
26 | private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
27 |
28 | private final R2dbcEntityOperations entityOperations;
29 | private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
30 | private final R2dbcConverter converter;
31 | private final ReactiveDataAccessStrategy dataAccessStrategy;
32 | private final ExpressionParser parser = new DynamicCachingExpressionParser(EXPRESSION_PARSER);
33 | private final QueryLookupStrategy r2dbcQueryLookupStrategy;
34 |
35 | public DynamicR2dbcQueryLookupStrategy(
36 | R2dbcEntityOperations entityOperations,
37 | ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider,
38 | R2dbcConverter converter,
39 | ReactiveDataAccessStrategy dataAccessStrategy,
40 | QueryLookupStrategy r2dbcQueryLookupStrategy
41 | ) {
42 | this.entityOperations = entityOperations;
43 | this.evaluationContextProvider = evaluationContextProvider;
44 | this.converter = converter;
45 | this.dataAccessStrategy = dataAccessStrategy;
46 | this.r2dbcQueryLookupStrategy = r2dbcQueryLookupStrategy;
47 | }
48 |
49 | @Override
50 | public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) {
51 | if (isMethodDynamicJpaHandle(method)) {
52 | DynamicR2dbcQueryMethod queryMethod = new DynamicR2dbcQueryMethod(method, metadata, factory, this.converter.getMappingContext());
53 | return new DynamicR2dbcRepositoryQuery(
54 | queryMethod,
55 | this.entityOperations,
56 | this.converter,
57 | this.dataAccessStrategy,
58 | this.parser,
59 | this.evaluationContextProvider
60 | );
61 | } else {
62 | return r2dbcQueryLookupStrategy.resolveQuery(method, metadata, factory, namedQueries);
63 | }
64 | }
65 |
66 | private boolean isMethodDynamicJpaHandle(Method method) {
67 | DynamicQuery annotation = method.getAnnotation(DynamicQuery.class);
68 | return annotation != null;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/com/joutvhu/dynamic/r2dbc/query/DynamicR2dbcQueryMethod.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.query;
2 |
3 | import com.joutvhu.dynamic.commons.DynamicQueryTemplate;
4 | import com.joutvhu.dynamic.commons.DynamicQueryTemplateProvider;
5 | import com.joutvhu.dynamic.commons.util.ApplicationContextHolder;
6 | import com.joutvhu.dynamic.r2dbc.DynamicQuery;
7 | import org.springframework.core.annotation.AnnotatedElementUtils;
8 | import org.springframework.core.annotation.AnnotationUtils;
9 | import org.springframework.data.mapping.context.MappingContext;
10 | import org.springframework.data.projection.ProjectionFactory;
11 | import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod;
12 | import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
13 | import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
14 | import org.springframework.data.repository.core.RepositoryMetadata;
15 | import org.springframework.lang.Nullable;
16 | import org.springframework.util.StringUtils;
17 |
18 | import java.lang.reflect.Method;
19 | import java.util.HashMap;
20 | import java.util.Map;
21 |
22 | /**
23 | * Reactive specific implementation of {@link R2dbcQueryMethod}.
24 | *
25 | * @author Giao Ho
26 | * @since 1.5.0
27 | */
28 | public class DynamicR2dbcQueryMethod extends R2dbcQueryMethod {
29 | private static final Map templateMap = new HashMap<>();
30 |
31 | private final Method method;
32 | private final DynamicQuery query;
33 |
34 | private DynamicQueryTemplateProvider queryTemplateProvider;
35 | private DynamicQueryTemplate queryTemplate;
36 |
37 | static {
38 | templateMap.put("value", "");
39 | templateMap.put("countQuery", "count");
40 | templateMap.put("countProjection", "projection");
41 | }
42 |
43 | protected DynamicR2dbcQueryMethod(
44 | Method method,
45 | RepositoryMetadata metadata,
46 | ProjectionFactory projectionFactory,
47 | MappingContext extends RelationalPersistentEntity>, ? extends RelationalPersistentProperty> mappingContext
48 | ) {
49 | super(method, metadata, projectionFactory, mappingContext);
50 | this.method = method;
51 | this.query = AnnotatedElementUtils.findMergedAnnotation(method, DynamicQuery.class);
52 | }
53 |
54 | public DynamicQueryTemplateProvider getTemplateProvider() {
55 | if (queryTemplateProvider == null)
56 | queryTemplateProvider = ApplicationContextHolder.getBean(DynamicQueryTemplateProvider.class);
57 | return queryTemplateProvider;
58 | }
59 |
60 | protected DynamicQueryTemplate findTemplate(String name) {
61 | DynamicQueryTemplateProvider provider = getTemplateProvider();
62 | return provider != null ? provider.findTemplate(name) : null;
63 | }
64 |
65 | protected DynamicQueryTemplate createTemplate(String name, String content) {
66 | DynamicQueryTemplateProvider provider = getTemplateProvider();
67 | return provider != null ? provider.createTemplate(name, content) : null;
68 | }
69 |
70 | protected DynamicQueryTemplate getTemplate(String name) {
71 | String templateName = templateMap.get(name);
72 | if (StringUtils.hasText(templateName)) templateName = "." + templateName;
73 | String templateMethodName = getMergedOrDefaultAnnotationValue("name", DynamicQuery.class, String.class);
74 | if (!StringUtils.hasText(templateMethodName)) templateMethodName = getTemplateKey();
75 | templateName = templateMethodName + templateName;
76 | String query = getMergedOrDefaultAnnotationValue(name, DynamicQuery.class, String.class);
77 | return StringUtils.hasText(query) ? createTemplate(templateName, query) : findTemplate(templateName);
78 | }
79 |
80 | @Nullable
81 | public DynamicQueryTemplate getQueryTemplate() {
82 | if (queryTemplate == null)
83 | queryTemplate = getTemplate("value");
84 | return queryTemplate;
85 | }
86 |
87 | private String getEntityName() {
88 | return getEntityInformation().getJavaType().getSimpleName();
89 | }
90 |
91 | private String getTemplateKey() {
92 | return getEntityName() + ":" + getName();
93 | }
94 |
95 | @SuppressWarnings({"rawtypes", "unchecked"})
96 | private T getMergedOrDefaultAnnotationValue(String attribute, Class annotationType, Class targetType) {
97 | if (this.query == null)
98 | return targetType.cast(AnnotationUtils.getDefaultValue(annotationType, attribute));
99 | return targetType.cast(AnnotationUtils.getValue(this.query, attribute));
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/main/java/com/joutvhu/dynamic/r2dbc/query/DynamicR2dbcRepositoryQuery.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.query;
2 |
3 | import com.joutvhu.dynamic.commons.DynamicQueryTemplate;
4 | import com.joutvhu.dynamic.r2dbc.DynamicQuery;
5 | import org.springframework.data.r2dbc.convert.R2dbcConverter;
6 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
7 | import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
8 | import org.springframework.data.r2dbc.repository.query.DynamicR2dbcParameterAccessor;
9 | import org.springframework.data.r2dbc.repository.query.DynamicStringBasedR2dbcQuery;
10 | import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
11 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
12 | import org.springframework.data.repository.query.RepositoryQuery;
13 | import org.springframework.expression.ExpressionParser;
14 | import org.springframework.r2dbc.core.PreparedOperation;
15 | import reactor.core.publisher.Mono;
16 |
17 | import java.util.Map;
18 |
19 | /**
20 | * {@link RepositoryQuery} implementation that inspects a {@link DynamicR2dbcQueryMethod}
21 | * for the existence of an {@link DynamicQuery} annotation and creates a R2dbc {@link DynamicQuery} from it.
22 | *
23 | * @author Giao Ho
24 | * @since 1.5.0
25 | */
26 | public class DynamicR2dbcRepositoryQuery extends DynamicStringBasedR2dbcQuery {
27 | private final DynamicR2dbcQueryMethod method;
28 |
29 | public DynamicR2dbcRepositoryQuery(
30 | DynamicR2dbcQueryMethod method,
31 | R2dbcEntityOperations entityOperations,
32 | R2dbcConverter converter,
33 | ReactiveDataAccessStrategy dataAccessStrategy,
34 | ExpressionParser expressionParser,
35 | ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider
36 | ) {
37 | super(method, entityOperations, converter, dataAccessStrategy, expressionParser, evaluationContextProvider);
38 | this.method = method;
39 | }
40 |
41 | @Override
42 | protected boolean isModifyingQuery() {
43 | return getQueryMethod().isModifyingQuery();
44 | }
45 |
46 | @Override
47 | protected boolean isCountQuery() {
48 | return false;
49 | }
50 |
51 | @Override
52 | protected boolean isExistsQuery() {
53 | return false;
54 | }
55 |
56 | @Override
57 | protected Mono> createQuery(RelationalParameterAccessor accessor) {
58 | String queryString = buildQuery(method.getQueryTemplate(), accessor);
59 | return super.createQuery(queryString, accessor);
60 | }
61 |
62 | protected String buildQuery(DynamicQueryTemplate template, RelationalParameterAccessor accessor) {
63 | try {
64 | if (template != null) {
65 | Map model = DynamicR2dbcParameterAccessor.of(method, accessor).getParamModel();
66 | String queryString = template.process(model)
67 | .replaceAll("\n", " ")
68 | .replaceAll("\t", " ")
69 | .replaceAll(" +", " ")
70 | .trim();
71 | return queryString.isEmpty() ? null : queryString;
72 | }
73 | } catch (Exception e) {
74 | e.printStackTrace();
75 | }
76 | return null;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/com/joutvhu/dynamic/r2dbc/support/DynamicR2dbcRepositoryFactory.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.support;
2 |
3 | import com.joutvhu.dynamic.r2dbc.query.DynamicR2dbcQueryLookupStrategy;
4 | import org.springframework.data.r2dbc.convert.R2dbcConverter;
5 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
6 | import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
7 | import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
8 | import org.springframework.data.r2dbc.repository.R2dbcRepository;
9 | import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory;
10 | import org.springframework.data.repository.query.QueryLookupStrategy;
11 | import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
12 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
13 | import org.springframework.r2dbc.core.DatabaseClient;
14 |
15 | import java.util.Optional;
16 |
17 | /**
18 | * Factory to create {@link R2dbcRepository} instances.
19 | *
20 | * @author Giao Ho
21 | * @since 1.5.0
22 | */
23 | public class DynamicR2dbcRepositoryFactory extends R2dbcRepositoryFactory {
24 | private final ReactiveDataAccessStrategy dataAccessStrategy;
25 | private final R2dbcConverter converter;
26 | private final R2dbcEntityOperations operations;
27 |
28 | public DynamicR2dbcRepositoryFactory(DatabaseClient databaseClient, ReactiveDataAccessStrategy dataAccessStrategy) {
29 | this(new R2dbcEntityTemplate(databaseClient, dataAccessStrategy));
30 | }
31 |
32 | public DynamicR2dbcRepositoryFactory(R2dbcEntityOperations operations) {
33 | super(operations);
34 | this.dataAccessStrategy = operations.getDataAccessStrategy();
35 | this.converter = dataAccessStrategy.getConverter();
36 | this.operations = operations;
37 | }
38 |
39 | @Override
40 | protected Optional getQueryLookupStrategy(QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) {
41 | Optional queryLookupStrategy = super.getQueryLookupStrategy(key, evaluationContextProvider);
42 | if (queryLookupStrategy.isPresent()) {
43 | return Optional.of(new DynamicR2dbcQueryLookupStrategy(
44 | this.operations,
45 | (ReactiveQueryMethodEvaluationContextProvider) evaluationContextProvider,
46 | this.converter,
47 | this.dataAccessStrategy,
48 | queryLookupStrategy.get()
49 | ));
50 | }
51 | return queryLookupStrategy;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/com/joutvhu/dynamic/r2dbc/support/DynamicR2dbcRepositoryFactoryBean.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.support;
2 |
3 | import com.joutvhu.dynamic.commons.util.ApplicationContextHolder;
4 | import org.springframework.beans.BeansException;
5 | import org.springframework.context.ApplicationContext;
6 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
7 | import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
8 | import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactoryBean;
9 | import org.springframework.data.repository.Repository;
10 | import org.springframework.data.repository.core.support.RepositoryFactorySupport;
11 | import org.springframework.r2dbc.core.DatabaseClient;
12 |
13 | import java.io.Serializable;
14 |
15 | /**
16 | * Special adapter for Springs {@link DynamicR2dbcRepositoryFactoryBean} interface to allow easy setup of
17 | * repository factories via Spring configuration.
18 | *
19 | * @author Giao Ho
20 | * @since 1.5.0
21 | */
22 | public class DynamicR2dbcRepositoryFactoryBean, S, ID extends Serializable> extends R2dbcRepositoryFactoryBean {
23 | /**
24 | * Creates a new {@link DynamicR2dbcRepositoryFactoryBean} for the given repository interface.
25 | *
26 | * @param repositoryInterface must not be {@literal null}.
27 | */
28 | public DynamicR2dbcRepositoryFactoryBean(Class extends T> repositoryInterface) {
29 | super(repositoryInterface);
30 | }
31 |
32 | @Override
33 | protected RepositoryFactorySupport getFactoryInstance(DatabaseClient client, ReactiveDataAccessStrategy dataAccessStrategy) {
34 | return new DynamicR2dbcRepositoryFactory(client, dataAccessStrategy);
35 | }
36 |
37 | protected RepositoryFactorySupport getFactoryInstance(R2dbcEntityOperations operations) {
38 | return new DynamicR2dbcRepositoryFactory(operations);
39 | }
40 |
41 | @Override
42 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
43 | ApplicationContextHolder.appContext = applicationContext;
44 | super.setApplicationContext(applicationContext);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/data/r2dbc/repository/query/DynamicR2dbcParameterAccessor.java:
--------------------------------------------------------------------------------
1 | package org.springframework.data.r2dbc.repository.query;
2 |
3 | import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
4 | import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
5 | import org.springframework.data.repository.query.Parameters;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 |
10 | public class DynamicR2dbcParameterAccessor extends R2dbcParameterAccessor {
11 | private RelationalParametersParameterAccessor accessor;
12 |
13 | private DynamicR2dbcParameterAccessor(R2dbcQueryMethod method, RelationalParametersParameterAccessor accessor) {
14 | super(method, accessor.getValues());
15 | this.accessor = accessor;
16 | }
17 |
18 | public static DynamicR2dbcParameterAccessor of(R2dbcQueryMethod method, RelationalParameterAccessor accessor) {
19 | assert (accessor instanceof RelationalParametersParameterAccessor);
20 | return new DynamicR2dbcParameterAccessor(method, (RelationalParametersParameterAccessor) accessor);
21 | }
22 |
23 | /**
24 | * Get map param with value
25 | *
26 | * @return a map
27 | */
28 | public Map getParamModel() {
29 | if (this.accessor != null)
30 | return getParamModel(this.accessor);
31 | return getParamModel(this);
32 | }
33 |
34 | private Map getParamModel(RelationalParametersParameterAccessor accessor) {
35 | Map result = new HashMap<>();
36 | Parameters, ?> parameters = accessor.getParameters();
37 | Object[] values = accessor.getValues();
38 | parameters.forEach(parameter -> {
39 | Object value = values[parameter.getIndex()];
40 | if (value != null && parameter.isBindable()) {
41 | String key = parameter.getName().orElse(String.valueOf(parameter.getIndex()));
42 | result.put(key, value);
43 | }
44 | });
45 | return result;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/data/r2dbc/repository/query/DynamicStringBasedR2dbcQuery.java:
--------------------------------------------------------------------------------
1 | package org.springframework.data.r2dbc.repository.query;
2 |
3 | import org.springframework.data.r2dbc.convert.R2dbcConverter;
4 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
5 | import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
6 | import org.springframework.data.r2dbc.dialect.BindTargetBinder;
7 | import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
8 | import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
9 | import org.springframework.data.repository.query.ResultProcessor;
10 | import org.springframework.data.spel.ExpressionDependencies;
11 | import org.springframework.expression.ExpressionParser;
12 | import org.springframework.r2dbc.core.Parameter;
13 | import org.springframework.r2dbc.core.PreparedOperation;
14 | import org.springframework.r2dbc.core.binding.BindTarget;
15 | import reactor.core.publisher.Mono;
16 |
17 | import java.util.ArrayList;
18 | import java.util.LinkedHashMap;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | public abstract class DynamicStringBasedR2dbcQuery extends AbstractR2dbcQuery {
23 | protected final ReactiveDataAccessStrategy dataAccessStrategy;
24 | protected final ExpressionParser expressionParser;
25 | protected final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
26 |
27 | /**
28 | * Creates a new {@link AbstractR2dbcQuery} from the given {@link R2dbcQueryMethod} and {@link R2dbcEntityOperations}.
29 | *
30 | * @param method must not be {@literal null}.
31 | * @param entityOperations must not be {@literal null}.
32 | * @param converter must not be {@literal null}.
33 | * @since 1.4
34 | */
35 | public DynamicStringBasedR2dbcQuery(
36 | R2dbcQueryMethod method,
37 | R2dbcEntityOperations entityOperations,
38 | R2dbcConverter converter,
39 | ReactiveDataAccessStrategy dataAccessStrategy,
40 | ExpressionParser expressionParser,
41 | ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider
42 | ) {
43 | super(method, entityOperations, converter);
44 | this.dataAccessStrategy = dataAccessStrategy;
45 | this.expressionParser = expressionParser;
46 | this.evaluationContextProvider = evaluationContextProvider;
47 | }
48 |
49 | protected Mono> createQuery(String queryString, RelationalParameterAccessor accessor) {
50 | ExpressionQuery expressionQuery = ExpressionQuery.create(queryString);
51 | ExpressionDependencies expressionDependencies = createExpressionDependencies(expressionQuery);
52 | ExpressionEvaluatingParameterBinder binder = new ExpressionEvaluatingParameterBinder(expressionQuery, dataAccessStrategy);
53 | return getSpelEvaluator(accessor, expressionDependencies)
54 | .map(evaluator -> new ExpandedQuery(expressionQuery, binder, accessor, evaluator));
55 | }
56 |
57 | public ExpressionDependencies createExpressionDependencies(ExpressionQuery expressionQuery) {
58 | if (expressionQuery.getBindings().isEmpty()) {
59 | return ExpressionDependencies.none();
60 | }
61 | List dependencies = new ArrayList<>();
62 | for (ExpressionQuery.ParameterBinding binding : expressionQuery.getBindings()) {
63 | dependencies.add(ExpressionDependencies.discover(expressionParser.parseExpression(binding.getExpression())));
64 | }
65 | return ExpressionDependencies.merged(dependencies);
66 | }
67 |
68 | protected Mono getSpelEvaluator(
69 | RelationalParameterAccessor accessor,
70 | ExpressionDependencies expressionDependencies
71 | ) {
72 | return evaluationContextProvider
73 | .getEvaluationContextLater(getQueryMethod().getParameters(), accessor.getValues(), expressionDependencies)
74 | .map(context -> new DefaultR2dbcSpELExpressionEvaluator(expressionParser, context))
75 | .defaultIfEmpty(DefaultR2dbcSpELExpressionEvaluator.unsupported());
76 | }
77 |
78 | @Override
79 | Class> resolveResultType(ResultProcessor resultProcessor) {
80 | Class> returnedType = resultProcessor.getReturnedType().getReturnedType();
81 | return !returnedType.isInterface() ? returnedType : super.resolveResultType(resultProcessor);
82 | }
83 |
84 | protected class ExpandedQuery implements PreparedOperation {
85 | private final ExpressionQuery expressionQuery;
86 | private final BindTargetRecorder recordedBindings;
87 | private final PreparedOperation> expanded;
88 | private final Map remainderByName;
89 | private final Map remainderByIndex;
90 |
91 | public ExpandedQuery(
92 | ExpressionQuery expressionQuery,
93 | ExpressionEvaluatingParameterBinder binder,
94 | RelationalParameterAccessor accessor,
95 | R2dbcSpELExpressionEvaluator evaluator
96 | ) {
97 | this.expressionQuery = expressionQuery;
98 | this.recordedBindings = new BindTargetRecorder();
99 | binder.bind(recordedBindings, accessor, evaluator);
100 |
101 | remainderByName = new LinkedHashMap<>(recordedBindings.byName);
102 | remainderByIndex = new LinkedHashMap<>(recordedBindings.byIndex);
103 | expanded = dataAccessStrategy.processNamedParameters(expressionQuery.getQuery(), (index, name) -> {
104 | if (recordedBindings.byName.containsKey(name)) {
105 | remainderByName.remove(name);
106 | return recordedBindings.byName.get(name);
107 | }
108 |
109 | if (recordedBindings.byIndex.containsKey(index)) {
110 | remainderByIndex.remove(index);
111 | return recordedBindings.byIndex.get(index);
112 | }
113 |
114 | return null;
115 | });
116 | }
117 |
118 | @Override
119 | public String getSource() {
120 | return expressionQuery.getQuery();
121 | }
122 |
123 | @Override
124 | public void bindTo(BindTarget target) {
125 | BindTargetBinder binder = new BindTargetBinder(target);
126 | expanded.bindTo(target);
127 | remainderByName.forEach(binder::bind);
128 | }
129 |
130 | @Override
131 | public String toQuery() {
132 | return expanded.toQuery();
133 | }
134 |
135 | @Override
136 | public String toString() {
137 | return String.format("Original: [%s], Expanded: [%s]", expressionQuery.getQuery(), expanded.toQuery());
138 | }
139 | }
140 |
141 | protected static class BindTargetRecorder implements BindTarget {
142 | final Map byIndex = new LinkedHashMap<>();
143 | final Map byName = new LinkedHashMap<>();
144 |
145 | @Override
146 | public void bind(String identifier, Object value) {
147 | byName.put(identifier, toParameter(value));
148 | }
149 |
150 | private Parameter toParameter(Object value) {
151 | return value instanceof Parameter ? (Parameter) value : Parameter.from(value);
152 | }
153 |
154 | @Override
155 | public void bind(int index, Object value) {
156 | byIndex.put(index, toParameter(value));
157 | }
158 |
159 | @Override
160 | public void bindNull(String identifier, Class> type) {
161 | byName.put(identifier, Parameter.empty(type));
162 | }
163 |
164 | @Override
165 | public void bindNull(int index, Class> type) {
166 | byIndex.put(index, Parameter.empty(type));
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/data/r2dbc/repository/support/DynamicCachingExpressionParser.java:
--------------------------------------------------------------------------------
1 | package org.springframework.data.r2dbc.repository.support;
2 |
3 | import org.springframework.expression.ExpressionParser;
4 |
5 | public class DynamicCachingExpressionParser extends CachingExpressionParser {
6 | public DynamicCachingExpressionParser(ExpressionParser delegate) {
7 | super(delegate);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/R2dbcDynamicApplication.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc;
2 |
3 | import com.joutvhu.dynamic.commons.DynamicQueryTemplateProvider;
4 | import com.joutvhu.dynamic.freemarker.FreemarkerQueryTemplateProvider;
5 | import com.joutvhu.dynamic.r2dbc.support.DynamicR2dbcRepositoryFactoryBean;
6 | import io.r2dbc.h2.H2ConnectionConfiguration;
7 | import io.r2dbc.h2.H2ConnectionFactory;
8 | import io.r2dbc.spi.ConnectionFactory;
9 | import org.springframework.boot.SpringApplication;
10 | import org.springframework.boot.autoconfigure.SpringBootApplication;
11 | import org.springframework.context.annotation.Bean;
12 | import org.springframework.core.io.ClassPathResource;
13 | import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
14 | import org.springframework.r2dbc.connection.init.CompositeDatabasePopulator;
15 | import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
16 | import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
17 |
18 | @SpringBootApplication
19 | @EnableR2dbcRepositories(
20 | basePackages = {"com.joutvhu.dynamic.r2dbc.repository"},
21 | repositoryFactoryBeanClass = DynamicR2dbcRepositoryFactoryBean.class
22 | )
23 | public class R2dbcDynamicApplication {
24 | public static void main(String[] args) {
25 | SpringApplication.run(R2dbcDynamicApplication.class);
26 | }
27 |
28 | @Bean
29 | public ConnectionFactory connectionFactory() {
30 | return new H2ConnectionFactory(
31 | H2ConnectionConfiguration.builder()
32 | .url("mem:testdb;DB_CLOSE_DELAY=-1;")
33 | .username("sa")
34 | .build()
35 | );
36 | }
37 |
38 | @Bean
39 | public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) {
40 | ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
41 | initializer.setConnectionFactory(connectionFactory);
42 |
43 | CompositeDatabasePopulator populator = new CompositeDatabasePopulator();
44 | populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("sql/table.sql")));
45 | initializer.setDatabasePopulator(populator);
46 |
47 | return initializer;
48 | }
49 |
50 | @Bean
51 | public DynamicQueryTemplateProvider dynamicQueryTemplates() {
52 | DynamicQueryTemplateProvider queryTemplates = new FreemarkerQueryTemplateProvider();
53 | queryTemplates.setSuffix(".dsql");
54 | return queryTemplates;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/R2dbcDynamicApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc;
2 |
3 | import com.joutvhu.dynamic.r2dbc.entity.TableA;
4 | import com.joutvhu.dynamic.r2dbc.model.ModelC;
5 | import com.joutvhu.dynamic.r2dbc.repository.TableARepository;
6 | import com.joutvhu.dynamic.r2dbc.repository.TableBRepository;
7 | import com.joutvhu.dynamic.r2dbc.repository.TableCRepository;
8 | import org.junit.jupiter.api.Assertions;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.extension.ExtendWith;
11 | import org.springframework.beans.factory.annotation.Autowired;
12 | import org.springframework.boot.test.context.SpringBootTest;
13 | import org.springframework.test.context.junit.jupiter.SpringExtension;
14 | import reactor.core.publisher.Flux;
15 | import reactor.test.StepVerifier;
16 |
17 | import java.util.ArrayList;
18 | import java.util.List;
19 |
20 | @ExtendWith(SpringExtension.class)
21 | @SpringBootTest(classes = R2dbcDynamicApplication.class)
22 | public class R2dbcDynamicApplicationTest {
23 | @Autowired
24 | private TableARepository tableARepository;
25 | @Autowired
26 | private TableBRepository tableBRepository;
27 | @Autowired
28 | private TableCRepository tableCRepository;
29 |
30 | @Test
31 | public void findA1() {
32 | tableARepository.findA1(410L, "DSFGT4510A")
33 | .as(StepVerifier::create)
34 | .expectNextCount(1)
35 | .verifyComplete();
36 | }
37 |
38 | @Test
39 | public void findA1CNull() {
40 | tableARepository.findA1(104L, null)
41 | .as(StepVerifier::create)
42 | .expectNextCount(1)
43 | .verifyComplete();
44 | }
45 |
46 | @Test
47 | public void findA1CEmpty() {
48 | tableARepository.findA1(104L, "")
49 | .as(StepVerifier::create)
50 | .expectNextCount(1)
51 | .verifyComplete();
52 | }
53 |
54 | @Test
55 | public void findA2() {
56 | tableARepository.findA2(195L, "DSFGT4510A")
57 | .as(StepVerifier::create)
58 | .expectNextCount(1)
59 | .verifyComplete();
60 | }
61 |
62 | @Test
63 | public void findAllA() {
64 | Flux result = tableARepository.findAll();
65 | Assertions.assertEquals(3, result.count().block());
66 | }
67 |
68 | @Test
69 | public void findJ1() {
70 | tableARepository.findJ(101L, 12042107L)
71 | .as(StepVerifier::create)
72 | .expectNextCount(1)
73 | .verifyComplete();
74 | }
75 |
76 | @Test
77 | public void findJ2() {
78 | tableARepository.findJ(104L, null)
79 | .as(StepVerifier::create)
80 | .expectNextCount(0)
81 | .verifyComplete();
82 | }
83 |
84 | @Test
85 | public void findJ3() {
86 | tableARepository.findJ(null, 41017100L)
87 | .as(StepVerifier::create)
88 | .expectNextCount(1)
89 | .verifyComplete();
90 | }
91 |
92 | @Test
93 | public void findJ4() {
94 | tableARepository.findJ(null, null)
95 | .as(StepVerifier::create)
96 | .expectNextCount(2)
97 | .verifyComplete();
98 | }
99 |
100 | @Test
101 | public void findB1StartH() {
102 | tableBRepository.findB1("HBTVB")
103 | .as(StepVerifier::create)
104 | .expectNextCount(1)
105 | .verifyComplete();
106 | }
107 |
108 | @Test
109 | public void findB1StartG() {
110 | tableBRepository.findB1("GSDRB")
111 | .as(StepVerifier::create)
112 | .expectNextCount(5)
113 | .verifyComplete();
114 | }
115 |
116 | @Test
117 | public void findB1All() {
118 | tableBRepository.findB1(null)
119 | .as(StepVerifier::create)
120 | .expectNextCount(5)
121 | .verifyComplete();
122 | }
123 |
124 | @Test
125 | public void findB2() {
126 | tableBRepository.findB2(50000000L)
127 | .as(StepVerifier::create)
128 | .expectNextCount(4)
129 | .verifyComplete();
130 | }
131 |
132 | @Test
133 | public void findB2UL() {
134 | tableBRepository.findB2(null)
135 | .as(StepVerifier::create)
136 | .expectNextCount(5)
137 | .verifyComplete();
138 | }
139 |
140 | @Test
141 | public void findB2P() {
142 | tableBRepository.findB2(50000000L)
143 | .as(StepVerifier::create)
144 | .expectNextCount(4)
145 | .verifyComplete();
146 | }
147 |
148 | @Test
149 | public void sumB1() {
150 | tableBRepository.sumB1(40000000L)
151 | .as(StepVerifier::create)
152 | .expectNextCount(1)
153 | .verifyComplete();
154 | }
155 |
156 | @Test
157 | public void findB4() {
158 | tableBRepository.findB4(new ModelC(0L, "HTYRB"))
159 | .as(StepVerifier::create)
160 | .expectNextCount(1)
161 | .verifyComplete();
162 | }
163 |
164 | @Test
165 | public void findB5() {
166 | tableBRepository.findB5(12042107L)
167 | .as(StepVerifier::create)
168 | .expectNextCount(1)
169 | .verifyComplete();
170 | }
171 |
172 | @Test
173 | public void findC1() {
174 | List c = new ArrayList<>();
175 | c.add(101L);
176 | c.add(104L);
177 | c.add(410L);
178 | tableCRepository.search(null, "T", c)
179 | .as(StepVerifier::create)
180 | .expectNextCount(3)
181 | .verifyComplete();
182 | }
183 | }
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/entity/TableA.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.entity;
2 |
3 | import org.springframework.data.annotation.Id;
4 | import org.springframework.data.relational.core.mapping.Column;
5 | import org.springframework.data.relational.core.mapping.Table;
6 |
7 | @Table("TABLE_A")
8 | public class TableA {
9 | @Id
10 | @Column("FIELD_A")
11 | private Long fieldA;
12 |
13 | @Column("FIELD_B")
14 | private Long fieldB;
15 |
16 | @Column("FIELD_C")
17 | private String fieldC;
18 |
19 | public Long getFieldA() {
20 | return fieldA;
21 | }
22 |
23 | public void setFieldA(Long fieldA) {
24 | this.fieldA = fieldA;
25 | }
26 |
27 | public Long getFieldB() {
28 | return fieldB;
29 | }
30 |
31 | public void setFieldB(Long fieldB) {
32 | this.fieldB = fieldB;
33 | }
34 |
35 | public String getFieldC() {
36 | return fieldC;
37 | }
38 |
39 | public void setFieldC(String fieldC) {
40 | this.fieldC = fieldC;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/entity/TableB.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.entity;
2 |
3 | import org.springframework.data.annotation.Id;
4 | import org.springframework.data.relational.core.mapping.Column;
5 | import org.springframework.data.relational.core.mapping.Table;
6 |
7 | @Table("TABLE_B")
8 | public class TableB {
9 | @Id
10 | @Column("FIELD_A")
11 | private Long fieldA;
12 |
13 | @Column("FIELD_D")
14 | private Long fieldD;
15 |
16 | @Column("FIELD_E")
17 | private String fieldE;
18 |
19 | public Long getFieldA() {
20 | return fieldA;
21 | }
22 |
23 | public void setFieldA(Long fieldA) {
24 | this.fieldA = fieldA;
25 | }
26 |
27 | public Long getFieldD() {
28 | return fieldD;
29 | }
30 |
31 | public void setFieldD(Long fieldD) {
32 | this.fieldD = fieldD;
33 | }
34 |
35 | public String getFieldE() {
36 | return fieldE;
37 | }
38 |
39 | public void setFieldE(String fieldE) {
40 | this.fieldE = fieldE;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/entity/TableC.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.entity;
2 |
3 | import org.springframework.data.annotation.Id;
4 | import org.springframework.data.relational.core.mapping.Column;
5 | import org.springframework.data.relational.core.mapping.Table;
6 |
7 | @Table("TABLE_C")
8 | public class TableC {
9 | @Id
10 | @Column("FIELD_A")
11 | private Long fieldA;
12 |
13 | @Column("FIELD_B")
14 | private String fieldB;
15 |
16 | @Column("FIELD_C")
17 | private Long fieldC;
18 |
19 | public Long getFieldA() {
20 | return fieldA;
21 | }
22 |
23 | public void setFieldA(Long fieldA) {
24 | this.fieldA = fieldA;
25 | }
26 |
27 | public String getFieldB() {
28 | return fieldB;
29 | }
30 |
31 | public void setFieldB(String fieldB) {
32 | this.fieldB = fieldB;
33 | }
34 |
35 | public Long getFieldC() {
36 | return fieldC;
37 | }
38 |
39 | public void setFieldC(Long fieldC) {
40 | this.fieldC = fieldC;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/model/ModelC.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.model;
2 |
3 | import org.springframework.data.relational.core.mapping.Column;
4 |
5 | public class ModelC {
6 | @Column("FIELD_E")
7 | private Long fieldA;
8 |
9 | @Column("FIELD_C")
10 | private String fieldC;
11 |
12 | public ModelC() {
13 | }
14 |
15 | public ModelC(Long fieldA, String fieldC) {
16 | this.fieldA = fieldA;
17 | this.fieldC = fieldC;
18 | }
19 |
20 | public Long getFieldA() {
21 | return fieldA;
22 | }
23 |
24 | public void setFieldA(Long fieldA) {
25 | this.fieldA = fieldA;
26 | }
27 |
28 | public String getFieldC() {
29 | return fieldC;
30 | }
31 |
32 | public void setFieldC(String fieldC) {
33 | this.fieldC = fieldC;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/model/TableAB.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.model;
2 |
3 | import org.springframework.data.relational.core.mapping.Column;
4 |
5 | public class TableAB {
6 | @Column("FIELD_A")
7 | private Long fieldA;
8 |
9 | @Column("FIELD_B")
10 | private Long fieldB;
11 |
12 | @Column("FIELD_C")
13 | private String fieldC;
14 |
15 | @Column("FIELD_D")
16 | private Long fieldD;
17 |
18 | @Column("FIELD_E")
19 | private String fieldE;
20 |
21 | public Long getFieldA() {
22 | return fieldA;
23 | }
24 |
25 | public void setFieldA(Long fieldA) {
26 | this.fieldA = fieldA;
27 | }
28 |
29 | public Long getFieldB() {
30 | return fieldB;
31 | }
32 |
33 | public void setFieldB(Long fieldB) {
34 | this.fieldB = fieldB;
35 | }
36 |
37 | public String getFieldC() {
38 | return fieldC;
39 | }
40 |
41 | public void setFieldC(String fieldC) {
42 | this.fieldC = fieldC;
43 | }
44 |
45 | public Long getFieldD() {
46 | return fieldD;
47 | }
48 |
49 | public void setFieldD(Long fieldD) {
50 | this.fieldD = fieldD;
51 | }
52 |
53 | public String getFieldE() {
54 | return fieldE;
55 | }
56 |
57 | public void setFieldE(String fieldE) {
58 | this.fieldE = fieldE;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/repository/TableARepository.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.repository;
2 |
3 | import com.joutvhu.dynamic.r2dbc.DynamicQuery;
4 | import com.joutvhu.dynamic.r2dbc.entity.TableA;
5 | import com.joutvhu.dynamic.r2dbc.model.TableAB;
6 | import org.springframework.data.r2dbc.repository.Query;
7 | import org.springframework.data.r2dbc.repository.R2dbcRepository;
8 | import reactor.core.publisher.Flux;
9 |
10 | public interface TableARepository extends R2dbcRepository {
11 | @DynamicQuery(value = "select * from Table_A t where t.field_B = :fieldB\n" +
12 | "<#if fieldC?has_content>\n" +
13 | " and t.field_C = :fieldC\n" +
14 | "#if>"
15 | )
16 | Flux findA1(Long fieldB, String fieldC);
17 |
18 | @Query(value = "select * from Table_A t where t.field_A = :fieldA and t.field_C = :fieldC")
19 | Flux findA2(Long fieldA, String fieldC);
20 |
21 | @DynamicQuery(value = "select * from Table_A a inner join Table_B b\n" +
22 | "on a.field_A = b.field_A\n" +
23 | "<#if fieldB??>\n" +
24 | " and a.field_B = :fieldB\n" +
25 | "#if>" +
26 | "<#if fieldD??>\n" +
27 | " and b.field_D = :fieldD\n" +
28 | "#if>"
29 | )
30 | Flux findJ(Long fieldB, Long fieldD);
31 | }
32 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/repository/TableBRepository.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.repository;
2 |
3 | import com.joutvhu.dynamic.r2dbc.DynamicQuery;
4 | import com.joutvhu.dynamic.r2dbc.entity.TableB;
5 | import com.joutvhu.dynamic.r2dbc.model.ModelC;
6 | import org.springframework.data.repository.reactive.ReactiveCrudRepository;
7 | import reactor.core.publisher.Flux;
8 |
9 | import java.util.List;
10 |
11 | public interface TableBRepository extends ReactiveCrudRepository {
12 | @DynamicQuery
13 | Flux findB1(String fieldE);
14 |
15 | @DynamicQuery
16 | Flux findB2(Long maxD);
17 |
18 | @DynamicQuery
19 | Flux sumB1(Long maxD);
20 |
21 | @DynamicQuery("select * from Table_B t\n" +
22 | "<#if modelC.fieldC?has_content>\n" +
23 | " where t.field_E = :#{#modelC.fieldC}\n" +
24 | "#if>")
25 | Flux findB4(ModelC modelC);
26 |
27 | @DynamicQuery(name = "findTableBByFieldD")
28 | Flux findB5(Long fieldD);
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/java/com/joutvhu/dynamic/r2dbc/repository/TableCRepository.java:
--------------------------------------------------------------------------------
1 | package com.joutvhu.dynamic.r2dbc.repository;
2 |
3 | import com.joutvhu.dynamic.r2dbc.DynamicQuery;
4 | import com.joutvhu.dynamic.r2dbc.entity.TableC;
5 | import org.springframework.data.repository.reactive.ReactiveSortingRepository;
6 | import reactor.core.publisher.Flux;
7 |
8 | import java.util.List;
9 |
10 | public interface TableCRepository extends ReactiveSortingRepository {
11 | @DynamicQuery("select * from Table_C i\n" +
12 | "<@where>\n" +
13 | " <#if fieldA??>\n" +
14 | " i.field_A = :fieldA\n" +
15 | " #if>\n" +
16 | " <#if fieldB??>\n" +
17 | " and i.field_B like concat('%',:fieldB,'%')\n" +
18 | " #if>\n" +
19 | " <#if fieldCs??>\n" +
20 | " and i.field_C in (:fieldCs)\n" +
21 | " #if>\n" +
22 | "@where>")
23 | Flux search(Long fieldA, String fieldB, List fieldCs);
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | logging.level.org.springframework.r2dbc=DEBUG
2 |
3 | # H2 Datasource
4 | spring.r2dbc.url=r2dbc:h2:mem:DYNAMIC_R2DBC;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
5 | spring.r2dbc.username=sa
6 | spring.r2dbc.password=
7 | spring.data.r2dbc.repositories.enabled=true
8 |
--------------------------------------------------------------------------------
/src/test/resources/query/query.dsql:
--------------------------------------------------------------------------------
1 | --TableB:findB1
2 | select * from TABLE_B
3 | <#if fieldE?has_content && fieldE?starts_with("H")>
4 | where FIELD_E = :fieldE
5 | #if>
6 |
7 | --TableB:findB2
8 | select * from TABLE_B
9 | <#if maxD??>
10 | where FIELD_D < :maxD
11 | #if>
12 |
13 | --TableB:sumB1
14 | select sum(FIELD_D) from TABLE_B
15 | <#if maxD??>
16 | where FIELD_D < :maxD
17 | #if>
18 |
19 | -- findTableBByFieldD
20 | select * from TABLE_B
21 | <#if fieldD??>
22 | where FIELD_D = :fieldD
23 | #if>
24 |
--------------------------------------------------------------------------------
/src/test/resources/sql/table.sql:
--------------------------------------------------------------------------------
1 | drop all OBJECTS;
2 |
3 | create table TABLE_A
4 | (
5 | FIELD_A NUMBER default 0 not null,
6 | FIELD_B NUMBER default 0 not null,
7 | FIELD_C VARCHAR(10) default ' ' not null
8 | );
9 |
10 | create table TABLE_B
11 | (
12 | FIELD_A NUMBER default 0 not null,
13 | FIELD_D NUMBER default 0 not null,
14 | FIELD_E VARCHAR(5) default ' ' not null
15 | );
16 |
17 | create table TABLE_C
18 | (
19 | FIELD_A NUMBER default 0 not null,
20 | FIELD_B VARCHAR(10) default ' ' not null,
21 | FIELD_C NUMBER default 0 not null
22 | );
23 |
24 | INSERT INTO TABLE_A (FIELD_A, FIELD_B, FIELD_C) VALUES (195, 410, 'DSFGT4510A');
25 | INSERT INTO TABLE_A (FIELD_A, FIELD_B, FIELD_C) VALUES (210, 104, 'DGFHRTNR5A');
26 | INSERT INTO TABLE_A (FIELD_A, FIELD_B, FIELD_C) VALUES (224, 101, '151DDRVTBA');
27 |
28 | INSERT INTO TABLE_B (FIELD_A, FIELD_D, FIELD_E) VALUES (195, 41017100, 'HBTVB');
29 | INSERT INTO TABLE_B (FIELD_A, FIELD_D, FIELD_E) VALUES (204, 41414404, 'GSDRB');
30 | INSERT INTO TABLE_B (FIELD_A, FIELD_D, FIELD_E) VALUES (224, 12042107, 'HTYRB');
31 | INSERT INTO TABLE_B (FIELD_A, FIELD_D, FIELD_E) VALUES (414, 21410574, 'KGFHT');
32 | INSERT INTO TABLE_B (FIELD_A, FIELD_D, FIELD_E) VALUES (821, 54334453, 'TDSEV');
33 |
34 | INSERT INTO TABLE_C (FIELD_A, FIELD_B, FIELD_C) VALUES (195, 'DSFGT4510A', 410);
35 | INSERT INTO TABLE_C (FIELD_A, FIELD_B, FIELD_C) VALUES (210, 'DGFHRTNR5A', 104);
36 | INSERT INTO TABLE_C (FIELD_A, FIELD_B, FIELD_C) VALUES (224, '151DDRVTBA', 101);
37 | INSERT INTO TABLE_C (FIELD_A, FIELD_B, FIELD_C) VALUES (433, 'FFRVCS78CE', 344);
38 |
--------------------------------------------------------------------------------