diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..568014294
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,12 @@
+
+
+- [ ] You have read the [Spring Data contribution guidelines](https://github.com/spring-projects/spring-data-build/blob/main/CONTRIBUTING.adoc).
+- [ ] You use the code formatters provided [here](https://github.com/spring-projects/spring-data-build/tree/main/etc/ide) and have them applied to your changes. Don’t submit any formatting related changes.
+- [ ] You submit test cases (unit or integration tests) that back your changes.
+- [ ] You added yourself as author in the headers of the classes you touched. Amend the date range in the Apache license header if needed. For new types, add the license header (copy from another file and set the current year only).
+
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 000000000..20dd84a5d
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,27 @@
+name: CI Build
+
+on:
+ push:
+ branches: [ main ]
+
+jobs:
+ build:
+ name: Build project
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - name: Check out sources
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: 17
+ cache: 'maven'
+
+ - name: Build with Maven
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
+ run: ./mvnw clean verify -B -Pci
diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml
new file mode 100644
index 000000000..606226523
--- /dev/null
+++ b/.github/workflows/project.yml
@@ -0,0 +1,47 @@
+# GitHub Actions to automate GitHub issues for Spring Data Project Management
+
+name: Spring Data GitHub Issues
+
+on:
+ issues:
+ types: [opened, edited, reopened]
+ issue_comment:
+ types: [created]
+ pull_request_target:
+ types: [opened, edited, reopened]
+
+jobs:
+ Inbox:
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request == null
+ steps:
+ - name: Create or Update Issue Card
+ uses: peter-evans/create-or-update-project-card@v1.1.2
+ with:
+ project-name: 'Spring Data'
+ column-name: 'Inbox'
+ project-location: 'spring-projects'
+ token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
+ Pull-Request:
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request != null
+ steps:
+ - name: Create or Update Pull Request Card
+ uses: peter-evans/create-or-update-project-card@v1.1.2
+ with:
+ project-name: 'Spring Data'
+ column-name: 'Review pending'
+ project-location: 'spring-projects'
+ issue-number: ${{ github.event.pull_request.number }}
+ token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
+ Feedback-Provided:
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'spring-projects' && github.event_name == 'issue_comment' && github.event.action == 'created' && github.actor != 'spring-projects-issues' && github.event.pull_request == null && github.event.issue.state == 'open' && contains(toJSON(github.event.issue.labels), 'waiting-for-feedback')
+ steps:
+ - name: Update Project Card
+ uses: peter-evans/create-or-update-project-card@v1.1.2
+ with:
+ project-name: 'Spring Data'
+ column-name: 'Feedback provided'
+ project-location: 'spring-projects'
+ token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
diff --git a/.gitignore b/.gitignore
index 8a95788e7..7cc5f4de1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,15 @@
.project
.classpath
.springBeans
+.develocity/
.settings/
target/
+.mvn/.gradle-enterprise
#IntelliJ Stuff
.idea
*.iml
+
+#Geode Stuff
+*.log
+*.dat
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
new file mode 100644
index 000000000..1e3bb355f
--- /dev/null
+++ b/.mvn/extensions.xml
@@ -0,0 +1,8 @@
+
+
+
+ io.spring.develocity.conventions
+ develocity-conventions-maven-extension
+ 0.0.19
+
+
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 000000000..c1dd12f17
Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 000000000..b7cb93e70
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
diff --git a/.travis.yml b/.travis.yml
index 1877ebad8..abfaad348 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,27 +1,22 @@
language: java
-matrix:
+os: linux
+
+jobs:
include:
- - jdk: oraclejdk8
- env: JDK='Oracle JDK 8'
- - jdk: oraclejdk9
- env: JDK='Oracle JDK 9'
- - env: JDK='Oracle JDK 10'
- before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 10
- - env: JDK='Oracle JDK 11'
- before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 11
+ - env: JDK='OpenJDK 16'
+ before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -f 16
addons:
apt:
sources:
- - sourceline: 'deb http://packages.couchbase.com/releases/couchbase-server/community/deb/ trusty trusty/main'
- - sourceline: 'deb http://packages.couchbase.com/ubuntu trusty trusty/main'
+ - sourceline: 'deb http://packages.couchbase.com/releases/couchbase-server/community/deb/ xenial xenial/main'
key_url: 'http://packages.couchbase.com/ubuntu/couchbase.key'
packages:
- couchbase-server-community
services:
- - redis-server
+ - redis
- docker
cache:
@@ -38,7 +33,8 @@ install:
- /opt/couchbase/bin/cbdocloader -c 127.0.0.1:8091 -u Administrator -p password -b travel-sample -m 256 -d /opt/couchbase/samples/travel-sample.zip
- /opt/couchbase/bin/couchbase-cli user-manage -c 127.0.0.1:8091 -u Administrator -p password --set --rbac-username=travel-sample --rbac-password=password --roles=admin --auth-domain local
-
script:
+ - cp -f settings.xml $HOME/.m2/settings.xml
- mvn -version
- - mvn clean dependency:list test -U -Dsort -Dmaven.test.redirectTestOutputToFile=true -B
+ - java -version
+ - mvn clean test -U -Dsort -Dmaven.test.redirectTestOutputToFile=true -B -s settings.xml
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 000000000..43d26ddbd
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,130 @@
+= Spring Data Examples image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring%20Data%20-%20Examples"]
+
+image:https://travis-ci.org/spring-projects/spring-data-examples.svg?branch=main[Build Status,link=https://travis-ci.org/spring-projects/spring-data-examples]
+
+This repository contains example projects for the different Spring Data modules to showcase the API and how to use the features provided by the modules.
+
+We have separate folders for the samples of individual modules:
+
+== Spring Data for Apache Cassandra
+
+* `example` - Shows core Spring Data support for Apache Cassandra.
+* `kotlin` - Example for using Cassandra with Kotlin.
+* `reactive` - Example project to show reactive template and repository support.
+
+== Spring Data Elasticsearch
+
+* `example` - Example how to use basic text search, geo-spatial search and facets. It uses
+ the High Level REST Client backing template and repository.
+* `reactive` - Example how to use reactive client, template and repository features.
+
+Local Elasticsearch instance must be running to run the tests.
+
+== Spring Data JDBC
+
+* `basic` - Basic usage of Spring Data JDBC.
+* `graalvm-native` - This example compiles a basic Spring Data JDBC application into a GraalVM native image.
+* `howto` - A collection of projects to go with the https://spring.io/blog/2021/09/09/spring-data-jdbc-how-to-use-custom-id-generation[Spring Data JDBC - How to blog posts].
+* `immutables` - Showing Spring Data JDBC usage
+ with https://immutables.github.io/[Immutables]
+* `jmolecules` - Demonstrates the interaction of jMolecules with Spring Data JDBC.
+* `jooq` - Demonstrates how to use jOOQ and Spring Data JDBC together.
+* `mybatis` - Demonstrate how to use MyBatis to generate SQL for Spring Data JDBC.
+* `singlequeryloading` - Demonstrates how to enable Single Query Loading.
+
+== Spring Data JPA
+
+* `eclipselink` - Sample project to show how to use Spring Data JPA with Spring Boot and https://www.eclipse.org/eclipselink/[Eclipselink].
+* `example` - Probably the project you want to have a look at first.
+Contains a variety of sample packages, showcasing the different levels at which you can use Spring Data JPA.
+Have a look at the `simple` package for the most basic setup.
+Contains also examples running on Virtual Threads.
+* `interceptors` - Example of how to enrich the repositories with AOP.
+* `jpa21` - Shows support for JPA 2.1 specific features (stored procedures support).
+* `multiple-datasources` - Examples of how to use Spring Data JPA with multiple `DataSource`s.
+* `query-by-example` - Example project showing usage of Query by Example with Spring Data JPA.
+* `security` - Example of how to integrate Spring Data JPA Repositories with Spring Security.
+* `showcase` - Refactoring show case of how to improve a plain-JPA-based persistence layer by using Spring Data JPA (read: removing close to all of the implementation code).Follow the `demo.txt` file for detailed instructions.
+* `vavr` - Shows the support of https://www.vavr.io[Vavr] collection types as return types for query methods.
+
+== Spring Data LDAP
+
+* `example` - Sample for Spring Data repositories to access an LDAP store.
+
+== Spring Data MongoDB
+
+* `aggregation` - Example project to showcase the MongoDB aggregation framework support.
+* `example` - Example project for general repository functionality (including geo-spatial functionality), Querydsl integration and advanced topics.
+* `fluent-api` - Example project to show the new fluent API (`MongoTemplate`-alternative) to interact with MongoDB.
+* `geo-json` - Example project showing usage of http://geojson.org[GeoJSON] with MongoDB.
+* `gridfs` - Example project showing usage of gridFS with MongoDB.
+* `jmolecules` - Example of Spring Data MongoDB working with a jMolecules based domain model.
+* `kotlin` - Example for using https://kotlinlang.org/[Kotlin] with MongoDB.
+* `linking` - Example demonstrating possibilities for linking documents.
+* `query-by-example` - Example project showing usage of Query by Example with MongoDB.
+* `querydsl` - Example project showing imperative and reactive https://github.com/querydsl/querydsl[Querydsl] support for MongoDB.
+* `reactive` - Example project to show reactive template and repository support.
+* `repository-metrics` - Example project to show how to collect repository method invocation metrics.
+* `security` - Example project showing usage of Spring Security with MongoDB.
+* `text-search` - Example project showing usage of MongoDB text search feature.
+* `transactions` - Example project for imperative and reactive MongoDB 4.0 transaction support.
+
+== Spring Data Neo4j
+
+* `example` - Example to show basic node and relationship entities and repository usage.
+
+== Spring Data R2DBC
+
+* `example` - Basic usage of Spring Data R2DBC.
+
+== Spring Data Redis
+
+* `cluster` - Example for Redis Cluster support.
+* `example` - Example for basic Spring Data Redis setup.
+* `pubsub` - Example project to show Pub/Sub usage using Platform and Virtual Threads.
+* `reactive` - Example project to show reactive template support.
+* `repositories` - Example demonstrating Spring Data repository abstraction on top of Redis.
+* `sentinel` - Example for Redis Sentinel support.
+* `streams` - Example for https://redis.io/topics/streams-intro[Redis Streams] support.
+
+Local Redis instances must be running to run the tests. One option is to use Docker in a separate terminal:
+
+```
+$ docker run -p 6379:6379 redis:5.0
+```
+
+WARNING: If you're done using it, don't forget to shut it down!
+
+== Spring Data REST
+
+* `headers` - A sample showing the population of HTTP headers and the usage of them to perform conditional `GET` requests.
+* `multi-store` - A sample REST web-service based on both Spring Data JPA and Spring Data MongoDB.
+* `projections` - A sample REST web-service showing how to use projections.
+* `security` - A sample REST web-service secured using Spring Security.
+* `starbucks` - A sample REST web-service built with Spring Data REST and MongoDB.
+* `uri-customizations` - Example project to show URI customization capabilities.
+
+== Spring Data web support
+
+* `projections` - Example for Spring Data web support for JSONPath and XPath expressions on projection interfaces.
+* `querydsl` - Example for Spring Data Querydsl web integration (creating a `Predicate` from web requests).
+* `web` - Example for Spring Data web integration (binding `Pageable` instances to Spring MVC controller methods, using interfaces to bind Spring MVC request payloads).
+
+== Miscellaneous
+
+* `mongodb/fragment-spi` - Example project how to use Spring Data Fragment SPI to provide reusable custom extensions.
+* `bom` - Example project how to use the Spring Data release train bom in non-Spring-Boot scenarios.
+* `map` - Example project to show how to use `Map`-backed repositories.
+* `multi-store` - Example project to use both Spring Data MongoDB and Spring Data JPA in
+ one project.
+
+== Note
+
+* The example projects make use of the https://projectlombok.org/[Lombok] plugin. To get
+ proper code navigation in your IDE, you must install it separately. Lombok is available
+ in the IntelliJ plugins repository and as
+ a https://projectlombok.org/download[download] for Eclipse-based IDEs.
+* The code makes use of Java 16 language features therefore you need Java 16 or newer to
+ run and compile the examples.
+* Most store modules examples start their database via Testcontainers or as
+ embedded/in-memory server unless stated otherwise.
diff --git a/README.md b/README.md
deleted file mode 100644
index 1b8fc4307..000000000
--- a/README.md
+++ /dev/null
@@ -1,96 +0,0 @@
-# Spring Data Examples
-
-[](https://travis-ci.org/spring-projects/spring-data-examples)
-
-This repository contains example projects for the different Spring Data modules to showcase the API and how to use the features provided by the modules.
-
-We have separate folders for the samples of individual modules:
-
-## Spring Data JPA
-
-* `eclipselink` - Sample project to show how to use Spring Data JPA with Spring Boot and [Eclipselink](https://www.eclipse.org/eclipselink/).
-* `example` - Probably the project you want to have a look at first. Contains a variety of sample packages, showcasing the different levels at which you can use Spring Data JPA. Have a look at the `simple` package for the most basic setup.
-* `interceptors` - Example of how to enrich the repositories with AOP.
-* `java8` - Example of how to use Spring Data JPA auditing with Java 8 date time types as well as the usage of `Optional` as return type for repository methods. Note, this project requires to be build with JDK 8.
-* `jpa21` - Shows support for JPA 2.1 specific features (stored procedures support).
-* `multiple-datasources` - Examples of how to use Spring Data JPA with multiple `DataSource`s.
-* `query-by-example` - Example project showing usage of Query by Example with Spring Data JPA.
-* `security` - Example of how to integrate Spring Data JPA Repositories with Spring Security.
-* `showcase` - Refactoring show case of how to improve a plain-JPA-based persistence layer by using Spring Data JPA (read: removing close to all of the implementation code). Follow the `demo.txt` file for detailed instructions.
-* `vavr` - Shows the support of [Vavr](https://www.vavr.io) collection types as return types for query methods.
-
-## Spring Data MongoDB
-
-* `aggregation` - Example project to showcase the MongoDB aggregation framework support.
-* `example` - Example project for general repository functionality (including geo-spatial functionality), Querydsl integration and advanced topics.
-* `fluent-api` - Example project to show the new fluent API (`MongoTemplate`-alternative) to interact with MongoDB.
-* `geo-json` - Example project showing usage of [GeoJSON](http://geojson.org) with MongoDB.
-* `gridfs` - Example project showing usage of gridFS with MongoDB.
-* `java8` - Example of how to use Spring Data MongoDB with Java 8 date time types as well as the usage of `Optional` as return type for repository methods. Note, this project requires to be build with JDK 8.
-* `kotlin` - Example for using Cassandra with MongoDB.
-* `query-by-example` - Example project showing usage of Query by Example with MongoDB.
-* `reactive` - Example project to show reactive template and repository support.
-* `security` - Example project showing usage of Spring Security with MongoDB.
-* `text-search` - Example project showing usage of MongoDB text search feature.
-* `transactions` - Example project for synchronous and reactive MongoDB 4.0 transaction support.
-
-## Spring Data REST
-
-* `headers` - A sample showing the population of HTTP headers and the usage of them to perform conditional `GET` requests.
-* `multi-store` - A sample REST web-service based on both Spring Data JPA and Spring Data MongoDB.
-* `projections` - A sample REST web-service showing how to use projections.
-* `security` - A sample REST web-service secured using Spring Security.
-* `starbucks` - A sample REST web-service built with Spring Data REST and MongoDB.
-* `uri-customizations` - Example project to show URI customization capabilities.
-
-## Spring Data Redis
-
-* `cluster` - Example for Redis Cluster support.
-* `example` - Example for basic Spring Data Redis setup.
-* `reactive` - Example project to show reactive template support.
-* `repositories` - Example demonstrating Spring Data repository abstraction on top of Redis.
-* `sentinel` - Example for Redis Sentinel support.
-
-## Spring Data for Apache Solr
-
-* `example` - Example project for Spring Data repositories for Apache Solr.
-* `managed-schema` - Example project to show managed schema integration.
-
-## Spring Data Elasticsearch
-
-* `example` - Example how to use basic text search, geo-spatial search and facets.
-
-## Spring Data Neo4j
-
-* `example` - Example to show basic node and relationship entities and repository usage.
-
-## Spring Data web support
-
-* `projections` - Example for Spring Data web support for JSONPath and XPath expressions on projection interfaces.
-* `querydsl` - Example for Spring Data Querydsl web integration (creating a `Predicate` from web requests).
-* `web` - Example for Spring Data web integration (binding `Pageable` instances to Spring MVC controller methods, using interfaces to bind Spring MVC request payloads).
-
-## Spring Data for Apache Cassandra
-
-* `example` - Shows core Spring Data support for Apache Cassandra.
-* `java8` - Java 8 specific functionality like the support for JSR-310 types in object mapping.
-* `kotlin` - Example for using Cassandra with Kotlin.
-* `reactive` - Example project to show reactive template and repository support.
-
-## Spring Data LDAP
-
-* `example` - Sample for Spring Data repositories to access an LDAP store.
-
-## Spring Data JDBC
-
-* `basic` - Basic usage of Spring Data JDBC.
-
-## Spring Data R2DBC
-
-* `example` - Basic usage of Spring Data R2DBC.
-
-## Miscellaneous
-
-* `bom` - Example project how to use the Spring Data release train bom in non-Spring-Boot scenarios.
-* `map` - Example project to show how to use `Map`-backed repositories.
-* `multi-store` - Example project to use both Spring Data MongoDB and Spring Data JPA in one project.
diff --git a/bom/README.adoc b/bom/README.adoc
index b14864221..439130629 100644
--- a/bom/README.adoc
+++ b/bom/README.adoc
@@ -1,17 +1,17 @@
= Spring Data - Release Train BOM example
-This project shows the usage of the Spring Data release train in a non-Spring-Boot project with both Maven and Gradle.
+This project shows the usage of the Spring Data BOM in a non-Spring-Boot project with both Maven and Gradle.
== Properties
-In both Maven and Gradle a couple of properties are used to define the versions of Spring Framework and Spring Data to use. For Spring Framework a plain version is used. For Spring Data we refer to a particular revision of a release train. The naming of Spring Data releases uses the following conventions:
+In both Maven and Gradle a couple of properties are used to define the versions of Spring Framework and Spring Data to use. For Spring Framework a plain version is used. For Spring Data we refer to the https://spring.io/blog/2020/04/30/updates-to-spring-versions[calver revision] of the BOM. The naming of Spring Data releases uses the following conventions:
-** `${release-train}-M1` -> Milestones
+** `${calver-version}-M1` -> Milestones
** …
-** `${release-train}-RC1` -> Release candidate
+** `${calver-version}-RC1` -> Release candidate
** …
-** `${release-train}-RELEASE` -> GA version
-** `${release-train}-SR1` -> Services release (bugfixes) for that release train
+** `${calver-version}` -> GA version
+** `${calver-version}` -> Services release (bugfixes) for that release train
== Maven
@@ -19,11 +19,11 @@ The `` section declares dependencies to the BOMs for bot
The standard `` section can now list Spring Framework and Spring Data dependencies without declaring a version and still be sure all libraries are in matching versions.
-Note, that we don't declare a Spring Framework dependency here. The import of the Spring Framework BOM nonetheless makes sure we control the version of all transitive Spring Framework dependencies pulled in by the Spring Data modules.
+Note that we do not declare a Spring Framework dependency here. The import of the Spring Framework BOM nonetheless makes sure we control the version of all transitive Spring Framework dependencies pulled in by the Spring Data modules.
== Gradle
-Gradle does not support Maven BOMs (Bill of Materials) out of the box, so the first thing to do is important the
+Gradle does not support Maven BOMs (Bill of Materials) out of the box, so the first thing to do is to import the
https://github.com/spring-gradle-plugins/dependency-management-plugin[dependency management plugin]. This example is based on Java,
but if you need a different language plugin (e.g. Kotlin), you can do so.
@@ -32,5 +32,5 @@ The `dependencyManagement` section can be used to import the Spring Framework BO
The standard `dependencies` section can now list Spring and Spring Data dependencies without declaring a version and still
be sure all libraries are align with each other.
-Note how you don't declare a Spring Framework dependency. Nevertheless, the dependency management plugin and the Spring Framework BOM
+Note how you do not declare a Spring Framework dependency. Nevertheless, the dependency management plugin and the Spring Framework BOM
ensures you control the version of all transitive Spring Framework dependencies pulled in by Spring Data.
diff --git a/bom/build.gradle b/bom/build.gradle
index 3b66459a6..eeef658f5 100644
--- a/bom/build.gradle
+++ b/bom/build.gradle
@@ -8,14 +8,14 @@ repositories {
}
ext {
- springVersion = '5.1.5.RELEASE'
- springDataVersion = 'Lovelace-SR5'
+ springVersion = '5.3.9'
+ springDataVersion = '2021.0.4'
}
dependencyManagement {
imports {
mavenBom "org.springframework:spring-framework-bom:${springVersion}"
- mavenBom "org.springframework.data:spring-data-releasetrain:${springDataVersion}"
+ mavenBom "org.springframework.data:spring-data-bom:${springDataVersion}"
}
}
diff --git a/bom/pom.xml b/bom/pom.xml
index 80bfd2c47..441c4d4f2 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -4,13 +4,13 @@
org.springframework.data.examplesspring-data-examples-bom
- 1.0.0.BUILD-SNAPSHOT
+ 1.0.0-SNAPSHOTSpring Data - Using the BOM for dependency management
- 5.1.5.RELEASE
- Lovelace-SR5
+ 7.0.0-M9
+ 2025.1.0-M6
@@ -26,7 +26,7 @@
org.springframework.data
- spring-data-releasetrain
+ spring-data-bom${spring-data.version}importpom
@@ -50,4 +50,27 @@
+
+
+
+
+ com.gradle
+ develocity-maven-extension
+
+
+
+
+ maven-surefire-plugin
+
+ these tests showcase Spring Data features and should always rerun
+
+
+
+
+
+
+
+
+
+
diff --git a/cassandra/aot-optimization/README.adoc b/cassandra/aot-optimization/README.adoc
new file mode 100644
index 000000000..107d8743d
--- /dev/null
+++ b/cassandra/aot-optimization/README.adoc
@@ -0,0 +1,94 @@
+= Spring Data Cassandra - Ahead of Time Repository Optimization Example
+
+The project shows the usage of AOT Repositories.
+Ahead of Time Repositories implement query methods through code contribution and allow for debugging queries during runtime.
+Additionally, AOT repositories improve startup time and reduce memory consumption because AOT optimized query methods do not require reflective introspection.
+Each AOT repository is documented with a JSON file that describes the queries implemented by the repository.
+
+== Using AOT Repositories
+
+Repository AOT processing is enabled by default when using Spring Boot's AOT processing (see `pom.xml` for `spring-boot-maven-plugin` usage).
+AOT processing generates AOT artifacts to `target/spring-aot` and through the regular build.
+When using the JVM mode (not Graal Native Images), then you need to enable AOT mode on the JVM when running your application through `-Dspring.aot.enabled=true`.
+
+[source,bash]
+----
+$ mvn clean package
+$ java -Dspring.aot.enabled=true -jar target/spring-data-cassandra-aot-optimization-4.0.0-SNAPSHOT.jar
+----
+
+You can find more details about AOT processing in the https://docs.spring.io/spring-data/cassandra/reference/5.0/cassandra/repositories/aot.html#aot.repositories[Spring Data Cassandra Reference Documentation].
+
+== AOT Repository
+
+**`UserRepositoryImpl__AotRepository`**
+
+Excerpt from: `target/spring-aot/main/sources/example/springdata/cassandra/UserRepositoryImpl__AotRepository.java`
+
+[source,java]
+----
+@Generated
+public class UserRepositoryImpl__AotRepository extends AotRepositoryFragmentSupport {
+ private final CassandraOperations operations;
+
+ public UserRepositoryImpl__AotRepository(CassandraOperations operations,
+ RepositoryFactoryBeanSupport.FragmentCreationContext context) {
+ // …
+ }
+
+ public User findUserByIdIn(long id) {
+ Object[] args = new Object[1];
+ args[0] = potentiallyConvertBindingValue(id);
+ SimpleStatement query = SimpleStatement.newInstance("SELECT * from users where user_id in(?)", args);
+
+ ExecutableSelectOperation.TerminatingResults select = operations.query(query).as(User.class);
+ return select.oneValue();
+ }
+
+ public User findUserByUsername(String username) {
+ Query query = Query.query(Criteria.where("username").is(username));
+
+ ExecutableSelectOperation.TerminatingSelect select = operations.query(User.class).matching(query);
+ return select.oneValue();
+ }
+}
+----
+
+== Metadata
+
+**`UserRepository.json`**
+
+Excerpt from: `target/spring-aot/main/resources/example/springdata/cassandra/UserRepository.json`
+
+[source,json]
+----
+{
+ "name": "example.springdata.cassandra.UserRepository",
+ "module": "Cassandra",
+ "type": "IMPERATIVE",
+ "methods": [
+ {
+ "name": "findUserByIdIn",
+ "signature": "public abstract example.springdata.cassandra.User example.springdata.cassandra.UserRepository.findUserByIdIn(long)",
+ "query": {
+ "query": "SELECT * from users where user_id in(?)"
+ }
+ },
+ {
+ "name": "findUserByUsername",
+ "signature": "public abstract example.springdata.cassandra.User example.springdata.cassandra.UserRepository.findUserByUsername(java.lang.String)",
+ "query": {
+ "query": "SELECT * FROM users WHERE uname=?"
+ }
+ },
+ {
+ "name": "save",
+ "signature": "org.springframework.data.cassandra.repository.support.SimpleCassandraRepository",
+ "fragment": {
+ "interface": "org.springframework.data.cassandra.repository.support.SimpleCassandraRepository",
+ "fragment": "org.springframework.data.cassandra.repository.support.SimpleCassandraRepository"
+ }
+ }
+ ]
+}
+----
diff --git a/cassandra/aot-optimization/pom.xml b/cassandra/aot-optimization/pom.xml
new file mode 100644
index 000000000..11cedb307
--- /dev/null
+++ b/cassandra/aot-optimization/pom.xml
@@ -0,0 +1,49 @@
+
+ 4.0.0
+
+
+ org.springframework.data.examples
+ spring-data-cassandra-examples
+ 4.0.0-SNAPSHOT
+
+
+ spring-data-cassandra-aot-optimization
+ Spring Data Cassandra - AOT Optimization Example
+
+
+
+
+ ${project.groupId}
+ spring-data-cassandra-example-utils
+ ${project.version}
+ test
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ process-aot
+
+ process-aot
+
+
+
+
+
+
+
+
diff --git a/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/Address.java b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/Address.java
new file mode 100644
index 000000000..4ae619ac8
--- /dev/null
+++ b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/Address.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra;
+
+import org.springframework.data.cassandra.core.mapping.UserDefinedType;
+
+/**
+ * @author Mark Paluch
+ */
+@UserDefinedType
+public record Address(String street, String zip, String city) {
+
+}
diff --git a/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/AotApplication.java b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/AotApplication.java
new file mode 100644
index 000000000..bd0cb3add
--- /dev/null
+++ b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/AotApplication.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra;
+
+import java.util.Collections;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.cassandra.core.CassandraOperations;
+import org.springframework.data.domain.Limit;
+
+/**
+ * Basic Spring Boot application.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootApplication
+public class AotApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AotApplication.class, args);
+ }
+
+ @Bean
+ CommandLineRunner commandLineRunner(UserRepository repository, CassandraOperations operations)
+ throws InterruptedException {
+
+ operations.getCqlOperations().execute("CREATE INDEX IF NOT EXISTS user_username ON users (uname);");
+ operations.getCqlOperations().execute(
+ "CREATE CUSTOM INDEX IF NOT EXISTS users_lname_idx_1 ON users (lname) USING 'org.apache.cassandra.index.sasi.SASIIndex';");
+
+ /*
+ Cassandra secondary indexes are created in the background without the possibility to check
+ whether they are available or not. So we are forced to just wait. *sigh*
+ */
+ Thread.sleep(1000);
+
+ return args -> {
+
+ User user = new User();
+ user.setId(42L);
+ user.setUsername("heisenberg");
+ user.setFirstname("Walter");
+ user.setLastname("White");
+
+ user.setCurrent(new Address("308 Negra Arroyo Lane", "87104", "Albuquerque"));
+ user.setPrevious(Collections.singletonList(new Address("12000 – 12100 Coors Rd SW", "87045", "Albuquerque")));
+
+ repository.save(user);
+
+ System.out.println("------- Annotated Single -------");
+ System.out.println(repository.findUserByIdIn(1000));
+ System.out.println(repository.findUserByIdIn(42));
+
+ System.out.println("------- Derived Single -------");
+ System.out.println(repository.findUserByUsername(user.getUsername()));
+
+ System.out.println("------- Derived SASI -------");
+ System.out.println(repository.findUsersByLastnameStartsWith("White", Limit.of(1)));
+ };
+
+ }
+
+}
diff --git a/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/User.java b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/User.java
new file mode 100644
index 000000000..111db6f2f
--- /dev/null
+++ b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/User.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+import org.springframework.data.cassandra.core.mapping.CassandraType;
+import org.springframework.data.cassandra.core.mapping.Column;
+import org.springframework.data.cassandra.core.mapping.PrimaryKey;
+import org.springframework.data.cassandra.core.mapping.Table;
+
+import com.datastax.oss.driver.api.core.data.UdtValue;
+
+/**
+ * Sample user class.
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Mark Paluch
+ */
+@Data
+@NoArgsConstructor
+@Table(value = "users")
+public class User {
+
+ @PrimaryKey("user_id") private Long id;
+
+ @Column("uname") private String username;
+ @Column("fname") private String firstname;
+ @Column("lname") private String lastname;
+
+ Address current;
+ List previous;
+
+ @CassandraType(type = CassandraType.Name.UDT, userTypeName = "address") UdtValue alternative;
+
+ public User(Long id) {
+ this.setId(id);
+ }
+
+ public User(Long id, String firstname, String lastname) {
+ this.id = id;
+ this.firstname = firstname;
+ this.lastname = lastname;
+ }
+}
diff --git a/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/UserRepository.java b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/UserRepository.java
new file mode 100644
index 000000000..487f98efe
--- /dev/null
+++ b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/UserRepository.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra;
+
+import java.util.List;
+
+import org.springframework.data.cassandra.repository.Query;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Simple repository interface for {@link User} instances. The interface is used to declare the so-called query methods,
+ * i.e. methods to retrieve single entities or collections of them.
+ *
+ * @author Thomas Darimont
+ */
+public interface UserRepository extends CrudRepository {
+
+ /**
+ * Sample method annotated with {@link Query}. This method executes the CQL from the {@link Query} value.
+ *
+ * @param id
+ * @return
+ */
+ @Query("SELECT * from users where user_id in(?0)")
+ User findUserByIdIn(long id);
+
+ /**
+ * Derived query method. This query corresponds with {@code SELECT * FROM users WHERE uname = ?0}.
+ * {@link User#username} is not part of the primary so it requires a secondary index.
+ *
+ * @param username
+ * @return
+ */
+ User findUserByUsername(String username);
+
+ /**
+ * Derived query method using SASI (SSTable Attached Secondary Index) features through the {@code LIKE} keyword. This
+ * query corresponds with {@code SELECT * FROM users WHERE lname LIKE '?0'}. {@link User#lastname} is not part of the
+ * primary key so it requires a secondary index.
+ *
+ * @param lastnamePrefix
+ * @return
+ */
+ List findUsersByLastnameStartsWith(String lastnamePrefix);
+
+ /**
+ * Same as {@link #findUsersByLastnameStartsWith(String)} but reducing the result size to a given {@link Limit}.
+ *
+ * @param lastnamePrefix
+ * @param maxResults the maximum number of results returned.
+ * @return
+ */
+ List findUsersByLastnameStartsWith(String lastnamePrefix, Limit maxResults);
+}
diff --git a/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/package-info.java b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/package-info.java
new file mode 100644
index 000000000..b4857317a
--- /dev/null
+++ b/cassandra/aot-optimization/src/main/java/example/springdata/cassandra/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Package showing a simple repository interface to use basic query method execution functionality.
+ */
+package example.springdata.cassandra;
diff --git a/cassandra/aot-optimization/src/main/resources/application.properties b/cassandra/aot-optimization/src/main/resources/application.properties
new file mode 100644
index 000000000..8a531ae97
--- /dev/null
+++ b/cassandra/aot-optimization/src/main/resources/application.properties
@@ -0,0 +1,3 @@
+spring.cassandra.keyspace-name=example
+spring.cassandra.schema-action=recreate
+spring.cassandra.local-datacenter=datacenter1
diff --git a/cassandra/example/pom.xml b/cassandra/example/pom.xml
index 1df483427..8bf284bea 100644
--- a/cassandra/example/pom.xml
+++ b/cassandra/example/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data.examplesspring-data-cassandra-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOT../pom.xml
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPerson.java b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPerson.java
new file mode 100644
index 000000000..95771e209
--- /dev/null
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPerson.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.auditing;
+
+import lombok.Data;
+
+import java.time.Instant;
+
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.LastModifiedBy;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.annotation.Transient;
+import org.springframework.data.cassandra.core.mapping.Table;
+import org.springframework.data.domain.Persistable;
+
+/**
+ * Simple domain object that declares properties annotated with Spring Data's annotations for auditing.
+ *
+ * @author Mark Paluch
+ */
+@Data
+@Table
+public class AuditedPerson implements Persistable {
+
+ @Id Long id;
+
+ @CreatedBy String createdBy;
+
+ @LastModifiedBy String lastModifiedBy;
+
+ @CreatedDate Instant createdDate;
+
+ @LastModifiedDate Instant lastModifiedDate;
+
+ @Transient boolean isNew;
+
+ @Override
+ public boolean isNew() {
+ return isNew;
+ }
+}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPersonRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPersonRepository.java
new file mode 100644
index 000000000..a21c26d8a
--- /dev/null
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/AuditedPersonRepository.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.auditing;
+
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Simple repository interface for {@link AuditedPerson} instances.
+ *
+ * @author Mark Paluch
+ */
+public interface AuditedPersonRepository extends CrudRepository {}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/auditing/BasicConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/BasicConfiguration.java
new file mode 100644
index 000000000..392743c0d
--- /dev/null
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/auditing/BasicConfiguration.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.auditing;
+
+import java.util.Optional;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.persistence.autoconfigure.EntityScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.cassandra.config.EnableCassandraAuditing;
+import org.springframework.data.cassandra.core.convert.CassandraCustomConversions;
+import org.springframework.data.cassandra.core.convert.MappingCassandraConverter;
+import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
+import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver;
+import org.springframework.data.domain.AuditorAware;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+
+/**
+ * Basic {@link Configuration} to create the necessary schema for the {@link AuditedPerson} table.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootApplication
+@EntityScan(basePackageClasses = AuditedPerson.class)
+@EnableCassandraAuditing
+class BasicConfiguration {
+
+ /**
+ * {@code @Bean} method defining a supplier for an auditor. This could be also an integration with a security
+ * framework such as Spring Security.
+ */
+ @Bean
+ AuditorAware auditorAware() {
+ return () -> Optional.of("Some user");
+ }
+
+ /**
+ * {@code @Bean} method defining a {@link MappingCassandraConverter} as currently the auditing requires a bean
+ * definition for {@link MappingCassandraConverter}.
+ */
+ @Bean
+ public MappingCassandraConverter cassandraConverter(CassandraMappingContext mapping,
+ CassandraCustomConversions conversions, CqlSession session) {
+
+ var converter = new MappingCassandraConverter(mapping);
+
+ converter.setCodecRegistry(session.getContext().getCodecRegistry());
+ converter.setCustomConversions(conversions);
+ converter.setUserTypeResolver(new SimpleUserTypeResolver(session));
+
+ return converter;
+ }
+
+}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicConfiguration.java
index 7373a1826..824016c35 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicConfiguration.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicConfiguration.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2013-2018 the original author or authors.
+ * Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,7 +16,7 @@
package example.springdata.cassandra.basic;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.context.annotation.Configuration;
/**
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java
index ca0482bcf..a3fc0601b 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2018 the original author or authors.
+ * Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,11 +18,12 @@
import java.util.List;
import org.springframework.data.cassandra.repository.Query;
+import org.springframework.data.domain.Limit;
import org.springframework.data.repository.CrudRepository;
/**
- * Simple repository interface for {@link User} instances. The interface is used to declare so called query methods,
- * methods to retrieve single entities or collections of them.
+ * Simple repository interface for {@link User} instances. The interface is used to declare the so-called query methods,
+ * i.e. methods to retrieve single entities or collections of them.
*
* @author Thomas Darimont
*/
@@ -55,4 +56,13 @@ public interface BasicUserRepository extends CrudRepository {
* @return
*/
List findUsersByLastnameStartsWith(String lastnamePrefix);
+
+ /**
+ * Same as {@link #findUsersByLastnameStartsWith(String)} but reducing the result size to a given {@link Limit}.
+ *
+ * @param lastnamePrefix
+ * @param maxResults the maximum number of results returned.
+ * @return
+ */
+ List findUsersByLastnameStartsWith(String lastnamePrefix, Limit maxResults);
}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/basic/User.java b/cassandra/example/src/main/java/example/springdata/cassandra/basic/User.java
index 922e0103c..96c05b186 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/basic/User.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/basic/User.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2018 the original author or authors.
+ * Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,4 +43,10 @@ public class User {
public User(Long id) {
this.setId(id);
}
+
+ public User(Long id, String firstname, String lastname) {
+ this.id = id;
+ this.firstname = firstname;
+ this.lastname = lastname;
+ }
}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/convert/Address.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Address.java
index 94a2826c8..47198f6dd 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/convert/Address.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Address.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,10 +15,6 @@
*/
package example.springdata.cassandra.convert;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
import org.springframework.data.cassandra.core.mapping.Element;
import org.springframework.data.cassandra.core.mapping.Tuple;
@@ -27,13 +23,7 @@
*
* @author Mark Paluch
*/
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
@Tuple
-public class Address {
+public record Address(@Element(0) String address, @Element(1) String city, @Element(2) String zip) {
- @Element(0) String address;
- @Element(1) String city;
- @Element(2) String zip;
}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/convert/Addressbook.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Addressbook.java
index 73ca183d6..97c817745 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/convert/Addressbook.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Addressbook.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/convert/Contact.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Contact.java
index c7df43e6b..8c9ca1a6d 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/convert/Contact.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Contact.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,20 +15,11 @@
*/
package example.springdata.cassandra.convert;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
/**
* Sample Contact class.
*
* @author Mark Paluch
*/
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-public class Contact {
+public record Contact(String firstname, String lastname) {
- String firstname;
- String lastname;
}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/convert/ConverterConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/ConverterConfiguration.java
index f8bdca6ca..719b30af5 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/convert/ConverterConfiguration.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/ConverterConfiguration.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -21,14 +21,14 @@
import java.util.List;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.cassandra.core.convert.CassandraCustomConversions;
import org.springframework.util.StringUtils;
-import com.datastax.driver.core.Row;
+import com.datastax.oss.driver.api.core.cql.Row;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
@@ -93,12 +93,7 @@ static class CustomAddressbookReadConverter implements Converter {}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/SimplePerson.java b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/SimplePerson.java
new file mode 100644
index 000000000..f1f26beda
--- /dev/null
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/SimplePerson.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.optimisticlocking;
+
+import lombok.Data;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.cassandra.core.mapping.Table;
+
+/**
+ * Simple domain object.
+ *
+ * @author Mark Paluch
+ */
+@Data
+@Table
+public class SimplePerson {
+
+ @Id Long id;
+
+ String name;
+
+}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/Customer.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/Customer.java
index 1fb8be58f..ddfbca773 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/projection/Customer.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/Customer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,10 +23,7 @@
/**
* @author Mark Paluch
*/
-@Value
@Table
-class Customer {
+record Customer(@Id String id, String firstname, String lastname) {
- @Id String id;
- String firstname, lastname;
}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerProjection.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerProjection.java
index 92dc48b53..a022c5b40 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerProjection.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerProjection.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerRepository.java
index cd7289d8f..2ff8f506a 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerRepository.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerSummary.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerSummary.java
index a86c31d51..ff19a305f 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerSummary.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerSummary.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/ProjectionConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/ProjectionConfiguration.java
index fc23bc278..a8434f85b 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/projection/ProjectionConfiguration.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/ProjectionConfiguration.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,7 +16,7 @@
package example.springdata.cassandra.projection;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.context.annotation.Configuration;
/**
diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/CassandraConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/CassandraConfiguration.java
similarity index 81%
rename from cassandra/java8/src/main/java/example/springdata/cassandra/java8/CassandraConfiguration.java
rename to cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/CassandraConfiguration.java
index 881432a5a..217fd1db4 100644
--- a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/CassandraConfiguration.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/CassandraConfiguration.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package example.springdata.cassandra.java8;
+package example.springdata.cassandra.streamoptional;
import org.springframework.boot.autoconfigure.SpringBootApplication;
diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Order.java b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/Order.java
similarity index 85%
rename from cassandra/java8/src/main/java/example/springdata/cassandra/java8/Order.java
rename to cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/Order.java
index 2ee4f6c3a..28986be11 100644
--- a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Order.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/Order.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package example.springdata.cassandra.java8;
+package example.springdata.cassandra.streamoptional;
import lombok.AllArgsConstructor;
import lombok.Data;
diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/OrderRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/OrderRepository.java
similarity index 69%
rename from cassandra/java8/src/main/java/example/springdata/cassandra/java8/OrderRepository.java
rename to cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/OrderRepository.java
index f16e92bf2..126a01741 100644
--- a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/OrderRepository.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/OrderRepository.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package example.springdata.cassandra.java8;
+package example.springdata.cassandra.streamoptional;
import java.time.LocalDate;
import java.time.ZoneId;
-import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.cassandra.repository.Query;
import org.springframework.data.repository.Repository;
@@ -35,12 +34,6 @@ public interface OrderRepository extends Repository {
@Query("SELECT * from pizza_orders WHERE orderdate = ?0 and zoneid = ?1 ALLOW FILTERING")
Order findOrderByOrderDateAndZoneId(LocalDate orderDate, ZoneId zoneId);
- /**
- * Parameter conversion can be overridden by using the {@link CassandraType} annotation.
- */
- @Query("SELECT * from pizza_orders WHERE orderdate = ?0 and zoneid = ?1 ALLOW FILTERING")
- Order findOrderByDate(com.datastax.driver.core.LocalDate orderDate, String zoneId);
-
void deleteAll();
Order save(Order order);
diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Person.java b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/Person.java
similarity index 84%
rename from cassandra/java8/src/main/java/example/springdata/cassandra/java8/Person.java
rename to cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/Person.java
index a7b45e9ec..0fa9730c5 100644
--- a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Person.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/Person.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package example.springdata.cassandra.java8;
+package example.springdata.cassandra.streamoptional;
import lombok.AllArgsConstructor;
import lombok.Data;
diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/PersonRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/PersonRepository.java
similarity index 90%
rename from cassandra/java8/src/main/java/example/springdata/cassandra/java8/PersonRepository.java
rename to cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/PersonRepository.java
index 79b56e941..408f8643e 100644
--- a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/PersonRepository.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/PersonRepository.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package example.springdata.cassandra.java8;
+package example.springdata.cassandra.streamoptional;
import java.util.Optional;
import java.util.stream.Stream;
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/package-info.java b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/package-info.java
new file mode 100644
index 000000000..fe7ea764c
--- /dev/null
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/streamoptional/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Package showing Java 8 features.
+ */
+package example.springdata.cassandra.streamoptional;
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/udt/Address.java b/cassandra/example/src/main/java/example/springdata/cassandra/udt/Address.java
index feef6853b..e133029d1 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/udt/Address.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/udt/Address.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,18 +15,12 @@
*/
package example.springdata.cassandra.udt;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
/**
* @author Mark Paluch
*/
-@Data
@UserDefinedType
-@AllArgsConstructor
-public class Address {
+public record Address(String street, String zip, String city) {
- String street, zip, city;
}
diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/udt/Person.java b/cassandra/example/src/main/java/example/springdata/cassandra/udt/Person.java
index c89b31d4a..14918c086 100644
--- a/cassandra/example/src/main/java/example/springdata/cassandra/udt/Person.java
+++ b/cassandra/example/src/main/java/example/springdata/cassandra/udt/Person.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,8 +23,7 @@
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.cassandra.core.mapping.Table;
-import com.datastax.driver.core.DataType.Name;
-import com.datastax.driver.core.UDTValue;
+import com.datastax.oss.driver.api.core.data.UdtValue;
/**
* @author Mark Paluch
@@ -39,6 +38,5 @@ public class Person {
Address current;
List previous;
- @CassandraType(type = Name.UDT, userTypeName = "address")
- UDTValue alternative;
+ @CassandraType(type = CassandraType.Name.UDT, userTypeName = "address") UdtValue alternative;
}
diff --git a/cassandra/example/src/main/resources/application.properties b/cassandra/example/src/main/resources/application.properties
index b94b9f183..8a531ae97 100644
--- a/cassandra/example/src/main/resources/application.properties
+++ b/cassandra/example/src/main/resources/application.properties
@@ -1,2 +1,3 @@
-spring.data.cassandra.keyspace-name=example
-spring.data.cassandra.schema-action=recreate
+spring.cassandra.keyspace-name=example
+spring.cassandra.schema-action=recreate
+spring.cassandra.local-datacenter=datacenter1
diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java
new file mode 100644
index 000000000..0a7cc86ba
--- /dev/null
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.auditing;
+
+import static org.assertj.core.api.Assertions.*;
+
+import example.springdata.cassandra.util.CassandraKeyspace;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * Integration test showing the basic usage of Auditing through {@link AuditedPersonRepository}.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootTest(classes = BasicConfiguration.class)
+@CassandraKeyspace
+class AuditedPersonRepositoryTests {
+
+ @Autowired AuditedPersonRepository repository;
+
+ @BeforeEach
+ void setUp() {
+ repository.deleteAll();
+ }
+
+ /**
+ * Saving an object using the Cassandra Repository will create a persistent representation of the object in Cassandra.
+ */
+ @Test
+ void insertShouldSetCreatedDate() {
+
+ var person = new AuditedPerson();
+ person.setId(42L);
+ person.setNew(true); // Cassandra does not support auto-generation hence we need
+ // to supply whether our object is a new one.
+ var saved = repository.save(person);
+
+ assertThat(saved.getCreatedBy()).isEqualTo("Some user");
+ assertThat(saved.getLastModifiedBy()).isEqualTo("Some user");
+ assertThat(saved.getCreatedDate()).isBetween(Instant.now().minus(Duration.ofMinutes(1)),
+ Instant.now().plus(Duration.ofMinutes(1)));
+ assertThat(saved.getLastModifiedDate()).isBetween(Instant.now().minus(Duration.ofMinutes(1)),
+ Instant.now().plus(Duration.ofMinutes(1)));
+ }
+
+ /**
+ * Modifying an existing object will update the last modified fields.
+ */
+ @Test
+ void updateShouldSetLastModifiedDate() {
+
+ var person = new AuditedPerson();
+ person.setId(42L);
+ person.setNew(true); // Cassandra does not support auto-generation hence we need
+ // to supply whether our object is a new one.
+ repository.save(person);
+
+ person.setNew(false);
+
+ var modified = repository.save(person);
+
+ assertThat(modified.getCreatedBy()).isEqualTo("Some user");
+ assertThat(modified.getLastModifiedBy()).isEqualTo("Some user");
+ assertThat(modified.getCreatedDate()).isBetween(Instant.now().minus(Duration.ofMinutes(1)),
+ Instant.now().plus(Duration.ofMinutes(1)));
+ assertThat(modified.getLastModifiedDate())
+ .isBetween(Instant.now().minus(Duration.ofMinutes(1)), Instant.now().plus(Duration.ofMinutes(1)))
+ .isNotEqualTo(modified.getCreatedDate());
+ }
+
+}
diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java
index f4d107cec..20832d068 100644
--- a/cassandra/example/src/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2013-2018 the original author or authors.
+ * Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,21 +16,22 @@
package example.springdata.cassandra.basic;
import static org.assertj.core.api.Assertions.*;
-import static org.junit.Assume.*;
+import static org.assertj.core.api.Assumptions.*;
import example.springdata.cassandra.util.CassandraKeyspace;
import example.springdata.cassandra.util.CassandraVersion;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import java.util.stream.LongStream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Limit;
import org.springframework.data.util.Version;
-import org.springframework.test.context.junit4.SpringRunner;
-import com.datastax.driver.core.Session;
+import com.datastax.oss.driver.api.core.CqlSession;
/**
* Integration test showing the basic usage of {@link BasicUserRepository}.
@@ -40,20 +41,18 @@
* @author Christoph Strobl
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
@SpringBootTest(classes = BasicConfiguration.class)
-public class BasicUserRepositoryTests {
-
- public final static Version CASSANDRA_3_4 = Version.parse("3.4");
+@CassandraKeyspace
+class BasicUserRepositoryTests {
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
+ private final static Version CASSANDRA_3_4 = Version.parse("3.4");
@Autowired BasicUserRepository repository;
- @Autowired Session session;
- User user;
+ @Autowired CqlSession session;
+ private User user;
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
user = new User();
user.setId(42L);
@@ -66,7 +65,7 @@ public void setUp() {
* Saving an object using the Cassandra Repository will create a persistent representation of the object in Cassandra.
*/
@Test
- public void findSavedUserById() {
+ void findSavedUserById() {
user = repository.save(user);
@@ -77,7 +76,7 @@ public void findSavedUserById() {
* Cassandra can be queries by using query methods annotated with {@link @Query}.
*/
@Test
- public void findByAnnotatedQueryMethod() {
+ void findByAnnotatedQueryMethod() {
repository.save(user);
@@ -91,7 +90,7 @@ public void findByAnnotatedQueryMethod() {
* key requires a secondary index.
*/
@Test
- public void findByDerivedQueryMethod() throws InterruptedException {
+ void findByDerivedQueryMethod() throws InterruptedException {
session.execute("CREATE INDEX IF NOT EXISTS user_username ON users (uname);");
/*
@@ -109,11 +108,12 @@ public void findByDerivedQueryMethod() throws InterruptedException {
* Spring Data Cassandra supports {@code LIKE} and {@code CONTAINS} query keywords to for SASI indexes.
*/
@Test
- public void findByDerivedQueryMethodWithSASI() throws InterruptedException {
+ void findByDerivedQueryMethodWithSASI() throws InterruptedException {
- assumeTrue(CassandraVersion.getReleaseVersion(session).isGreaterThanOrEqualTo(CASSANDRA_3_4));
+ assumeThat(CassandraVersion.getReleaseVersion(session).isGreaterThanOrEqualTo(CASSANDRA_3_4)).isTrue();
- session.execute("CREATE CUSTOM INDEX ON users (lname) USING 'org.apache.cassandra.index.sasi.SASIIndex';");
+ session.execute(
+ "CREATE CUSTOM INDEX IF NOT EXISTS users_lname_idx_1 ON users (lname) USING 'org.apache.cassandra.index.sasi.SASIIndex';");
/*
Cassandra secondary indexes are created in the background without the possibility to check
whether they are available or not. So we are forced to just wait. *sigh*
@@ -124,4 +124,25 @@ public void findByDerivedQueryMethodWithSASI() throws InterruptedException {
assertThat(repository.findUsersByLastnameStartsWith("last")).contains(user);
}
+
+ /**
+ * Spring Data Cassandra supports {@code Limit} to reduce the number of returned results.
+ */
+ @Test
+ void limitResultSize() throws InterruptedException {
+
+ assumeThat(CassandraVersion.getReleaseVersion(session).isGreaterThanOrEqualTo(CASSANDRA_3_4)).isTrue();
+
+ session.execute(
+ "CREATE CUSTOM INDEX IF NOT EXISTS users_lname_idx_1 ON users (lname) USING 'org.apache.cassandra.index.sasi.SASIIndex';");
+ /*
+ Cassandra secondary indexes are created in the background without the possibility to check
+ whether they are available or not. So we are forced to just wait. *sigh*
+ */
+ Thread.sleep(1000);
+
+ LongStream.range(0, 10).forEach(id -> repository.save(new User(id, user.getFirstname(), user.getLastname())));
+
+ assertThat(repository.findUsersByLastnameStartsWith("last", Limit.of(5))).hasSize(5);
+ }
}
diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/basic/CassandraOperationsIntegrationTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/basic/CassandraOperationsIntegrationTests.java
index 6e0d44d75..3c660b05f 100644
--- a/cassandra/example/src/test/java/example/springdata/cassandra/basic/CassandraOperationsIntegrationTests.java
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/basic/CassandraOperationsIntegrationTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,44 +19,36 @@
import example.springdata.cassandra.util.CassandraKeyspace;
-import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.cassandra.core.AsyncCassandraTemplate;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.CassandraTemplate;
-import org.springframework.test.context.junit4.SpringRunner;
-import org.springframework.util.concurrent.ListenableFuture;
-import com.datastax.driver.core.Row;
-import com.datastax.driver.core.Session;
-import com.datastax.driver.core.querybuilder.Insert;
-import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.cql.Row;
+import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
/**
* Integration test showing the basic usage of {@link CassandraTemplate}.
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
@SpringBootTest(classes = BasicConfiguration.class)
-public class CassandraOperationsIntegrationTests {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
+@CassandraKeyspace
+class CassandraOperationsIntegrationTests {
- @Autowired Session session;
+ @Autowired CqlSession session;
@Autowired CassandraOperations template;
- @Before
- public void setUp() throws Exception {
+ @BeforeEach
+ void setUp() {
template.getCqlOperations().execute("TRUNCATE users");
}
@@ -65,20 +57,20 @@ public void setUp() throws Exception {
* mapping layer.
*/
@Test
- public void insertAndSelect() {
+ void insertAndSelect() {
- Insert insert = QueryBuilder.insertInto("users").value("user_id", 42L) //
- .value("uname", "heisenberg") //
- .value("fname", "Walter") //
- .value("lname", "White") //
+ var insert = QueryBuilder.insertInto("users").value("user_id", QueryBuilder.literal(42L)) //
+ .value("uname", QueryBuilder.literal("heisenberg")) //
+ .value("fname", QueryBuilder.literal("Walter")) //
+ .value("lname", QueryBuilder.literal("White")) //
.ifNotExists(); //
- template.getCqlOperations().execute(insert);
+ template.getCqlOperations().execute(insert.asCql());
- User user = template.selectOneById(42L, User.class);
+ var user = template.selectOneById(42L, User.class);
assertThat(user.getUsername()).isEqualTo("heisenberg");
- List users = template.select(QueryBuilder.select().from("users"), User.class);
+ var users = template.select(QueryBuilder.selectFrom("users").all().asCql(), User.class);
assertThat(users).hasSize(1);
assertThat(users.get(0)).isEqualTo(user);
}
@@ -88,9 +80,9 @@ public void insertAndSelect() {
* {@code select}.
*/
@Test
- public void insertAndUpdate() {
+ void insertAndUpdate() {
- User user = new User();
+ var user = new User();
user.setId(42L);
user.setUsername("heisenberg");
user.setFirstname("Walter");
@@ -101,7 +93,7 @@ public void insertAndUpdate() {
user.setFirstname(null);
template.update(user);
- User loaded = template.selectOneById(42L, User.class);
+ var loaded = template.selectOneById(42L, User.class);
assertThat(loaded.getUsername()).isEqualTo("heisenberg");
assertThat(loaded.getFirstname()).isNull();
}
@@ -110,26 +102,24 @@ public void insertAndUpdate() {
* Asynchronous query execution using callbacks.
*/
@Test
- public void insertAsynchronously() throws InterruptedException {
+ void insertAsynchronously() throws InterruptedException {
- User user = new User();
+ var user = new User();
user.setId(42L);
user.setUsername("heisenberg");
user.setFirstname("Walter");
user.setLastname("White");
- final CountDownLatch countDownLatch = new CountDownLatch(1);
-
- AsyncCassandraTemplate asyncTemplate = new AsyncCassandraTemplate(session);
-
- ListenableFuture future = asyncTemplate.insert(user);
+ final var countDownLatch = new CountDownLatch(1);
- future.addCallback(it -> countDownLatch.countDown(), throwable -> countDownLatch.countDown());
+ var asyncTemplate = new AsyncCassandraTemplate(session);
- countDownLatch.await(5, TimeUnit.SECONDS);
+ var future = asyncTemplate.insert(user);
- User loaded = template.selectOneById(user.getId(), User.class);
- assertThat(loaded).isEqualTo(user);
+ future.whenComplete((it, ex) -> {
+ var loaded = template.selectOneById(it.getId(), User.class);
+ assertThat(loaded).isEqualTo(it);
+ });
}
/**
@@ -138,9 +128,9 @@ public void insertAsynchronously() throws InterruptedException {
*/
@Test
@SuppressWarnings("unchecked")
- public void selectProjections() {
+ void selectProjections() {
- User user = new User();
+ var user = new User();
user.setId(42L);
user.setUsername("heisenberg");
user.setFirstname("Walter");
@@ -148,13 +138,13 @@ public void selectProjections() {
template.insert(user);
- Long id = template.selectOne(QueryBuilder.select("user_id").from("users"), Long.class);
+ var id = template.selectOne(QueryBuilder.selectFrom("users").column("user_id").asCql(), Long.class);
assertThat(id).isEqualTo(user.getId());
- Row row = template.selectOne(QueryBuilder.select("user_id").from("users"), Row.class);
+ var row = template.selectOne(QueryBuilder.selectFrom("users").column("user_id").asCql(), Row.class);
assertThat(row.getLong(0)).isEqualTo(user.getId());
- Map map = template.selectOne(QueryBuilder.select().from("users"), Map.class);
+ Map map = template.selectOne(QueryBuilder.selectFrom("users").all().asCql(), Map.class);
assertThat(map).containsEntry("user_id", user.getId());
assertThat(map).containsEntry("fname", "Walter");
}
diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/convert/ConversionIntegrationTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/convert/ConversionIntegrationTests.java
index 210a604b7..14a89d4d9 100644
--- a/cassandra/example/src/test/java/example/springdata/cassandra/convert/ConversionIntegrationTests.java
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/convert/ConversionIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,32 +24,28 @@
import java.util.HashMap;
import java.util.Map;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.cassandra.core.CassandraOperations;
-import org.springframework.test.context.junit4.SpringRunner;
-import com.datastax.driver.core.Row;
-import com.datastax.driver.core.TupleValue;
-import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.oss.driver.api.core.cql.Row;
+import com.datastax.oss.driver.api.core.data.TupleValue;
+import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
/**
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
+@CassandraKeyspace
@SpringBootTest(classes = ConverterConfiguration.class)
-public class ConversionIntegrationTests {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
+class ConversionIntegrationTests {
@Autowired CassandraOperations operations;
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
operations.truncate(Addressbook.class);
}
@@ -58,9 +54,9 @@ public void setUp() {
* custom {@link example.springdata.cassandra.convert.ConverterConfiguration.PersonWriteConverter}.
*/
@Test
- public void shouldCreateAddressbook() {
+ void shouldCreateAddressbook() {
- Addressbook addressbook = new Addressbook();
+ var addressbook = new Addressbook();
addressbook.setId("private");
addressbook.setMe(new Contact("Walter", "White"));
@@ -68,7 +64,7 @@ public void shouldCreateAddressbook() {
operations.insert(addressbook);
- Row row = operations.selectOne(QueryBuilder.select().from("addressbook"), Row.class);
+ var row = operations.selectOne(QueryBuilder.selectFrom("addressbook").all().asCql(), Row.class);
assertThat(row).isNotNull();
@@ -82,9 +78,9 @@ public void shouldCreateAddressbook() {
* custom {@link example.springdata.cassandra.convert.ConverterConfiguration.PersonReadConverter}.
*/
@Test
- public void shouldReadAddressbook() {
+ void shouldReadAddressbook() {
- Addressbook addressbook = new Addressbook();
+ var addressbook = new Addressbook();
addressbook.setId("private");
addressbook.setMe(new Contact("Walter", "White"));
@@ -92,7 +88,7 @@ public void shouldReadAddressbook() {
operations.insert(addressbook);
- Addressbook loaded = operations.selectOne(QueryBuilder.select().from("addressbook"), Addressbook.class);
+ var loaded = operations.selectOne(QueryBuilder.selectFrom("addressbook").all().asCql(), Addressbook.class);
assertThat(loaded.getMe()).isEqualTo(addressbook.getMe());
assertThat(loaded.getFriends()).isEqualTo(addressbook.getFriends());
@@ -104,54 +100,52 @@ public void shouldReadAddressbook() {
* {@link example.springdata.cassandra.convert.ConverterConfiguration.CustomAddressbookReadConverter}.
*/
@Test
- public void shouldReadCustomAddressbook() {
+ void shouldReadCustomAddressbook() {
- Addressbook addressbook = new Addressbook();
+ var addressbook = new Addressbook();
addressbook.setId("private");
addressbook.setMe(new Contact("Walter", "White"));
operations.insert(addressbook);
- CustomAddressbook loaded = operations.selectOne(QueryBuilder.select().from("addressbook"), CustomAddressbook.class);
+ var loaded = operations.selectOne(QueryBuilder.selectFrom("addressbook").all().asCql(),
+ CustomAddressbook.class);
- assertThat(loaded.getTheId()).isEqualTo(addressbook.getId());
- assertThat(loaded.getMyDetailsAsJson()).contains("\"firstname\":\"Walter\"");
+ assertThat(loaded.theId()).isEqualTo(addressbook.getId());
+ assertThat(loaded.myDetailsAsJson()).contains("\"firstname\":\"Walter\"");
}
/**
* Creates and stores a new {@link Addressbook} inside of Cassandra writing map and tuple columns.
*/
@Test
- public void shouldWriteConvertedMapsAndTuples() {
+ void shouldWriteConvertedMapsAndTuples() {
- Addressbook addressbook = new Addressbook();
+ var addressbook = new Addressbook();
addressbook.setId("private");
Map preferredCurrencies = new HashMap<>();
preferredCurrencies.put(1, Currency.getInstance("USD"));
preferredCurrencies.put(2, Currency.getInstance("EUR"));
- Address address = new Address();
- address.setAddress("3828 Piermont Dr");
- address.setCity("Albuquerque");
- address.setZip("87111");
+ var address = new Address("3828 Piermont Dr", "Albuquerque", "87111");
addressbook.setPreferredCurrencies(preferredCurrencies);
addressbook.setAddress(address);
operations.insert(addressbook);
- Row row = operations.selectOne(QueryBuilder.select().from("addressbook"), Row.class);
+ var row = operations.selectOne(QueryBuilder.selectFrom("addressbook").all().asCql(), Row.class);
assertThat(row).isNotNull();
- TupleValue tupleValue = row.getTupleValue("address");
- assertThat(tupleValue.getString(0)).isEqualTo(address.getAddress());
- assertThat(tupleValue.getString(1)).isEqualTo(address.getCity());
- assertThat(tupleValue.getString(2)).isEqualTo(address.getZip());
+ var tupleValue = row.getTupleValue("address");
+ assertThat(tupleValue.getString(0)).isEqualTo(address.address());
+ assertThat(tupleValue.getString(1)).isEqualTo(address.city());
+ assertThat(tupleValue.getString(2)).isEqualTo(address.zip());
- Map rawPreferredCurrencies = row.getMap("preferredCurrencies", Integer.class, String.class);
+ var rawPreferredCurrencies = row.getMap("preferredCurrencies", Integer.class, String.class);
assertThat(rawPreferredCurrencies).containsEntry(1, "USD").containsEntry(2, "EUR");
}
diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/events/LifecycleEventsTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/events/LifecycleEventsTests.java
index 6fdb4ac0b..9809423a5 100644
--- a/cassandra/example/src/test/java/example/springdata/cassandra/events/LifecycleEventsTests.java
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/events/LifecycleEventsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,52 +20,48 @@
import java.util.List;
import java.util.stream.Stream;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.query.Query;
-import org.springframework.test.context.junit4.SpringRunner;
/**
* Test showing differences between fetching results as {@link List} and {@link Stream streaming} results using
- * Cassandra Lifecyle Events.
+ * Cassandra Lifecycle Events.
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
@SpringBootTest(classes = BasicConfiguration.class)
-public class LifecycleEventsTests {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
+@CassandraKeyspace
+class LifecycleEventsTests {
@Autowired CassandraOperations operations;
@Test
- public void shouldStreamEntities() {
+ void shouldStreamEntities() {
insertEntities();
- Stream userStream = operations.stream(Query.empty(), User.class);
+ var userStream = operations.stream(Query.empty(), User.class);
userStream.forEach(System.out::println);
}
@Test
- public void shouldReturnEntitiesAsList() {
+ void shouldReturnEntitiesAsList() {
insertEntities();
- List userStream = operations.select(Query.empty(), User.class);
+ var userStream = operations.select(Query.empty(), User.class);
userStream.forEach(System.out::println);
}
private void insertEntities() {
- User walter = new User(1, "Walter", "White");
- User skyler = new User(2, "Skyler", "White");
- User jesse = new User(3, "Jesse Pinkman", "Jesse Pinkman");
+ var walter = new User(1, "Walter", "White");
+ var skyler = new User(2, "Skyler", "White");
+ var jesse = new User(3, "Jesse Pinkman", "Jesse Pinkman");
operations.truncate(User.class);
diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepositoryTests.java
new file mode 100644
index 000000000..4266f036c
--- /dev/null
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepositoryTests.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.optimisticlocking;
+
+import static org.assertj.core.api.Assertions.*;
+
+import example.springdata.cassandra.util.CassandraKeyspace;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.dao.OptimisticLockingFailureException;
+import org.springframework.data.cassandra.core.CassandraOperations;
+import org.springframework.data.cassandra.core.UpdateOptions;
+import org.springframework.data.cassandra.core.query.Criteria;
+
+/**
+ * Integration test showing the basic usage of Optimistic Locking through {@link OptimisticPersonRepository}.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootTest(classes = BasicConfiguration.class)
+@CassandraKeyspace
+class OptimisticPersonRepositoryTests {
+
+ @Autowired OptimisticPersonRepository repository;
+ @Autowired CassandraOperations operations;
+
+ @BeforeEach
+ void setUp() {
+ repository.deleteAll();
+ }
+
+ /**
+ * Saving an object using the Cassandra Repository will create a persistent representation of the object in Cassandra
+ * and increment the version property.
+ */
+ @Test
+ void insertShouldIncrementVersion() {
+
+ var person = new OptimisticPerson(42L, 0, "Walter White");
+
+ var saved = repository.save(person);
+
+ assertThat(saved.version()).isGreaterThan(0);
+ }
+
+ /**
+ * Modifying an existing object will update the last modified fields.
+ */
+ @Test
+ void updateShouldDetectChangedEntity() {
+
+ var person = new OptimisticPerson(42L, 0, "Walter White");
+
+ // Load the person because we intend to change it.
+ var saved = repository.save(person);
+
+ // Another process has changed the person object in the meantime.
+ var anotherProcess = repository.findById(person.id()).get();
+ anotherProcess = anotherProcess.withName("Heisenberg");
+ repository.save(anotherProcess);
+
+ // Now it's our turn to modify the object...
+ var ourSaved = saved.withName("Walter");
+
+ // ...which fails with a OptimisticLockingFailureException, using LWT under the hood.
+ assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> repository.save(ourSaved));
+ }
+
+ /**
+ * This tests uses lightweight transactions by leveraging mapped {@code IF} conditions with the {@code UPDATE}
+ * statement through {@link CassandraOperations#update(Object, UpdateOptions)}.
+ */
+ @Test
+ void updateUsingLightWeightTransactions() {
+
+ var person = new SimplePerson();
+ person.setId(42L);
+ person.setName("Walter White");
+
+ operations.insert(person);
+
+ var success = operations.update(person,
+ UpdateOptions.builder().ifCondition(Criteria.where("name").is("Walter White")).build());
+
+ assertThat(success.wasApplied()).isTrue();
+
+ var failed = operations.update(person,
+ UpdateOptions.builder().ifCondition(Criteria.where("name").is("Heisenberg")).build());
+
+ assertThat(failed.wasApplied()).isFalse();
+ }
+
+}
diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/projection/CustomerRepositoryIntegrationTest.java b/cassandra/example/src/test/java/example/springdata/cassandra/projection/CustomerRepositoryIntegrationTest.java
index cc5eb8f14..fb92dacb2 100644
--- a/cassandra/example/src/test/java/example/springdata/cassandra/projection/CustomerRepositoryIntegrationTest.java
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/projection/CustomerRepositoryIntegrationTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,32 +21,28 @@
import java.util.Collection;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.projection.TargetAware;
-import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Integration tests for {@link CustomerRepository} to show projection capabilities.
*
* @author Mark Paluch
*/
-@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = ProjectionConfiguration.class)
-public class CustomerRepositoryIntegrationTest {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
+@CassandraKeyspace
+class CustomerRepositoryIntegrationTest {
@Autowired CustomerRepository customers;
- Customer dave, carter;
+ private Customer dave, carter;
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
customers.deleteAll();
@@ -55,27 +51,27 @@ public void setUp() {
}
@Test
- public void projectsEntityIntoInterface() {
+ void projectsEntityIntoInterface() {
- Collection result = customers.findAllProjectedBy();
+ var result = customers.findAllProjectedBy();
assertThat(result).hasSize(2);
assertThat(result.iterator().next().getFirstname()).isEqualTo("Carter");
}
@Test
- public void projectsDynamically() {
+ void projectsDynamically() {
- Collection result = customers.findById("d", CustomerProjection.class);
+ var result = customers.findById("d", CustomerProjection.class);
assertThat(result).hasSize(1);
assertThat(result.iterator().next().getFirstname()).isEqualTo("Dave");
}
@Test
- public void projectsIndividualDynamically() {
+ void projectsIndividualDynamically() {
- CustomerSummary result = customers.findProjectedById(dave.getId(), CustomerSummary.class);
+ var result = customers.findProjectedById(dave.id(), CustomerSummary.class);
assertThat(result).isNotNull();
assertThat(result.getFullName()).isEqualTo("Dave Matthews");
@@ -85,9 +81,9 @@ public void projectsIndividualDynamically() {
}
@Test
- public void projectIndividualInstance() {
+ void projectIndividualInstance() {
- CustomerProjection result = customers.findProjectedById(dave.getId());
+ var result = customers.findProjectedById(dave.id());
assertThat(result).isNotNull();
assertThat(result.getFirstname()).isEqualTo("Dave");
diff --git a/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Jsr310IntegrationTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/streamoptional/Jsr310IntegrationTests.java
similarity index 51%
rename from cassandra/java8/src/test/java/example/springdata/cassandra/java8/Jsr310IntegrationTests.java
rename to cassandra/example/src/test/java/example/springdata/cassandra/streamoptional/Jsr310IntegrationTests.java
index 9f08d9b4e..384d33295 100755
--- a/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Jsr310IntegrationTests.java
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/streamoptional/Jsr310IntegrationTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package example.springdata.cassandra.java8;
+package example.springdata.cassandra.streamoptional;
import static org.assertj.core.api.Assertions.*;
@@ -22,54 +22,35 @@
import java.time.LocalDate;
import java.time.ZoneId;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.data.util.Version;
-import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration test to show the usage of JSR-310 date/time types with Spring Data Cassandra.
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
+@CassandraKeyspace
@SpringBootTest(classes = CassandraConfiguration.class)
-public class Jsr310IntegrationTests {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost()
- .atLeast(Version.parse("3.0"));
+class Jsr310IntegrationTests {
@Autowired OrderRepository repository;
- @Before
- public void setUp() throws Exception {
+ @BeforeEach
+ void setUp() throws Exception {
repository.deleteAll();
}
@Test
- public void findOneByJsr310Types() {
+ void findOneByJsr310Types() {
- Order order = new Order("42", LocalDate.now(), ZoneId.systemDefault());
+ var order = new Order("42", LocalDate.now(), ZoneId.systemDefault());
repository.save(order);
assertThat(repository.findOrderByOrderDateAndZoneId(order.getOrderDate(), order.getZoneId())).isEqualTo(order);
}
-
- @Test
- public void findOneByConvertedTypes() {
-
- Order order = new Order("42", LocalDate.of(2010, 1, 2), ZoneId.systemDefault());
-
- repository.save(order);
-
- com.datastax.driver.core.LocalDate date = com.datastax.driver.core.LocalDate.fromYearMonthDay(2010, 1, 2);
- String zoneId = order.getZoneId().getId();
-
- assertThat(repository.findOrderByDate(date, zoneId)).isEqualTo(order);
- }
}
diff --git a/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Java8IntegrationTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/streamoptional/StreamOptionalIntegrationTests.java
similarity index 56%
rename from cassandra/java8/src/test/java/example/springdata/cassandra/java8/Java8IntegrationTests.java
rename to cassandra/example/src/test/java/example/springdata/cassandra/streamoptional/StreamOptionalIntegrationTests.java
index fb4d1dd8a..8734c9955 100755
--- a/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Java8IntegrationTests.java
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/streamoptional/StreamOptionalIntegrationTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package example.springdata.cassandra.java8;
+package example.springdata.cassandra.streamoptional;
import static org.assertj.core.api.Assertions.*;
@@ -23,48 +23,42 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.data.util.Version;
-import org.springframework.test.context.junit4.SpringRunner;
/**
- * Integration test to show the usage of Java 8 features with Spring Data Cassandra.
+ * Integration test to show the usage of Java {@link Stream} and {@link Optional} features with Spring Data Cassandra.
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
+@CassandraKeyspace
@SpringBootTest(classes = CassandraConfiguration.class)
-public class Java8IntegrationTests {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost()
- .atLeast(Version.parse("3.0"));
+class StreamOptionalIntegrationTests {
@Autowired PersonRepository repository;
- @Before
- public void setUp() throws Exception {
+ @BeforeEach
+ void setUp() throws Exception {
repository.deleteAll();
}
@Test
- public void providesFindOneWithOptional() {
+ void providesFindOneWithOptional() {
- Person homer = repository.save(new Person("1", "Homer", "Simpson"));
+ var homer = repository.save(new Person("1", "Homer", "Simpson"));
assertThat(repository.findById(homer.id).isPresent()).isTrue();
assertThat(repository.findById(homer.id + 1)).isEqualTo(Optional. empty());
}
@Test
- public void invokesDefaultMethod() {
+ void invokesDefaultMethod() {
- Person homer = repository.save(new Person("1", "Homer", "Simpson"));
- Optional result = repository.findByPerson(homer);
+ var homer = repository.save(new Person("1", "Homer", "Simpson"));
+ var result = repository.findByPerson(homer);
assertThat(result.isPresent()).isTrue();
assertThat(result.get()).isEqualTo(homer);
@@ -75,12 +69,12 @@ public void invokesDefaultMethod() {
* resulting {@link Stream} contains state it needs to be closed explicitly after use!
*/
@Test
- public void useJava8StreamsWithCustomQuery() {
+ void useJava8StreamsWithCustomQuery() {
- Person homer = repository.save(new Person("1", "Homer", "Simpson"));
- Person bart = repository.save(new Person("2", "Bart", "Simpson"));
+ var homer = repository.save(new Person("1", "Homer", "Simpson"));
+ var bart = repository.save(new Person("2", "Bart", "Simpson"));
- try (Stream stream = repository.findAll()) {
+ try (var stream = repository.findAll()) {
assertThat(stream.collect(Collectors.toList())).contains(homer, bart);
}
}
diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/udt/UserDefinedTypeIntegrationTest.java b/cassandra/example/src/test/java/example/springdata/cassandra/udt/UserDefinedTypeIntegrationTest.java
index 50e50ffb0..58faa0677 100644
--- a/cassandra/example/src/test/java/example/springdata/cassandra/udt/UserDefinedTypeIntegrationTest.java
+++ b/cassandra/example/src/test/java/example/springdata/cassandra/udt/UserDefinedTypeIntegrationTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,10 +21,9 @@
import java.util.Collections;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration;
@@ -32,11 +31,6 @@
import org.springframework.data.cassandra.config.SchemaAction;
import org.springframework.data.cassandra.core.CassandraAdminOperations;
import org.springframework.data.cassandra.core.CassandraOperations;
-import org.springframework.test.context.junit4.SpringRunner;
-
-import com.datastax.driver.core.KeyspaceMetadata;
-import com.datastax.driver.core.UDTValue;
-import com.datastax.driver.core.UserType;
/**
* Integration test to show User-Defined type support.
@@ -44,20 +38,33 @@
* @author Mark Paluch
* @author Oliver Gierke
*/
-@RunWith(SpringRunner.class)
@SpringBootTest
-public class UserDefinedTypeIntegrationTest {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
+@CassandraKeyspace
+class UserDefinedTypeIntegrationTest {
@Configuration
static class Config extends AbstractCassandraConfiguration {
+ @Override
+ protected int getPort() {
+ return Integer.getInteger("spring.cassandra.port");
+ }
+
+ @Override
+ protected String getContactPoints() {
+ return System.getProperty("spring.cassandra.contact-points");
+ }
+
@Override
public String getKeyspaceName() {
return "example";
}
+ @Override
+ protected String getLocalDataCenter() {
+ return "datacenter1";
+ }
+
@Override
public String[] getEntityBasePackages() {
return new String[] { Person.class.getPackage().getName() };
@@ -72,8 +79,8 @@ public SchemaAction getSchemaAction() {
@Autowired CassandraOperations operations;
@Autowired CassandraAdminOperations adminOperations;
- @Before
- public void before() throws Exception {
+ @BeforeEach
+ void before() throws Exception {
operations.getCqlOperations().execute("TRUNCATE person");
}
@@ -81,9 +88,9 @@ public void before() throws Exception {
* Insert a row with a mapped User-defined type.
*/
@Test
- public void insertMappedUdt() {
+ void insertMappedUdt() {
- Person person = new Person();
+ var person = new Person();
person.setId(42);
person.setFirstname("Walter");
person.setLastname("White");
@@ -93,7 +100,7 @@ public void insertMappedUdt() {
operations.insert(person);
- Person loaded = operations.selectOne("SELECT * FROM person WHERE id = 42", Person.class);
+ var loaded = operations.selectOne("SELECT * FROM person WHERE id = 42", Person.class);
assertThat(loaded.getCurrent()).isEqualTo(person.getCurrent());
assertThat(loaded.getPrevious()).containsAll(person.getPrevious());
@@ -103,17 +110,17 @@ public void insertMappedUdt() {
* Insert a row with a raw User-defined type.
*/
@Test
- public void insertRawUdt() {
+ void insertRawUdt() {
- KeyspaceMetadata keyspaceMetadata = adminOperations.getKeyspaceMetadata();
- UserType address = keyspaceMetadata.getUserType("address");
+ var keyspaceMetadata = adminOperations.getKeyspaceMetadata();
+ var address = keyspaceMetadata.getUserDefinedType("address").get();
- UDTValue udtValue = address.newValue();
+ var udtValue = address.newValue();
udtValue.setString("street", "308 Negra Arroyo Lane");
udtValue.setString("zip", "87104");
udtValue.setString("city", "Albuquerque");
- Person person = new Person();
+ var person = new Person();
person.setId(42);
person.setFirstname("Walter");
person.setLastname("White");
@@ -122,7 +129,7 @@ public void insertRawUdt() {
operations.insert(person);
- Person loaded = operations.selectOne("SELECT * FROM person WHERE id = 42", Person.class);
+ var loaded = operations.selectOne("SELECT * FROM person WHERE id = 42", Person.class);
assertThat(loaded.getAlternative().getString("zip")).isEqualTo("87104");
}
diff --git a/cassandra/java8/README.md b/cassandra/java8/README.md
deleted file mode 100644
index 8253681ff..000000000
--- a/cassandra/java8/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Spring Data Cassandra - Java 8 examples
-
-This project contains samples of Java 8 specific features of Spring Data (Cassandra).
-
-## Support for JDK 8's `Stream` for repository methods
-
-Repository methods can use a Java 8 `Stream` as a return type which will cause the reading of the results and the to-object-conversion of rows to happen while iterating over the stream.
-
-```java
-public interface PersonRepository extends CrudRepository {
-
- @Override
- List findAll();
-
- // Derived query method returning a Java 8 Stream
- Stream findAll();
-}
-```
-
-The test cases in `PersonRepositoryIntegrationTest` oppose a plain `List` based query method with one that uses a `Stream` and shows how the former pulls all data into memory first and the iteration is done over the pre-populated list. The execution of the `Stream`-based method in contrast shows that the individual elements are read and converted while iterating the stream.
diff --git a/cassandra/java8/pom.xml b/cassandra/java8/pom.xml
deleted file mode 100644
index 20fb0e32b..000000000
--- a/cassandra/java8/pom.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
- 4.0.0
-
-
- org.springframework.data.examples
- spring-data-cassandra-examples
- 2.0.0.BUILD-SNAPSHOT
- ../pom.xml
-
-
- spring-data-cassandra-java8
- Spring Data Cassandra - Java 8 specific features
-
-
-
-
- ${project.groupId}
- spring-data-cassandra-example-utils
- ${project.version}
- test
-
-
-
-
-
diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/package-info.java b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/package-info.java
deleted file mode 100644
index 476e12873..000000000
--- a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * Package showing Java 8 features.
- */
-package example.springdata.cassandra.java8;
diff --git a/cassandra/java8/src/main/resources/application.properties b/cassandra/java8/src/main/resources/application.properties
deleted file mode 100644
index b94b9f183..000000000
--- a/cassandra/java8/src/main/resources/application.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-spring.data.cassandra.keyspace-name=example
-spring.data.cassandra.schema-action=recreate
diff --git a/cassandra/kotlin/pom.xml b/cassandra/kotlin/pom.xml
index 5cc0a9ec4..ad47b113c 100644
--- a/cassandra/kotlin/pom.xml
+++ b/cassandra/kotlin/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data.examplesspring-data-cassandra-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOTspring-data-cassandra-kotlin
diff --git a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/ApplicationConfiguration.kt b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/ApplicationConfiguration.kt
index 88b7e04c3..da411332f 100644
--- a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/ApplicationConfiguration.kt
+++ b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/ApplicationConfiguration.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/Person.kt b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/Person.kt
index 370373bf0..d95c0c5c2 100644
--- a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/Person.kt
+++ b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/Person.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/PersonRepository.kt b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/PersonRepository.kt
index d728f689c..20e52dbb0 100644
--- a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/PersonRepository.kt
+++ b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/PersonRepository.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/kotlin/src/main/resources/application.properties b/cassandra/kotlin/src/main/resources/application.properties
index b94b9f183..8a531ae97 100644
--- a/cassandra/kotlin/src/main/resources/application.properties
+++ b/cassandra/kotlin/src/main/resources/application.properties
@@ -1,2 +1,3 @@
-spring.data.cassandra.keyspace-name=example
-spring.data.cassandra.schema-action=recreate
+spring.cassandra.keyspace-name=example
+spring.cassandra.schema-action=recreate
+spring.cassandra.local-datacenter=datacenter1
diff --git a/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/RepositoryTests.kt b/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/RepositoryTests.kt
index e416e16e8..013846cac 100644
--- a/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/RepositoryTests.kt
+++ b/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/RepositoryTests.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,37 +18,25 @@ package example.springdata.cassandra.kotlin
import example.springdata.cassandra.util.CassandraKeyspace
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
-import org.junit.Before
-import org.junit.ClassRule
-import org.junit.Test
-import org.junit.runner.RunWith
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.dao.EmptyResultDataAccessException
-import org.springframework.data.util.Version
-import org.springframework.test.context.junit4.SpringRunner
/**
* Tests showing Kotlin usage of Spring Data Repositories.
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner::class)
+@CassandraKeyspace
@SpringBootTest
class RepositoryTests {
- companion object {
-
- @JvmField
- @ClassRule
- val CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost()
- .atLeast(Version.parse("3.0"))
- }
-
@Autowired
lateinit var repository: PersonRepository
- @Before
+ @BeforeEach
fun before() {
repository.deleteAll()
}
diff --git a/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/TemplateTests.kt b/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/TemplateTests.kt
index 67f6c9a0e..d30decb0c 100644
--- a/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/TemplateTests.kt
+++ b/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/TemplateTests.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,52 +15,39 @@
*/
package example.springdata.cassandra.kotlin
-import com.datastax.driver.core.Row
-import com.datastax.driver.core.querybuilder.QueryBuilder
+import com.datastax.oss.driver.api.core.CqlIdentifier
+import com.datastax.oss.driver.api.querybuilder.QueryBuilder
import example.springdata.cassandra.util.CassandraKeyspace
import org.assertj.core.api.Assertions.assertThat
-import org.junit.Before
-import org.junit.ClassRule
-import org.junit.Test
-import org.junit.runner.RunWith
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.cassandra.core.*
-import org.springframework.data.cassandra.core.cql.CqlIdentifier
import org.springframework.data.cassandra.core.query.Query.query
import org.springframework.data.cassandra.core.query.isEqualTo
import org.springframework.data.cassandra.core.query.where
-import org.springframework.data.util.Version
-import org.springframework.test.context.junit4.SpringRunner
/**
* Tests showing Kotlin usage of [MongoTemplate] and its Kotlin extensions.
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner::class)
+@CassandraKeyspace
@SpringBootTest
class TemplateTests {
- companion object {
-
- @JvmField
- @ClassRule
- val CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost()
- .atLeast(Version.parse("3.0"))
- }
-
@Autowired
lateinit var operations: CassandraOperations
- @Before
+ @BeforeEach
fun before() {
operations.truncate()
}
@Test
fun `should create collection leveraging reified type parameters`() {
- assertThat(operations.getTableName()).isEqualTo(CqlIdentifier.of("person"))
+ assertThat(operations.getTableName()).isEqualTo(CqlIdentifier.fromCql("person"))
}
@Test
@@ -115,7 +102,7 @@ class TemplateTests {
@Test
fun `should apply defaulting for absent properties`() {
- operations.cqlOperations.execute(QueryBuilder.insertInto("person").value("firstname", "Walter"))
+ operations.cqlOperations.execute(QueryBuilder.insertInto("person").value("firstname", QueryBuilder.literal("Walter")).asCql())
val person = operations.query()
.matching(query(where("firstname").isEqualTo("Walter")))
diff --git a/cassandra/pom.xml b/cassandra/pom.xml
index 3ffbf9e43..554929ada 100644
--- a/cassandra/pom.xml
+++ b/cassandra/pom.xml
@@ -8,44 +8,33 @@
org.springframework.data.examplesspring-data-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOTSpring Data Cassandra - ExamplesSample projects for Spring Data Cassandra
- https://projects.spring.io/spring-data-cassandra
+ https://spring.io/spring-data-cassandra2014util
+ aot-optimizationexample
- java8kotlinreactive
-
- org.springframework.boot
- spring-boot-starter-aop
-
-
org.springframework.bootspring-boot-starter-data-cassandra
- com.datastax.cassandra
- cassandra-driver-core
-
-
-
-
- com.codahale.metrics
- metrics-core
- 3.0.2
+ org.springframework.boot
+ spring-boot-data-cassandra-test
+ test
diff --git a/cassandra/reactive/pom.xml b/cassandra/reactive/pom.xml
index 206f87553..83611380b 100644
--- a/cassandra/reactive/pom.xml
+++ b/cassandra/reactive/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data.examplesspring-data-cassandra-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOTspring-data-cassandra-reactive
@@ -18,21 +18,6 @@
reactor-core
-
- io.reactivex
- rxjava
-
-
-
- io.reactivex.rxjava2
- rxjava
-
-
-
- io.reactivex
- rxjava-reactive-streams
-
-
io.projectreactorreactor-test
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/ApplicationConfiguration.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/ApplicationConfiguration.java
new file mode 100644
index 000000000..ab8156a88
--- /dev/null
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/ApplicationConfiguration.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.auditing;
+
+import com.datastax.oss.driver.api.core.CqlIdentifier;
+import com.datastax.oss.driver.api.core.CqlSession;
+import reactor.core.publisher.Mono;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.cassandra.config.EnableReactiveCassandraAuditing;
+import org.springframework.data.cassandra.core.CassandraTemplate;
+import org.springframework.data.cassandra.core.InsertOptions;
+import org.springframework.data.cassandra.core.cql.CqlTemplate;
+import org.springframework.data.domain.ReactiveAuditorAware;
+
+/**
+ * Simple configuration for reactive Cassandra auditing.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootApplication
+@EnableReactiveCassandraAuditing
+class ApplicationConfiguration {
+
+ @Bean
+ ReactiveAuditorAware reactiveAuditorAware() {
+ return () -> Mono.just("the-current-user");
+ }
+}
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/Order.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/Order.java
new file mode 100644
index 000000000..27db7e648
--- /dev/null
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/Order.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.auditing;
+
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+import java.time.Instant;
+
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.LastModifiedBy;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.annotation.Transient;
+import org.springframework.data.cassandra.core.mapping.Table;
+import org.springframework.data.domain.Persistable;
+
+/**
+ * An entity to represent a Person.
+ *
+ * @author Mark Paluch
+ */
+@Data
+@Table
+@RequiredArgsConstructor
+public class Order implements Persistable {
+
+ @Transient boolean isNew;
+
+ @Id final String orderId;
+
+ @CreatedBy String createdBy;
+
+ @CreatedDate Instant createdDate;
+
+ @LastModifiedBy String lastModifiedBy;
+
+ @LastModifiedDate Instant lastModifiedDate;
+
+ @Override
+ public String getId() {
+ return getOrderId();
+ }
+
+ @Override
+ public boolean isNew() {
+ return isNew;
+ }
+}
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/OrderRepository.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/OrderRepository.java
new file mode 100644
index 000000000..792549c21
--- /dev/null
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/auditing/OrderRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.auditing;
+
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+
+/**
+ * @author Mark Paluch
+ */
+public interface OrderRepository extends ReactiveCrudRepository {}
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ApplicationConfiguration.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ApplicationConfiguration.java
index 13391475a..bddc7db74 100644
--- a/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ApplicationConfiguration.java
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ApplicationConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/people/Person.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/people/Person.java
index e43a95fae..1d41a703d 100644
--- a/cassandra/reactive/src/main/java/example/springdata/cassandra/people/Person.java
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/people/Person.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ReactivePersonRepository.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ReactivePersonRepository.java
index 5ede507b2..130781be3 100644
--- a/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ReactivePersonRepository.java
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ReactivePersonRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/
package example.springdata.cassandra.people;
+import org.springframework.data.domain.Limit;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -36,6 +37,15 @@ public interface ReactivePersonRepository extends ReactiveCrudRepository findByLastname(String lastname);
+ /**
+ * Derived query selecting by {@code lastname} reducing the result size to a given {@link Limit}.
+ *
+ * @param lastname
+ * @param maxResults the maximum number of results returned.
+ * @return
+ */
+ Flux findByLastname(String lastname, Limit maxResults);
+
/**
* String query selecting one entity.
*
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/people/RxJava2PersonRepository.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/people/RxJava2PersonRepository.java
deleted file mode 100644
index ed78f8ace..000000000
--- a/cassandra/reactive/src/main/java/example/springdata/cassandra/people/RxJava2PersonRepository.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2016-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.cassandra.people;
-
-import io.reactivex.Observable;
-import io.reactivex.Single;
-
-import org.springframework.data.cassandra.repository.Query;
-import org.springframework.data.repository.reactive.RxJava2CrudRepository;
-
-/**
- * Repository interface to manage {@link Person} instances.
- *
- * @author Mark Paluch
- */
-public interface RxJava2PersonRepository extends RxJava2CrudRepository {
-
- /**
- * Derived query selecting by {@code lastname}.
- *
- * @param lastname
- * @return
- */
- Observable findByLastname(String lastname);
-
- /**
- * String query selecting one entity.
- *
- * @param lastname
- * @return
- */
- @Query("SELECT * FROM person WHERE firstname = ?0 and lastname = ?1")
- Single findByFirstnameAndLastname(String firstname, String lastname);
-
- /**
- * Derived query selecting by {@code lastname}. {@code lastname} uses deferred resolution that does not require
- * blocking to obtain the parameter value.
- *
- * @param lastname
- * @return
- */
- Observable findByLastname(Single lastname);
-
- /**
- * Derived query selecting by {@code firstname} and {@code lastname}. {@code firstname} uses deferred resolution that
- * does not require blocking to obtain the parameter value.
- *
- * @param firstname
- * @param lastname
- * @return
- */
- Single findByFirstnameAndLastname(Single firstname, String lastname);
-}
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/ApplicationConfiguration.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/ApplicationConfiguration.java
new file mode 100644
index 000000000..8ad4d923d
--- /dev/null
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/ApplicationConfiguration.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.spel;
+
+import lombok.RequiredArgsConstructor;
+import lombok.Value;
+import reactor.core.publisher.Mono;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.spel.spi.EvaluationContextExtension;
+import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension;
+
+/**
+ * Simple configuration for reactive Cassandra SpEL support.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootApplication
+class ApplicationConfiguration {
+
+ @Bean
+ ReactiveTenantExtension tenantExtension() {
+ return ReactiveTenantExtension.INSTANCE;
+ }
+
+ /**
+ * Extension that looks up a {@link Tenant} from the {@link reactor.util.context.Context}.
+ */
+ enum ReactiveTenantExtension implements ReactiveEvaluationContextExtension {
+
+ INSTANCE;
+
+ @Override
+ public Mono extends EvaluationContextExtension> getExtension() {
+ return Mono.deferContextual(contextView -> Mono.just(new TenantExtension(contextView.get(Tenant.class))));
+ }
+
+ @Override
+ public String getExtensionId() {
+ return "my-reactive-tenant-extension";
+ }
+ }
+
+ /**
+ * Actual extension providing access to the {@link Tenant} value object.
+ */
+ @RequiredArgsConstructor
+ static class TenantExtension implements EvaluationContextExtension {
+
+ private final Tenant tenant;
+
+ @Override
+ public String getExtensionId() {
+ return "my-tenant-extension";
+ }
+
+ @Override
+ public Tenant getRootObject() {
+ return tenant;
+ }
+ }
+
+ /**
+ * The root object.
+ */
+ @Value
+ public static class Tenant {
+
+ String tenantId;
+
+ }
+}
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/Employee.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/Employee.java
new file mode 100644
index 000000000..5f3448f17
--- /dev/null
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/Employee.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.spel;
+
+import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
+import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
+import org.springframework.data.cassandra.core.mapping.Table;
+
+/**
+ * @author Mark Paluch
+ */
+@Table
+public record Employee(@PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED) String tenantId,
+ @PrimaryKeyColumn(type = PrimaryKeyType.CLUSTERED) String name) {
+
+}
diff --git a/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/EmployeeRepository.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/EmployeeRepository.java
new file mode 100644
index 000000000..33858406b
--- /dev/null
+++ b/cassandra/reactive/src/main/java/example/springdata/cassandra/spel/EmployeeRepository.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.spel;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.data.cassandra.repository.Query;
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+
+/**
+ * @author Mark Paluch
+ */
+public interface EmployeeRepository extends ReactiveCrudRepository {
+
+ @Query("SELECT * FROM employee WHERE tenantId = :#{getTenantId()} AND name = :name")
+ Flux findAllByName(String name);
+
+}
diff --git a/cassandra/reactive/src/test/java/example/springdata/cassandra/auditing/AuditingIntegrationTests.java b/cassandra/reactive/src/test/java/example/springdata/cassandra/auditing/AuditingIntegrationTests.java
new file mode 100644
index 000000000..814fdc9ae
--- /dev/null
+++ b/cassandra/reactive/src/test/java/example/springdata/cassandra/auditing/AuditingIntegrationTests.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.auditing;
+
+import static org.assertj.core.api.Assertions.*;
+
+import example.springdata.cassandra.util.CassandraKeyspace;
+import reactor.test.StepVerifier;
+
+import java.time.Instant;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.cassandra.test.autoconfigure.DataCassandraTest;
+
+/**
+ * Integration tests showing Reactive Auditing with Cassandra in action.
+ *
+ * @author Mark Paluch
+ */
+@CassandraKeyspace
+@DataCassandraTest
+public class AuditingIntegrationTests {
+
+ @Autowired OrderRepository orderRepository;
+
+ @Test
+ public void shouldUpdateAuditor() throws InterruptedException {
+
+ var order = new Order("4711");
+ order.setNew(true);
+
+ orderRepository.save(order).as(StepVerifier::create).assertNext(actual -> {
+
+ assertThat(actual.getCreatedBy()).isEqualTo("the-current-user");
+ assertThat(actual.getCreatedDate()).isBetween(Instant.now().minusSeconds(60), Instant.now().plusSeconds(60));
+ assertThat(actual.getLastModifiedBy()).isEqualTo("the-current-user");
+ assertThat(actual.getLastModifiedDate()).isBetween(Instant.now().minusSeconds(60), Instant.now().plusSeconds(60));
+
+ }).verifyComplete();
+
+ Thread.sleep(10);
+
+ order = orderRepository.findById("4711").block();
+
+ orderRepository.save(order).as(StepVerifier::create).assertNext(actual -> {
+
+ assertThat(actual.getCreatedBy()).isEqualTo("the-current-user");
+ assertThat(actual.getCreatedDate()).isBefore(actual.getLastModifiedDate());
+ assertThat(actual.getLastModifiedBy()).isEqualTo("the-current-user");
+ assertThat(actual.getLastModifiedDate()).isBetween(Instant.now().minusSeconds(60), Instant.now().plusSeconds(60));
+
+ }).verifyComplete();
+ }
+}
diff --git a/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactiveCassandraTemplateIntegrationTest.java b/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactiveCassandraTemplateIntegrationTest.java
index c1a62ce85..62cfc5148 100644
--- a/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactiveCassandraTemplateIntegrationTest.java
+++ b/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactiveCassandraTemplateIntegrationTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,51 +15,42 @@
*/
package example.springdata.cassandra.people;
-import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
-import static org.assertj.core.api.Assertions.*;
-
import example.springdata.cassandra.util.CassandraKeyspace;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-import reactor.test.StepVerifier;
-import rx.RxReactiveStreams;
-
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.cassandra.core.ReactiveCassandraTemplate;
-import org.springframework.test.context.junit4.SpringRunner;
+import reactor.core.publisher.Flux;
+import reactor.test.StepVerifier;
/**
* Integration test for {@link ReactiveCassandraTemplate}.
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
+@CassandraKeyspace
@SpringBootTest
-public class ReactiveCassandraTemplateIntegrationTest {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
+class ReactiveCassandraTemplateIntegrationTest {
@Autowired ReactiveCassandraTemplate template;
/**
* Truncate table and insert some rows.
*/
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
- Flux truncateAndInsert = template.truncate(Person.class) //
+ var truncateAndInsert = template.truncate(Person.class) //
.thenMany(Flux.just(new Person("Walter", "White", 50), //
new Person("Skyler", "White", 45), //
new Person("Saul", "Goodman", 42), //
new Person("Jesse", "Pinkman", 27))) //
.flatMap(template::insert);
- StepVerifier.create(truncateAndInsert).expectNextCount(4).verifyComplete();
+ truncateAndInsert.as(StepVerifier::create) //
+ .expectNextCount(4) //
+ .verifyComplete();
}
/**
@@ -67,9 +58,9 @@ public void setUp() {
* the two counts ({@code 4} and {@code 6}) to the console.
*/
@Test
- public void shouldInsertAndCountData() {
+ void shouldInsertAndCountData() {
- Mono saveAndCount = template.count(Person.class) //
+ var saveAndCount = template.count(Person.class) //
.doOnNext(System.out::println) //
.thenMany(Flux.just(new Person("Hank", "Schrader", 43), //
new Person("Mike", "Ehrmantraut", 62)))
@@ -78,23 +69,8 @@ public void shouldInsertAndCountData() {
.flatMap(v -> template.count(Person.class)) //
.doOnNext(System.out::println);
- StepVerifier.create(saveAndCount).expectNext(6L).verifyComplete();
- }
-
- /**
- * Note that the all object conversions are performed before the results are printed to the console.
- */
- @Test
- public void convertReactorTypesToRxJava1() throws Exception {
-
- Flux flux = template.select(select().from("person").where(eq("lastname", "White")), Person.class);
-
- long count = RxReactiveStreams.toObservable(flux) //
- .count() //
- .toSingle() //
- .toBlocking() //
- .value(); //
-
- assertThat(count).isEqualTo(2);
+ saveAndCount.as(StepVerifier::create) //
+ .expectNext(6L) //
+ .verifyComplete();
}
}
diff --git a/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactivePersonRepositoryIntegrationTest.java b/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactivePersonRepositoryIntegrationTest.java
index 670edec13..ca9f8a3b4 100644
--- a/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactivePersonRepositoryIntegrationTest.java
+++ b/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactivePersonRepositoryIntegrationTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,53 +16,52 @@
package example.springdata.cassandra.people;
import example.springdata.cassandra.util.CassandraKeyspace;
+import org.springframework.data.domain.Limit;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration test for {@link ReactivePersonRepository} using Project Reactor types and operators.
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
@SpringBootTest
-public class ReactivePersonRepositoryIntegrationTest {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
+@CassandraKeyspace
+class ReactivePersonRepositoryIntegrationTest {
@Autowired ReactivePersonRepository repository;
/**
* Clear table and insert some rows.
*/
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
- Flux deleteAndInsert = repository.deleteAll() //
+ var deleteAndInsert = repository.deleteAll() //
.thenMany(repository.saveAll(Flux.just(new Person("Walter", "White", 50), //
new Person("Skyler", "White", 45), //
new Person("Saul", "Goodman", 42), //
new Person("Jesse", "Pinkman", 27))));
- StepVerifier.create(deleteAndInsert).expectNextCount(4).verifyComplete();
+ deleteAndInsert.as(StepVerifier::create) //
+ .expectNextCount(4) //
+ .verifyComplete();
}
/**
* This sample performs a count, inserts data and performs a count again using reactive operator chaining.
*/
@Test
- public void shouldInsertAndCountData() {
+ void shouldInsertAndCountData() {
- Mono saveAndCount = repository.count() //
+ var saveAndCount = repository.count() //
.doOnNext(System.out::println) //
.thenMany(repository.saveAll(Flux.just(new Person("Hank", "Schrader", 43), //
new Person("Mike", "Ehrmantraut", 62)))) //
@@ -70,7 +69,9 @@ public void shouldInsertAndCountData() {
.flatMap(v -> repository.count()) //
.doOnNext(System.out::println);
- StepVerifier.create(saveAndCount).expectNext(6L).verifyComplete();
+ saveAndCount.as(StepVerifier::create) //
+ .expectNext(6L) //
+ .verifyComplete();
}
/**
@@ -78,9 +79,9 @@ public void shouldInsertAndCountData() {
* prefetch define the amount of fetched records.
*/
@Test
- public void shouldPerformConversionBeforeResultProcessing() {
+ void shouldPerformConversionBeforeResultProcessing() {
- StepVerifier.create(repository.findAll().doOnNext(System.out::println)) //
+ repository.findAll().doOnNext(System.out::println).as(StepVerifier::create) //
.expectNextCount(4) //
.verifyComplete();
}
@@ -89,33 +90,53 @@ public void shouldPerformConversionBeforeResultProcessing() {
* Fetch data using query derivation.
*/
@Test
- public void shouldQueryDataWithQueryDerivation() {
- StepVerifier.create(repository.findByLastname("White")).expectNextCount(2).verifyComplete();
+ void shouldQueryDataWithQueryDerivation() {
+
+ repository.findByLastname("White").as(StepVerifier::create) //
+ .expectNextCount(2) //
+ .verifyComplete();
+ }
+
+ /**
+ * Fetch data limiting result size.
+ */
+ @Test
+ void limitResultSize() {
+
+ repository.findByLastname("White", Limit.of(1)).as(StepVerifier::create) //
+ .expectNextCount(1) //
+ .verifyComplete();
}
/**
* Fetch data using a string query.
*/
@Test
- public void shouldQueryDataWithStringQuery() {
- StepVerifier.create(repository.findByFirstnameInAndLastname("Walter", "White")).expectNextCount(1).verifyComplete();
+ void shouldQueryDataWithStringQuery() {
+
+ repository.findByFirstnameInAndLastname("Walter", "White").as(StepVerifier::create) //
+ .expectNextCount(1) //
+ .verifyComplete();
}
/**
* Fetch data using query derivation.
*/
@Test
- public void shouldQueryDataWithDeferredQueryDerivation() {
- StepVerifier.create(repository.findByLastname(Mono.just("White"))).expectNextCount(2).verifyComplete();
+ void shouldQueryDataWithDeferredQueryDerivation() {
+
+ repository.findByLastname(Mono.just("White")).as(StepVerifier::create) //
+ .expectNextCount(2) //
+ .verifyComplete();
}
/**
* Fetch data using query derivation and deferred parameter resolution.
*/
@Test
- public void shouldQueryDataWithMixedDeferredQueryDerivation() {
+ void shouldQueryDataWithMixedDeferredQueryDerivation() {
- StepVerifier.create(repository.findByFirstnameAndLastname(Mono.just("Walter"), "White")) //
+ repository.findByFirstnameAndLastname(Mono.just("Walter"), "White").as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
}
diff --git a/cassandra/reactive/src/test/java/example/springdata/cassandra/people/RxJava2PersonRepositoryIntegrationTest.java b/cassandra/reactive/src/test/java/example/springdata/cassandra/people/RxJava2PersonRepositoryIntegrationTest.java
deleted file mode 100644
index bb315845e..000000000
--- a/cassandra/reactive/src/test/java/example/springdata/cassandra/people/RxJava2PersonRepositoryIntegrationTest.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright 2016-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.cassandra.people;
-
-import example.springdata.cassandra.util.CassandraKeyspace;
-import io.reactivex.Completable;
-import io.reactivex.Flowable;
-import io.reactivex.Single;
-
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
-import org.springframework.test.context.junit4.SpringRunner;
-
-/**
- * Integration test for {@link RxJava2PersonRepository} using RxJava1 types. Note that
- * {@link ReactiveCassandraOperations} is only available using Project Reactor types as the native Template API
- * implementation does not come in multiple reactive flavors.
- *
- * @author Mark Paluch
- */
-@RunWith(SpringRunner.class)
-@SpringBootTest
-public class RxJava2PersonRepositoryIntegrationTest {
-
- @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost();
-
- @Autowired RxJava2PersonRepository repository;
- @Autowired ReactiveCassandraOperations operations;
-
- @Before
- public void setUp() throws Exception {
-
- Completable deleteAll = repository.deleteAll();
-
- Flowable save = repository.saveAll(Flowable.just(new Person("Walter", "White", 50), //
- new Person("Skyler", "White", 45), //
- new Person("Saul", "Goodman", 42), //
- new Person("Jesse", "Pinkman", 27)));
-
- deleteAll.andThen(save).test().await().assertNoErrors();
- }
-
- /**
- * This sample performs a count, inserts data and performs a count again using reactive operator chaining. It prints
- * the two counts ({@code 4} and {@code 6}) to the console.
- */
- @Test
- public void shouldInsertAndCountData() {
-
-
- repository.count() //
- .doOnSuccess(System.out::println) //
- .toFlowable() //
- .switchMap(count -> repository.saveAll(Flowable.just(new Person("Hank", "Schrader", 43), //
- new Person("Mike", "Ehrmantraut", 62)))) //
- .lastElement() //
- .toSingle() //
- .flatMap(v -> repository.count()) //
- .doOnSuccess(System.out::println) //
- .test() //
- .awaitCount(1) //
- .assertValue(6L) //
- .assertNoErrors() //
- .awaitTerminalEvent();
- }
-
- /**
- * Result set {@link com.datastax.driver.core.Row}s are converted to entities as they are emitted. Reactive pull and
- * prefetch define the amount of fetched records.
- */
- @Test
- public void shouldPerformConversionBeforeResultProcessing() {
-
- repository.findAll() //
- .doOnNext(System.out::println) //
- .test() //
- .awaitCount(4) //
- .assertNoErrors() //
- .awaitTerminalEvent();
- }
-
- /**
- * Fetch data using query derivation.
- */
- @Test
- public void shouldQueryDataWithQueryDerivation() {
-
- repository.findByLastname("White") //
- .test() //
- .awaitCount(2) //
- .assertNoErrors() //
- .awaitTerminalEvent();
- }
-
- /**
- * Fetch data using a string query.
- */
- @Test
- public void shouldQueryDataWithStringQuery() {
-
- repository.findByFirstnameAndLastname("Walter", "White") //
- .test() //
- .awaitCount(1) //
- .assertNoErrors() //
- .awaitTerminalEvent();
- }
-
- /**
- * Fetch data using query derivation.
- */
- @Test
- public void shouldQueryDataWithDeferredQueryDerivation() {
-
- repository.findByLastname(Single.just("White")) //
- .test() //
- .awaitCount(2) //
- .assertNoErrors() //
- .awaitTerminalEvent();
- }
-
- /**
- * Fetch data using query derivation and deferred parameter resolution.
- */
- @Test
- public void shouldQueryDataWithMixedDeferredQueryDerivation() {
-
- repository.findByFirstnameAndLastname(Single.just("Walter"), "White") //
- .test() //
- .awaitCount(1) //
- .assertNoErrors() //
- .awaitTerminalEvent();
- }
-}
diff --git a/cassandra/reactive/src/test/java/example/springdata/cassandra/spel/ExpressionIntegrationTests.java b/cassandra/reactive/src/test/java/example/springdata/cassandra/spel/ExpressionIntegrationTests.java
new file mode 100644
index 000000000..3d4d97e2f
--- /dev/null
+++ b/cassandra/reactive/src/test/java/example/springdata/cassandra/spel/ExpressionIntegrationTests.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.spel;
+
+import static org.assertj.core.api.Assertions.*;
+
+import example.springdata.cassandra.spel.ApplicationConfiguration.Tenant;
+import example.springdata.cassandra.util.CassandraKeyspace;
+import reactor.test.StepVerifier;
+import reactor.util.context.Context;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.cassandra.test.autoconfigure.DataCassandraTest;
+
+/**
+ * Integration tests showing the SpEL context extension in action.
+ *
+ * @author Mark Paluch
+ */
+@CassandraKeyspace
+@DataCassandraTest
+class ExpressionIntegrationTests {
+
+ @Autowired EmployeeRepository employeeRepository;
+
+ @BeforeEach
+ void before() {
+
+ employeeRepository.deleteAll().as(StepVerifier::create).verifyComplete();
+
+ employeeRepository
+ .saveAll(Arrays.asList(new Employee("breaking-bad", "Walter"), new Employee("breaking-bad", "Hank"),
+ new Employee("south-park", "Hank"))) //
+ .as(StepVerifier::create) //
+ .expectNextCount(3) //
+ .verifyComplete();
+ }
+
+ @Test
+ void shouldFindByTenantIdAndName() {
+
+ employeeRepository.findAllByName("Walter") //
+ .contextWrite(Context.of(Tenant.class, new Tenant("breaking-bad"))).as(StepVerifier::create) //
+ .assertNext(actual -> {
+ assertThat(actual.tenantId()).isEqualTo("breaking-bad");
+ }).verifyComplete();
+
+ }
+}
diff --git a/cassandra/reactive/src/test/resources/application.properties b/cassandra/reactive/src/test/resources/application.properties
index 1ad354d9a..888a19512 100644
--- a/cassandra/reactive/src/test/resources/application.properties
+++ b/cassandra/reactive/src/test/resources/application.properties
@@ -1,4 +1,5 @@
-spring.data.cassandra.keyspace-name=example
-spring.data.cassandra.schema-action=recreate
+spring.cassandra.keyspace-name=example
+spring.cassandra.schema-action=recreate
+spring.cassandra.local-datacenter=datacenter1
logging.level.org.springframework.data.cassandra=INFO
diff --git a/cassandra/util/pom.xml b/cassandra/util/pom.xml
index 6ba8b4722..4e75c4988 100644
--- a/cassandra/util/pom.xml
+++ b/cassandra/util/pom.xml
@@ -1,11 +1,12 @@
-
+4.0.0org.springframework.data.examplesspring-data-cassandra-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOT../pom.xml
@@ -14,47 +15,29 @@
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
junitjunit
- org.apache.cassandra
- cassandra-all
- 3.9
-
-
- guava
- com.google.guava
-
-
- io.netty
- netty-all
-
-
- com.addthis.metrics
- reporter-config3
-
-
+ org.testcontainers
+ testcontainers-cassandra
- org.cassandraunit
- cassandra-unit
- 3.1.1.0
-
-
- io.netty
- netty-handler
-
-
+ org.apache.cassandra
+ java-driver-core
- com.google.guava
- guava
- 18.0
+ org.awaitility
+ awaitility
diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/Cassandra.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/Cassandra.java
deleted file mode 100644
index 5f6904723..000000000
--- a/cassandra/util/src/main/java/example/springdata/cassandra/util/Cassandra.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.cassandra.util;
-
-import java.util.concurrent.TimeUnit;
-
-import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
-import org.junit.AssumptionViolatedException;
-
-/**
- * {@link org.junit.rules.TestRule} for Cassandra server use. This rule can start a Cassandra instance, reuse a running
- * instance or simply require a running Cassandra server (will skip the test if Cassandra is not running).
- *
- * @author Mark Paluch
- */
-public class Cassandra extends CassandraResource {
-
- private final RuntimeMode runtimeMode;
-
- private Cassandra(String host, int port, RuntimeMode runtimeMode) {
-
- super(host, port);
- this.runtimeMode = runtimeMode;
- }
-
- /**
- * Require a running instance on {@code host:port}. Fails with {@link AssumptionViolatedException} if Cassandra is not
- * running.
- *
- * @param host must not be {@literal null} or empty.
- * @param port must be between 0 and 65535.
- * @return the {@link Cassandra} rule
- */
- public static Cassandra requireRunningInstance(String host, int port) {
- return new Cassandra(host, port, RuntimeMode.REQUIRE_RUNNING_INSTANCE);
- }
-
- /**
- * Start an embedded Cassandra instance on {@code host:port} if Cassandra is not running already.
- *
- * @param host must not be {@literal null} or empty.
- * @param port must be between 0 and 65535.
- * @return the {@link Cassandra} rule
- */
- public static Cassandra embeddedIfNotRunning(String host, int port) {
- return new Cassandra(host, port, RuntimeMode.EMBEDDED_IF_NOT_RUNNING);
- }
-
- @Override
- protected void before() throws Throwable {
-
- if (runtimeMode == RuntimeMode.REQUIRE_RUNNING_INSTANCE) {
- if (!CassandraSocket.isConnectable(getHost(), getPort())) {
- throw new AssumptionViolatedException(
- String.format("Cassandra is not reachable at %s:%s.", getHost(), getPort()));
- }
- }
-
- if (runtimeMode == RuntimeMode.EMBEDDED_IF_NOT_RUNNING) {
- if (CassandraSocket.isConnectable(getHost(), getPort())) {
- return;
- }
- }
-
- EmbeddedCassandraServerHelper.startEmbeddedCassandra("embedded-cassandra.yaml", "target/embeddedCassandra",
- TimeUnit.SECONDS.toMillis(60));
- super.before();
- }
-
- private enum RuntimeMode {
- REQUIRE_RUNNING_INSTANCE, EMBEDDED_IF_NOT_RUNNING;
- }
-}
diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraExtension.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraExtension.java
new file mode 100644
index 000000000..6adb2bdab
--- /dev/null
+++ b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraExtension.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.util;
+
+import java.net.InetSocketAddress;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.util.AnnotationUtils;
+
+import org.springframework.util.StringUtils;
+
+import org.testcontainers.cassandra.CassandraContainer;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+
+/**
+ * JUnit 5 {@link BeforeAllCallback} extension to ensure a running Cassandra server.
+ *
+ * @author Mark Paluch
+ * @see CassandraKeyspace
+ */
+class CassandraExtension implements BeforeAllCallback {
+
+ private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace
+ .create(CassandraExtension.class);
+
+ private static CassandraContainer container;
+
+ @Override
+ public void beforeAll(ExtensionContext context) {
+
+ var store = context.getStore(NAMESPACE);
+ var cassandra = findAnnotation(context);
+
+ var keyspace = store.getOrComputeIfAbsent(CassandraServer.class, it -> {
+
+ CassandraContainer container = runTestcontainer();
+ System.setProperty("spring.cassandra.port", "" + container.getMappedPort(9042));
+ System.setProperty("spring.cassandra.contact-points", "" + container.getHost());
+
+ return new CassandraServer(container.getHost(), container.getMappedPort(9042),
+ CassandraServer.RuntimeMode.EMBEDDED_IF_NOT_RUNNING);
+ }, CassandraServer.class);
+
+ keyspace.before();
+
+
+ Callable sessionFactory = () -> CqlSession.builder()
+ .addContactPoint(new InetSocketAddress(keyspace.host(), keyspace.port())).withLocalDatacenter("datacenter1")
+ .build();
+ Awaitility.await().ignoreExceptions().untilAsserted(() -> {
+
+ sessionFactory.call().close();
+ });
+
+ var session = store.getOrComputeIfAbsent(CqlSession.class, it -> {
+
+ try {
+ return sessionFactory.call();
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }, CqlSession.class);
+
+ session.execute(String.format("CREATE KEYSPACE IF NOT EXISTS %s \n"
+ + "WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };", cassandra.keyspace()));
+ }
+
+ private static CassandraKeyspace findAnnotation(ExtensionContext context) {
+
+ var testClass = context.getRequiredTestClass();
+
+ var annotation = AnnotationUtils.findAnnotation(testClass, CassandraKeyspace.class);
+
+ return annotation.orElseThrow(() -> new IllegalStateException("Test class not annotated with @Cassandra"));
+ }
+
+ private CassandraContainer runTestcontainer() {
+
+ if (container != null) {
+ return container;
+ }
+
+ container = new CassandraContainer(getCassandraDockerImageName());
+ container.withReuse(true);
+
+ container.start();
+
+ return container;
+ }
+
+ private String getCassandraDockerImageName() {
+
+ return String.format("cassandra:%s",
+ Optional.ofNullable(System.getenv("CASSANDRA_VERSION")).filter(StringUtils::hasText).orElse("3.11.10"));
+ }
+}
diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraKeyspace.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraKeyspace.java
index 217808536..6c8903a68 100644
--- a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraKeyspace.java
+++ b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraKeyspace.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2021-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,127 +15,29 @@
*/
package example.springdata.cassandra.util;
-import io.netty.channel.EventLoopGroup;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
-import java.util.concurrent.TimeUnit;
-
-import org.junit.AssumptionViolatedException;
-import org.springframework.data.util.Version;
-import org.springframework.util.Assert;
-
-import com.datastax.driver.core.Cluster;
-import com.datastax.driver.core.NettyOptions;
-import com.datastax.driver.core.ProtocolOptions;
-import com.datastax.driver.core.Session;
+import org.junit.jupiter.api.extension.ExtendWith;
/**
- * {@link CassandraResource} to require (create or reuse) an Apache Cassandra keyspace and optionally require a specific
- * Apache Cassandra version. This {@link org.junit.rules.TestRule} can be chained to depend on another
- * {@link CassandraResource} rule to require a running instance/start an embedded Apache Cassandra instance.
+ * Annotation that can activates embedded Cassandra providing a keyspace at {@link #keyspace()}
*
* @author Mark Paluch
*/
-public class CassandraKeyspace extends CassandraResource {
-
- private final String keyspaceName;
- private final Version requiredVersion;
- private final CassandraResource dependency;
-
- private CassandraKeyspace(String host, int port, String keyspaceName, CassandraResource dependency,
- Version requiredVersion) {
-
- super(host, port);
-
- this.keyspaceName = keyspaceName;
- this.dependency = dependency;
- this.requiredVersion = requiredVersion;
- }
-
- /**
- * Create a {@link CassandraKeyspace} test rule to provide a running Cassandra instance on {@code localhost:9042} with
- * a keyspace {@code example}. Reuses a running Cassandra instance if available or starts an embedded instance.
- *
- * @return the {@link CassandraKeyspace} rule.
- */
- public static CassandraKeyspace onLocalhost() {
- return new CassandraKeyspace("localhost", ProtocolOptions.DEFAULT_PORT, "example",
- Cassandra.embeddedIfNotRunning("localhost", ProtocolOptions.DEFAULT_PORT), new Version(0, 0, 0));
- }
-
- /**
- * Setup a dependency to an upstream {@link CassandraResource}. The dependency is activated by {@code this} test rule.
- *
- * @param cassandraResource must not be {@literal null}.
- * @return the {@link CassandraKeyspace} rule.
- */
- public CassandraKeyspace dependsOn(CassandraResource cassandraResource) {
-
- Assert.notNull(cassandraResource, "CassandraResource must not be null!");
-
- return new CassandraKeyspace(getHost(), getPort(), keyspaceName, cassandraResource, requiredVersion);
- }
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ExtendWith(CassandraExtension.class)
+public @interface CassandraKeyspace {
/**
- * Setup a version requirement.
+ * Name of the desired keyspace to be provided.
*
- * @param requiredVersion must not be {@literal null}.
- * @return the {@link CassandraKeyspace} rule
+ * @return
*/
- public CassandraKeyspace atLeast(Version requiredVersion) {
-
- Assert.notNull(requiredVersion, "Required version must not be null!");
-
- return new CassandraKeyspace(getHost(), getPort(), keyspaceName, dependency, requiredVersion);
- }
-
- /*
- * (non-Javadoc)
- * @see org.junit.rules.ExternalResource#before()
- */
- @Override
- protected void before() throws Throwable {
-
- dependency.before();
-
- Cluster cluster = Cluster.builder().addContactPoint(getHost()).withPort(getPort())
- .withNettyOptions(new NettyOptions() {
- @Override
- public void onClusterClose(EventLoopGroup eventLoopGroup) {
- eventLoopGroup.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS).syncUninterruptibly();
- }
- }).build();
-
- Session session = cluster.newSession();
-
- try {
-
- if (requiredVersion != null) {
-
- Version cassandraReleaseVersion = CassandraVersion.getReleaseVersion(session);
-
- if (cassandraReleaseVersion.isLessThan(requiredVersion)) {
- throw new AssumptionViolatedException(
- String.format("Cassandra at %s:%s runs in Version %s but we require at least %s", getHost(), getPort(),
- cassandraReleaseVersion, requiredVersion));
- }
- }
-
- session.execute(String.format("CREATE KEYSPACE IF NOT EXISTS %s \n"
- + "WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };", keyspaceName));
- } finally {
- session.close();
- cluster.close();
- }
- }
-
- /*
- * (non-Javadoc)
- * @see org.junit.rules.ExternalResource#after()
- */
- @Override
- protected void after() {
-
- super.after();
- dependency.after();
- }
+ String keyspace() default "example";
}
diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraResource.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraResource.java
deleted file mode 100644
index 444e0b849..000000000
--- a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraResource.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.cassandra.util;
-
-import org.junit.rules.ExternalResource;
-import org.springframework.util.Assert;
-
-/**
- * Base class to abstract contact point details for Apache Cassandra as {@link ExternalResource}.
- *
- * @author Mark Paluch
- */
-public abstract class CassandraResource extends ExternalResource {
-
- private final String host;
- private final int port;
-
- CassandraResource(String host, int port) {
-
- Assert.hasText(host, "Host must not be null or empty!");
- Assert.isTrue(port >= 0 && port <= 65535, "Port must be in the range of 0..65535!");
-
- this.host = host;
- this.port = port;
- }
-
- /**
- * @return the Cassandra hostname.
- */
- public String getHost() {
- return host;
- }
-
- /**
- * @return the Cassandra port.
- */
- public int getPort() {
- return port;
- }
-
- /*
- * (non-Javadoc)
- * @see org.junit.rules.ExternalResource#before()
- */
- @Override
- protected void before() throws Throwable {
- super.before();
- }
-
- /*
- * (non-Javadoc)
- * @see org.junit.rules.ExternalResource#after()
- */
- @Override
- protected void after() {
- super.after();
- }
-}
diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraServer.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraServer.java
new file mode 100644
index 000000000..bb5a6cbba
--- /dev/null
+++ b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraServer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.cassandra.util;
+
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Assumptions;
+
+import org.springframework.util.Assert;
+
+/**
+ * Utility for Cassandra server use. This utility can start a Cassandra instance, reuse a running instance or simply
+ * require a running Cassandra server (will skip the test if Cassandra is not running).
+ *
+ * @author Mark Paluch
+ */
+record CassandraServer(String host, int port,
+ example.springdata.cassandra.util.CassandraServer.RuntimeMode runtimeMode) {
+
+ /**
+ * Require a running instance on {@code host:port}. Fails with {@link AssumptionViolatedException} if Cassandra is not
+ * running.
+ *
+ * @param host must not be {@literal null} or empty.
+ * @param port must be between 0 and 65535.
+ * @return the {@link CassandraServer} rule
+ */
+ public static CassandraServer requireRunningInstance(String host, int port) {
+ return new CassandraServer(host, port, RuntimeMode.REQUIRE_RUNNING_INSTANCE);
+ }
+
+ /**
+ * Start an embedded Cassandra instance on {@code host:port} if Cassandra is not running already.
+ *
+ * @param host must not be {@literal null} or empty.
+ * @param port must be between 0 and 65535.
+ * @return the {@link CassandraServer} rule
+ */
+ public static CassandraServer embeddedIfNotRunning(String host, int port) {
+ return new CassandraServer(host, port, RuntimeMode.EMBEDDED_IF_NOT_RUNNING);
+ }
+
+ /**
+ * @param host must not be {@literal null} or empty.
+ * @param port
+ * @return {@literal true} if the TCP port accepts a connection.
+ */
+ public static boolean isConnectable(String host, int port) {
+
+ Assert.hasText(host, "Host must not be null or empty!");
+
+ try (var socket = new Socket()) {
+
+ socket.setSoLinger(true, 0);
+ socket.connect(new InetSocketAddress(host, port), (int) TimeUnit.MILLISECONDS.convert(10, TimeUnit.SECONDS));
+
+ return true;
+
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ protected void before() {
+
+ if (runtimeMode == RuntimeMode.REQUIRE_RUNNING_INSTANCE) {
+ Assumptions.assumeTrue(isConnectable(getHost(), getPort()),
+ () -> String.format("Cassandra is not reachable at %s:%s.", getHost(), getPort()));
+ }
+
+ if (runtimeMode == RuntimeMode.EMBEDDED_IF_NOT_RUNNING) {
+ if (isConnectable(getHost(), getPort())) {
+ return;
+ }
+ }
+ }
+
+ enum RuntimeMode {
+ REQUIRE_RUNNING_INSTANCE, EMBEDDED_IF_NOT_RUNNING;
+ }
+}
diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraSocket.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraSocket.java
deleted file mode 100644
index 1df0bd9d9..000000000
--- a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraSocket.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.cassandra.util;
-
-import lombok.experimental.UtilityClass;
-
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.util.concurrent.TimeUnit;
-
-import org.springframework.util.Assert;
-
-/**
- * @author Mark Paluch
- */
-@UtilityClass
-class CassandraSocket {
-
- /**
- * @param host must not be {@literal null} or empty.
- * @param port
- * @return {@literal true} if the TCP port accepts a connection.
- */
- public static boolean isConnectable(String host, int port) {
-
- Assert.hasText(host, "Host must not be null or empty!");
-
- try (Socket socket = new Socket()) {
-
- socket.setSoLinger(true, 0);
- socket.connect(new InetSocketAddress(host, port), (int) TimeUnit.MILLISECONDS.convert(10, TimeUnit.SECONDS));
-
- return true;
-
- } catch (Exception e) {
- return false;
- }
- }
-}
diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraVersion.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraVersion.java
index 295a21f34..77a99d40c 100644
--- a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraVersion.java
+++ b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraVersion.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,14 +15,14 @@
*/
package example.springdata.cassandra.util;
+import lombok.experimental.UtilityClass;
+
import org.springframework.data.util.Version;
import org.springframework.util.Assert;
-import com.datastax.driver.core.ResultSet;
-import com.datastax.driver.core.Row;
-import com.datastax.driver.core.Session;
-
-import lombok.experimental.UtilityClass;
+import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.cql.ResultSet;
+import com.datastax.oss.driver.api.core.cql.Row;
/**
* Utility to retrieve the Cassandra release version.
@@ -38,12 +38,12 @@ public class CassandraVersion {
* @param session must not be {@literal null}.
* @return the release {@link Version}.
*/
- public static Version getReleaseVersion(Session session) {
+ public static Version getReleaseVersion(CqlSession session) {
Assert.notNull(session, "Session must not be null");
- ResultSet resultSet = session.execute("SELECT release_version FROM system.local;");
- Row row = resultSet.one();
+ var resultSet = session.execute("SELECT release_version FROM system.local;");
+ var row = resultSet.one();
return Version.parse(row.getString(0));
}
diff --git a/cassandra/util/src/main/resources/application.conf b/cassandra/util/src/main/resources/application.conf
new file mode 100644
index 000000000..672734b89
--- /dev/null
+++ b/cassandra/util/src/main/resources/application.conf
@@ -0,0 +1,234 @@
+# Configuration for the DataStax Java driver for Apache Cassandra®.
+#
+# Unless you use a custom mechanism to load your configuration (see
+# SessionBuilder.withConfigLoader), all the values declared here will be used as defaults. You can
+# place your own `application.conf` in the classpath to override them.
+#
+# Options are classified into two categories:
+# - basic: what is most likely to be customized first when kickstarting a new application.
+# - advanced: more elaborate tuning options, or "expert"-level customizations.
+#
+# This file is in HOCON format, see https://github.com/typesafehub/config/blob/master/HOCON.md.
+datastax-java-driver {
+
+ basic.load-balancing-policy {
+ class = DcInferringLoadBalancingPolicy
+ }
+
+ basic.request {
+ # How long the driver waits for a request to complete. This is a global limit on the duration of
+ # a session.execute() call, including any internal retries the driver might do.
+ #
+ # By default, this value is set pretty high to ensure that DDL queries don't time out, in order
+ # to provide the best experience for new users trying the driver with the out-of-the-box
+ # configuration.
+ # For any serious deployment, we recommend that you use separate configuration profiles for DDL
+ # and DML; you can then set the DML timeout much lower (down to a few milliseconds if needed).
+ #
+ # Note that, because timeouts are scheduled on the driver's timer thread, the duration specified
+ # here must be greater than the timer tick duration defined by the
+ # advanced.netty.timer.tick-duration setting (see below). If that is not the case, timeouts will
+ # not be triggered as timely as desired.
+ #
+ # Required: yes
+ # Modifiable at runtime: yes, the new value will be used for requests issued after the change.
+ # Overridable in a profile: yes
+ timeout = 8 seconds
+ }
+
+ # ADVANCED OPTIONS -------------------------------------------------------------------------------
+
+ advanced.connection {
+ # The timeout to use for internal queries that run as part of the initialization process, just
+ # after we open a connection. If this timeout fires, the initialization of the connection will
+ # fail. If this is the first connection ever, the driver will fail to initialize as well,
+ # otherwise it will retry the connection later.
+ #
+ # Required: yes
+ # Modifiable at runtime: yes, the new value will be used for connections created after the
+ # change.
+ # Overridable in a profile: no
+ init-query-timeout = 500 milliseconds
+
+ # The driver maintains a connection pool to each node, according to the distance assigned to it
+ # by the load balancing policy. If the distance is IGNORED, no connections are maintained.
+ pool {
+ local {
+ # The number of connections in the pool.
+ #
+ # Required: yes
+ # Modifiable at runtime: yes; when the change is detected, all active pools will be notified
+ # and will adjust their size.
+ # Overridable in a profile: no
+ size = 1
+ }
+ remote {
+ size = 1
+ }
+ }
+ }
+ advanced.metrics {
+ # The session-level metrics (all disabled by default).
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ session {
+ enabled = []
+
+ }
+ # The node-level metrics (all disabled by default).
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ node {
+ enabled = []
+ }
+ }
+
+ advanced.control-connection {
+ schema-agreement {
+ # The interval between each attempt.
+ # Required: yes
+ # Modifiable at runtime: yes, the new value will be used for checks issued after the change.
+ # Overridable in a profile: no
+ interval = 100 seconds
+
+ # The timeout after which schema agreement fails.
+ # If this is set to 0, schema agreement is skipped and will always fail.
+ #
+ # Required: yes
+ # Modifiable at runtime: yes, the new value will be used for checks issued after the change.
+ # Overridable in a profile: no
+ timeout = 100 seconds
+
+ # Whether to log a warning if schema agreement fails.
+ # You might want to change this if you've set the timeout to 0.
+ #
+ # Required: yes
+ # Modifiable at runtime: yes, the new value will be used for checks issued after the change.
+ # Overridable in a profile: no
+ warn-on-failure = true
+ }
+ }
+
+ # Options related to the Netty event loop groups used internally by the driver.
+ advanced.netty {
+
+ # Whether the threads created by the driver should be daemon threads.
+ # This will apply to the threads in io-group, admin-group, and the timer thread.
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ daemon = false
+
+ # The event loop group used for I/O operations (reading and writing to Cassandra nodes).
+ # By default, threads in this group are named after the session name, "-io-" and an incrementing
+ # counter, for example "s0-io-0".
+ io-group {
+ # The number of threads.
+ # If this is set to 0, the driver will use `Runtime.getRuntime().availableProcessors() * 2`.
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ size = 4
+
+ # The options to shut down the event loop group gracefully when the driver closes. If a task
+ # gets submitted during the quiet period, it is accepted and the quiet period starts over.
+ # The timeout limits the overall shutdown time.
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ shutdown {quiet-period = 0, timeout = 0, unit = SECONDS}
+ }
+ # The event loop group used for admin tasks not related to request I/O (handle cluster events,
+ # refresh metadata, schedule reconnections, etc.)
+ # By default, threads in this group are named after the session name, "-admin-" and an
+ # incrementing counter, for example "s0-admin-0".
+ admin-group {
+ size = 2
+
+ shutdown {quiet-period = 0, timeout = 0, unit = SECONDS}
+ }
+ }
+
+ advanced.metadata {
+ # Topology events are external signals that inform the driver of the state of Cassandra nodes
+ # (by default, they correspond to gossip events received on the control connection).
+ # The debouncer helps smoothen out oscillations if conflicting events are sent out in short
+ # bursts.
+ # Debouncing may be disabled by setting the window to 0 or max-events to 1 (this is not
+ # recommended).
+ topology-event-debouncer {
+ # How long the driver waits to propagate an event. If another event is received within that
+ # time, the window is reset and a batch of accumulated events will be delivered.
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ window = 0 second
+
+ # The maximum number of events that can accumulate. If this count is reached, the events are
+ # delivered immediately and the time window is reset. This avoids holding events indefinitely
+ # if the window keeps getting reset.
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ max-events = 20
+ }
+
+ # Options relating to schema metadata (Cluster.getMetadata.getKeyspaces).
+ # This metadata is exposed by the driver for informational purposes, and is also necessary for
+ # token-aware routing.
+ schema {
+ # Whether schema metadata is enabled.
+ # If this is false, the schema will remain empty, or to the last known value.
+ #
+ # Required: yes
+ # Modifiable at runtime: yes, the new value will be used for refreshes issued after the
+ # change. It can also be overridden programmatically via Cluster.setSchemaMetadataEnabled.
+ # Overridable in a profile: no
+ enabled = true
+
+ # Protects against bursts of schema updates (for example when a client issues a sequence of
+ # DDL queries), by coalescing them into a single update.
+ # Debouncing may be disabled by setting the window to 0 or max-events to 1 (this is highly
+ # discouraged for schema refreshes).
+ debouncer {
+ # How long the driver waits to apply a refresh. If another refresh is requested within that
+ # time, the window is reset and a single refresh will be triggered when it ends.
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ window = 0 second
+
+ # The maximum number of refreshes that can accumulate. If this count is reached, a refresh
+ # is done immediately and the window is reset.
+ #
+ # Required: yes
+ # Modifiable at runtime: no
+ # Overridable in a profile: no
+ max-events = 20
+ }
+ }
+
+ # Whether token metadata (Cluster.getMetadata.getTokenMap) is enabled.
+ # This metadata is exposed by the driver for informational purposes, and is also necessary for
+ # token-aware routing.
+ # If this is false, it will remain empty, or to the last known value. Note that its computation
+ # requires information about the schema; therefore if schema metadata is disabled or filtered to
+ # a subset of keyspaces, the token map will be incomplete, regardless of the value of this
+ # property.
+ #
+ # Required: yes
+ # Modifiable at runtime: yes, the new value will be used for refreshes issued after the change.
+ # Overridable in a profile: no
+ token-map.enabled = true
+ }
+}
diff --git a/cassandra/util/src/main/resources/embedded-cassandra.yaml b/cassandra/util/src/main/resources/embedded-cassandra.yaml
deleted file mode 100644
index 2db7850c8..000000000
--- a/cassandra/util/src/main/resources/embedded-cassandra.yaml
+++ /dev/null
@@ -1,652 +0,0 @@
-# Cassandra storage config YAML
-
-# See http://wiki.apache.org/cassandra/StorageConfiguration for
-# full explanations of configuration directives
-# /NOTE
-
-# The name of the cluster. This is mainly used to prevent machines in
-# one logical cluster from joining another.
-cluster_name: 'Test Cluster'
-
-# This defines the number of tokens randomly assigned to this node on the ring
-# The more tokens, relative to other nodes, the larger the proportion of data
-# that this node will store. You probably want all nodes to have the same number
-# of tokens assuming they have equal hardware capability.
-#
-# If you leave this unspecified, Cassandra will use the default of 1 token for legacy compatibility,
-# and will use the initial_token as described below.
-#
-# Specifying initial_token will override this setting.
-#
-# If you already have a cluster with 1 token per node, and wish to migrate to
-# multiple tokens per node, see http://wiki.apache.org/cassandra/Operations
-# num_tokens: 256
-
-# If you haven't specified num_tokens, or have set it to the default of 1 then
-# you should always specify InitialToken when setting up a production
-# cluster for the first time, and often when adding capacity later.
-# The principle is that each node should be given an equal slice of
-# the token ring; see http://wiki.apache.org/cassandra/Operations
-# for more details.
-#
-# If blank, Cassandra will request a token bisecting the range of
-# the heaviest-loaded existing node. If there is no load information
-# available, such as is the case with a new cluster, it will pick
-# a random token, which will lead to hot spots.
-initial_token:
-
-# See http://wiki.apache.org/cassandra/HintedHandoff
-hinted_handoff_enabled: false
-
-# this defines the maximum amount of time a dead host will have hints
-# generated. After it has been dead this long, new hints for it will not be
-# created until it has been seen alive and gone down again.
-max_hint_window_in_ms: 10800000 # 3 hours
-# throttle in KBs per second, per delivery thread
-hinted_handoff_throttle_in_kb: 1024
-# Number of threads with which to deliver hints;
-# Consider increasing this number when you have multi-dc deployments, since
-# cross-dc handoff tends to be slower
-max_hints_delivery_threads: 2
-
-# The following setting populates the page cache on memtable flush and compaction
-# WARNING: Enable this setting only when the whole node's data fits in memory.
-# Defaults to: false
-# populate_io_cache_on_flush: false
-
-# Authentication backend, implementing IAuthenticator; used to identify users
-# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator,
-# PasswordAuthenticator}.
-#
-# - AllowAllAuthenticator performs no checks - set it to disable authentication.
-# - PasswordAuthenticator relies on username/password pairs to authenticate
-# users. It keeps usernames and hashed passwords in system_auth.credentials table.
-# Please increase system_auth keyspace replication factor if you use this authenticator.
-authenticator: org.apache.cassandra.auth.AllowAllAuthenticator
-
-# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions
-# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer,
-# CassandraAuthorizer}.
-#
-# - AllowAllAuthorizer allows any action to any user - set it to disable authorization.
-# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please
-# increase system_auth keyspace replication factor if you use this authorizer.
-authorizer: org.apache.cassandra.auth.AllowAllAuthorizer
-
-# Validity period for permissions cache (fetching permissions can be an
-# expensive operation depending on the authorizer, CassandraAuthorizer is
-# one example). Defaults to 2000, set to 0 to disable.
-# Will be disabled automatically for AllowAllAuthorizer.
-# permissions_validity_in_ms: 2000
-
-# The partitioner is responsible for distributing rows (by key) across
-# nodes in the cluster. Any IPartitioner may be used, including your
-# own as long as it is on the classpath. Out of the box, Cassandra
-# provides org.apache.cassandra.dht.{Murmur3Partitioner, RandomPartitioner
-# ByteOrderedPartitioner, OrderPreservingPartitioner (deprecated)}.
-#
-# - RandomPartitioner distributes rows across the cluster evenly by md5.
-# This is the default prior to 1.2 and is retained for compatibility.
-# - Murmur3Partitioner is similar to RandomPartioner but uses Murmur3_128
-# Hash Function instead of md5. When in doubt, this is the best option.
-# - ByteOrderedPartitioner orders rows lexically by key bytes. BOP allows
-# scanning rows in key order, but the ordering can generate hot spots
-# for sequential insertion workloads.
-# - OrderPreservingPartitioner is an obsolete form of BOP, that stores
-# - keys in a less-efficient format and only works with keys that are
-# UTF8-encoded Strings.
-# - CollatingOPP collates according to EN,US rules rather than lexical byte
-# ordering. Use this as an example if you need custom collation.
-#
-# See http://wiki.apache.org/cassandra/Operations for more on
-# partitioners and token selection.
-partitioner: org.apache.cassandra.dht.Murmur3Partitioner
-
-# Directories where Cassandra should store data on disk. Cassandra
-# will spread data evenly across them, subject to the granularity of
-# the configured compaction strategy.
-# If not set, the default directory is $CASSANDRA_HOME/data/data.
-# data_file_directories:
-# - /var/lib/cassandra/data
-
-# commit log. when running on magnetic HDD, this should be a
-# separate spindle than the data directories.
-# If not set, the default directory is $CASSANDRA_HOME/data/commitlog.
-# commitlog_directory: /var/lib/cassandra/commitlog
-
-# Enable / disable CDC functionality on a per-node basis. This modifies the logic used
-# for write path allocation rejection (standard: never reject. cdc: reject Mutation
-# containing a CDC-enabled table if at space limit in cdc_raw_directory).
-cdc_enabled: false
-
-# CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the
-# segment contains mutations for a CDC-enabled table. This should be placed on a
-# separate spindle than the data directories. If not set, the default directory is
-# $CASSANDRA_HOME/data/cdc_raw.
-cdc_raw_directory: target/embeddedCassandra/data/cdc_raw
-
-
-# Directories where Cassandra should store data on disk. Cassandra
-# will spread data evenly across them, subject to the granularity of
-# the configured compaction strategy.
-data_file_directories:
- - target/embeddedCassandra/data
-
-hints_directory:
- - target/embeddedCassandra/hints
-
-# commit log
-commitlog_directory: target/embeddedCassandra/commitlog
-
-# policy for data disk failures:
-# stop: shut down gossip and Thrift, leaving the node effectively dead, but
-# can still be inspected via JMX.
-# best_effort: stop using the failed disk and respond to requests based on
-# remaining available sstables. This means you WILL see obsolete
-# data at CL.ONE!
-# ignore: ignore fatal errors and let requests fail, as in pre-1.2 Cassandra
-disk_failure_policy: stop
-
-# Maximum size of the key cache in memory.
-#
-# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the
-# minimum, sometimes more. The key cache is fairly tiny for the amount of
-# time it saves, so it's worthwhile to use it at large numbers.
-# The row cache saves even more time, but must contain the entire row,
-# so it is extremely space-intensive. It's best to only use the
-# row cache if you have hot rows or static rows.
-#
-# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup.
-#
-# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache.
-key_cache_size_in_mb:
-
-# Duration in seconds after which Cassandra should
-# save the key cache. Caches are saved to saved_caches_directory as
-# specified in this configuration file.
-#
-# Saved caches greatly improve cold-start speeds, and is relatively cheap in
-# terms of I/O for the key cache. Row cache saving is much more expensive and
-# has limited use.
-#
-# Default is 14400 or 4 hours.
-key_cache_save_period: 14400
-
-# Number of keys from the key cache to save
-# Disabled by default, meaning all keys are going to be saved
-# key_cache_keys_to_save: 100
-
-# Maximum size of the row cache in memory.
-# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup.
-#
-# Default value is 0, to disable row caching.
-row_cache_size_in_mb: 0
-
-# Duration in seconds after which Cassandra should
-# safe the row cache. Caches are saved to saved_caches_directory as specified
-# in this configuration file.
-#
-# Saved caches greatly improve cold-start speeds, and is relatively cheap in
-# terms of I/O for the key cache. Row cache saving is much more expensive and
-# has limited use.
-#
-# Default is 0 to disable saving the row cache.
-row_cache_save_period: 0
-
-# Number of keys from the row cache to save
-# Disabled by default, meaning all keys are going to be saved
-# row_cache_keys_to_save: 100
-
-# saved caches
-saved_caches_directory: target/embeddedCassandra/saved_caches
-
-# commitlog_sync may be either "periodic" or "batch."
-# When in batch mode, Cassandra won't ack writes until the commit log
-# has been fsynced to disk. It will wait up to
-# commitlog_sync_batch_window_in_ms milliseconds for other writes, before
-# performing the sync.
-#
-# commitlog_sync: batch
-# commitlog_sync_batch_window_in_ms: 50
-#
-# the other option is "periodic" where writes may be acked immediately
-# and the CommitLog is simply synced every commitlog_sync_period_in_ms
-# milliseconds.
-commitlog_sync: periodic
-commitlog_sync_period_in_ms: 5000
-
-# The size of the individual commitlog file segments. A commitlog
-# segment may be archived, deleted, or recycled once all the data
-# in it (potentially from each columnfamily in the system) has been
-# flushed to sstables.
-#
-# The default size is 32, which is almost always fine, but if you are
-# archiving commitlog segments (see commitlog_archiving.properties),
-# then you probably want a finer granularity of archiving; 8 or 16 MB
-# is reasonable.
-commitlog_segment_size_in_mb: 8
-
-# any class that implements the SeedProvider interface and has a
-# constructor that takes a Map of parameters will do.
-seed_provider:
- # Addresses of hosts that are deemed contact points.
- # Cassandra nodes use this list of hosts to find each other and learn
- # the topology of the ring. You must change this if you are running
- # multiple nodes!
- - class_name: org.apache.cassandra.locator.SimpleSeedProvider
- parameters:
- # seeds is actually a comma-delimited list of addresses.
- # Ex: ",,"
- - seeds: "127.0.0.1"
-
-# For workloads with more data than can fit in memory, Cassandra's
-# bottleneck will be reads that need to fetch data from
-# disk. "concurrent_reads" should be set to (16 * number_of_drives) in
-# order to allow the operations to enqueue low enough in the stack
-# that the OS and drives can reorder them.
-#
-# On the other hand, since writes are almost never IO bound, the ideal
-# number of "concurrent_writes" is dependent on the number of cores in
-# your system; (8 * number_of_cores) is a good rule of thumb.
-concurrent_reads: 4
-concurrent_writes: 4
-
-# Total memory to use for memtables. Cassandra will flush the largest
-# memtable when this much memory is used.
-# If omitted, Cassandra will set it to 1/3 of the heap.
-# memtable_total_space_in_mb: 2048
-
-# Total space to use for commitlogs. Since commitlog segments are
-# mmapped, and hence use up address space, the default size is 32
-# on 32-bit JVMs, and 1024 on 64-bit JVMs.
-#
-# If space gets above this value (it will round up to the next nearest
-# segment multiple), Cassandra will flush every dirty CF in the oldest
-# segment and remove it. So a small total commitlog space will tend
-# to cause more flush activity on less-active columnfamilies.
-# commitlog_total_space_in_mb: 4096
-
-# This sets the amount of memtable flush writer threads. These will
-# be blocked by disk io, and each one will hold a memtable in memory
-# while blocked. If you have a large heap and many data directories,
-# you can increase this value for better flush performance.
-# By default this will be set to the amount of data directories defined.
-#memtable_flush_writers: 1
-
-# Whether to, when doing sequential writing, fsync() at intervals in
-# order to force the operating system to flush the dirty
-# buffers. Enable this to avoid sudden dirty buffer flushing from
-# impacting read latencies. Almost always a good idea on SSDs; not
-# necessarily on platters.
-trickle_fsync: false
-trickle_fsync_interval_in_kb: 10240
-
-# TCP port, for commands and data
-storage_port: 7000
-
-# SSL port, for encrypted communication. Unused unless enabled in
-# encryption_options
-ssl_storage_port: 7001
-
-# Address to bind to and tell other Cassandra nodes to connect to. You
-# _must_ change this if you want multiple nodes to be able to
-# communicate!
-#
-# Leaving it blank leaves it up to InetAddress.getLocalHost(). This
-# will always do the Right Thing _if_ the node is properly configured
-# (hostname, name resolution, etc), and the Right Thing is to use the
-# address associated with the hostname (it might not be).
-#
-# Setting this to 0.0.0.0 is always wrong.
-listen_address: localhost
-
-# Address to broadcast to other Cassandra nodes
-# Leaving this blank will set it to the same value as listen_address
-# broadcast_address: 1.2.3.4
-
-# Internode authentication backend, implementing IInternodeAuthenticator;
-# used to allow/disallow connections from peer nodes.
-# internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator
-
-# Whether to start the native transport server.
-# Please note that the address on which the native transport is bound is the
-# same as the rpc_address. The port however is different and specified below.
-start_native_transport: true
-# port for the CQL native transport to listen for clients on
-native_transport_port: 9042
-# The minimum and maximum threads for handling requests when the native
-# transport is used. They are similar to rpc_min_threads and rpc_max_threads,
-# though the defaults differ slightly.
-# native_transport_min_threads: 16
-#native_transport_max_threads: 48
-
-# Whether to start the thrift rpc server.
-start_rpc: false
-
-# The address to bind the Thrift RPC service to -- clients connect
-# here. Unlike ListenAddress above, you _can_ specify 0.0.0.0 here if
-# you want Thrift to listen on all interfaces.
-#
-# Leaving this blank has the same effect it does for ListenAddress,
-# (i.e. it will be based on the configured hostname of the node).
-rpc_address: localhost
-# port for Thrift to listen for clients on
-rpc_port: 9160
-
-# enable or disable keepalive on rpc connections
-rpc_keepalive: true
-
-# Cassandra provides three out-of-the-box options for the RPC Server:
-#
-# sync -> One thread per thrift connection. For a very large number of clients, memory
-# will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size
-# per thread, and that will correspond to your use of virtual memory (but physical memory
-# may be limited depending on use of stack space).
-#
-# hsha -> Stands for "half synchronous, half asynchronous." All thrift clients are handled
-# asynchronously using a small number of threads that does not vary with the amount
-# of thrift clients (and thus scales well to many clients). The rpc requests are still
-# synchronous (one thread per active request).
-#
-# The default is sync because on Windows hsha is about 30% slower. On Linux,
-# sync/hsha performance is about the same, with hsha of course using less memory.
-#
-# Alternatively, can provide your own RPC server by providing the fully-qualified class name
-# of an o.a.c.t.TServerFactory that can create an instance of it.
-rpc_server_type: sync
-
-# Uncomment rpc_min|max_thread to set request pool size limits.
-#
-# Regardless of your choice of RPC server (see above), the number of maximum requests in the
-# RPC thread pool dictates how many concurrent requests are possible (but if you are using the sync
-# RPC server, it also dictates the number of clients that can be connected at all).
-#
-# The default is unlimited and thus provides no protection against clients overwhelming the server. You are
-# encouraged to set a maximum that makes sense for you in production, but do keep in mind that
-# rpc_max_threads represents the maximum number of client requests this server may execute concurrently.
-#
-# rpc_min_threads: 16
-# rpc_max_threads: 2048
-
-# uncomment to set socket buffer sizes on rpc connections
-# rpc_send_buff_size_in_bytes:
-# rpc_recv_buff_size_in_bytes:
-
-# Uncomment to set socket buffer size for internode communication
-# Note that when setting this, the buffer size is limited by net.core.wmem_max
-# and when not setting it it is defined by net.ipv4.tcp_wmem
-# See:
-# /proc/sys/net/core/wmem_max
-# /proc/sys/net/core/rmem_max
-# /proc/sys/net/ipv4/tcp_wmem
-# /proc/sys/net/ipv4/tcp_wmem
-# and: man tcp
-# internode_send_buff_size_in_bytes:
-# internode_recv_buff_size_in_bytes:
-
-# Frame size for thrift (maximum field length).
-thrift_framed_transport_size_in_mb: 15
-
-# The max length of a thrift message, including all fields and
-# internal thrift overhead.
-thrift_max_message_length_in_mb: 16
-
-# Set to true to have Cassandra create a hard link to each sstable
-# flushed or streamed locally in a backups/ subdirectory of the
-# keyspace data. Removing these links is the operator's
-# responsibility.
-incremental_backups: false
-
-# Whether or not to take a snapshot before each compaction. Be
-# careful using this option, since Cassandra won't clean up the
-# snapshots for you. Mostly useful if you're paranoid when there
-# is a data format change.
-snapshot_before_compaction: false
-
-# Whether or not a snapshot is taken of the data before keyspace truncation
-# or dropping of column families. The STRONGLY advised default of true
-# should be used to provide data safety. If you set this flag to false, you will
-# lose data on truncation or drop.
-auto_snapshot: false
-
-# Add column indexes to a row after its contents reach this size.
-# Increase if your column values are large, or if you have a very large
-# number of columns. The competing causes are, Cassandra has to
-# deserialize this much of the row to read a single column, so you want
-# it to be small - at least if you do many partial-row reads - but all
-# the index data is read for each access, so you don't want to generate
-# that wastefully either.
-column_index_size_in_kb: 64
-
-# Number of simultaneous compactions to allow, NOT including
-# validation "compactions" for anti-entropy repair. Simultaneous
-# compactions can help preserve read performance in a mixed read/write
-# workload, by mitigating the tendency of small sstables to accumulate
-# during a single long running compactions. The default is usually
-# fine and if you experience problems with compaction running too
-# slowly or too fast, you should look at
-# compaction_throughput_mb_per_sec first.
-#
-# concurrent_compactors defaults to the number of cores.
-# Uncomment to make compaction mono-threaded, the pre-0.8 default.
-#concurrent_compactors: 1
-
-# Throttles compaction to the given total throughput across the entire
-# system. The faster you insert data, the faster you need to compact in
-# order to keep the sstable count down, but in general, setting this to
-# 16 to 32 times the rate you are inserting data is more than sufficient.
-# Setting this to 0 disables throttling. Note that this account for all types
-# of compaction, including validation compaction.
-compaction_throughput_mb_per_sec: 16
-
-# Throttles all outbound streaming file transfers on this node to the
-# given total throughput in Mbps. This is necessary because Cassandra does
-# mostly sequential IO when streaming data during bootstrap or repair, which
-# can lead to saturating the network connection and degrading rpc performance.
-# When unset, the default is 200 Mbps or 25 MB/s.
-# stream_throughput_outbound_megabits_per_sec: 200
-
-# How long the coordinator should wait for read operations to complete
-read_request_timeout_in_ms: 120000
-# How long the coordinator should wait for seq or index scans to complete
-range_request_timeout_in_ms: 120000
-# How long the coordinator should wait for writes to complete
-write_request_timeout_in_ms: 120000
-# How long the coordinator should wait for truncates to complete
-# (This can be much longer, because unless auto_snapshot is disabled
-# we need to flush first so we can snapshot before removing the data.)
-truncate_request_timeout_in_ms: 120000
-# The default timeout for other, miscellaneous operations
-request_timeout_in_ms: 120000
-
-# Enable operation timeout information exchange between nodes to accurately
-# measure request timeouts, If disabled cassandra will assuming the request
-# was forwarded to the replica instantly by the coordinator
-#
-# Warning: before enabling this property make sure to ntp is installed
-# and the times are synchronized between the nodes.
-cross_node_timeout: false
-
-# Enable socket timeout for streaming operation.
-# When a timeout occurs during streaming, streaming is retried from the start
-# of the current file. This _can_ involve re-streaming an important amount of
-# data, so you should avoid setting the value too low.
-# Default value is 0, which never timeout streams.
-# streaming_socket_timeout_in_ms: 0
-
-# phi value that must be reached for a host to be marked down.
-# most users should never need to adjust this.
-# phi_convict_threshold: 8
-
-# endpoint_snitch -- Set this to a class that implements
-# IEndpointSnitch. The snitch has two functions:
-# - it teaches Cassandra enough about your network topology to route
-# requests efficiently
-# - it allows Cassandra to spread replicas around your cluster to avoid
-# correlated failures. It does this by grouping machines into
-# "datacenters" and "racks." Cassandra will do its best not to have
-# more than one replica on the same "rack" (which may not actually
-# be a physical location)
-#
-# IF YOU CHANGE THE SNITCH AFTER DATA IS INSERTED INTO THE CLUSTER,
-# YOU MUST RUN A FULL REPAIR, SINCE THE SNITCH AFFECTS WHERE REPLICAS
-# ARE PLACED.
-#
-# Out of the box, Cassandra provides
-# - SimpleSnitch:
-# Treats Strategy order as proximity. This improves cache locality
-# when disabling read repair, which can further improve throughput.
-# Only appropriate for single-datacenter deployments.
-# - PropertyFileSnitch:
-# Proximity is determined by rack and data center, which are
-# explicitly configured in cassandra-topology.properties.
-# - GossipingPropertyFileSnitch
-# The rack and datacenter for the local node are defined in
-# cassandra-rackdc.properties and propagated to other nodes via gossip. If
-# cassandra-topology.properties exists, it is used as a fallback, allowing
-# migration from the PropertyFileSnitch.
-# - RackInferringSnitch:
-# Proximity is determined by rack and data center, which are
-# assumed to correspond to the 3rd and 2nd octet of each node's
-# IP address, respectively. Unless this happens to match your
-# deployment conventions (as it did Facebook's), this is best used
-# as an example of writing a custom Snitch class.
-# - Ec2Snitch:
-# Appropriate for EC2 deployments in a single Region. Loads Region
-# and Availability Zone information from the EC2 API. The Region is
-# treated as the datacenter, and the Availability Zone as the rack.
-# Only private IPs are used, so this will not work across multiple
-# Regions.
-# - Ec2MultiRegionSnitch:
-# Uses public IPs as broadcast_address to allow cross-region
-# connectivity. (Thus, you should set seed addresses to the public
-# IP as well.) You will need to open the storage_port or
-# ssl_storage_port on the public IP firewall. (For intra-Region
-# traffic, Cassandra will switch to the private IP after
-# establishing a connection.)
-#
-# You can use a custom Snitch by setting this to the full class name
-# of the snitch, which will be assumed to be on your classpath.
-endpoint_snitch: SimpleSnitch
-
-# controls how often to perform the more expensive part of host score
-# calculation
-dynamic_snitch_update_interval_in_ms: 100
-# controls how often to reset all host scores, allowing a bad host to
-# possibly recover
-dynamic_snitch_reset_interval_in_ms: 600000
-# if set greater than zero and read_repair_chance is < 1.0, this will allow
-# 'pinning' of replicas to hosts in order to increase cache capacity.
-# The badness threshold will control how much worse the pinned host has to be
-# before the dynamic snitch will prefer other replicas over it. This is
-# expressed as a double which represents a percentage. Thus, a value of
-# 0.2 means Cassandra would continue to prefer the static snitch values
-# until the pinned host was 20% worse than the fastest.
-dynamic_snitch_badness_threshold: 0.1
-
-# request_scheduler -- Set this to a class that implements
-# RequestScheduler, which will schedule incoming client requests
-# according to the specific policy. This is useful for multi-tenancy
-# with a single Cassandra cluster.
-# NOTE: This is specifically for requests from the client and does
-# not affect inter node communication.
-# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place
-# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of
-# client requests to a node with a separate queue for each
-# request_scheduler_id. The scheduler is further customized by
-# request_scheduler_options as described below.
-request_scheduler: org.apache.cassandra.scheduler.NoScheduler
-
-# Scheduler Options vary based on the type of scheduler
-# NoScheduler - Has no options
-# RoundRobin
-# - throttle_limit -- The throttle_limit is the number of in-flight
-# requests per client. Requests beyond
-# that limit are queued up until
-# running requests can complete.
-# The value of 80 here is twice the number of
-# concurrent_reads + concurrent_writes.
-# - default_weight -- default_weight is optional and allows for
-# overriding the default which is 1.
-# - weights -- Weights are optional and will default to 1 or the
-# overridden default_weight. The weight translates into how
-# many requests are handled during each turn of the
-# RoundRobin, based on the scheduler id.
-#
-# request_scheduler_options:
-# throttle_limit: 80
-# default_weight: 5
-# weights:
-# Keyspace1: 1
-# Keyspace2: 5
-
-# request_scheduler_id -- An identifier based on which to perform
-# the request scheduling. Currently the only valid option is keyspace.
-# request_scheduler_id: keyspace
-
-# index_interval controls the sampling of entries from the primrary
-# row index in terms of space versus time. The larger the interval,
-# the smaller and less effective the sampling will be. In technicial
-# terms, the interval coresponds to the number of index entries that
-# are skipped between taking each sample. All the sampled entries
-# must fit in memory. Generally, a value between 128 and 512 here
-# coupled with a large key cache size on CFs results in the best trade
-# offs. This value is not often changed, however if you have many
-# very small rows (many to an OS page), then increasing this will
-# often lower memory usage without a impact on performance.
-index_interval: 128
-
-# Enable or disable inter-node encryption
-# Default settings are TLS v1, RSA 1024-bit keys (it is imperative that
-# users generate their own keys) TLS_RSA_WITH_AES_128_CBC_SHA as the cipher
-# suite for authentication, key exchange and encryption of the actual data transfers.
-# NOTE: No custom encryption options are enabled at the moment
-# The available internode options are : all, none, dc, rack
-#
-# If set to dc cassandra will encrypt the traffic between the DCs
-# If set to rack cassandra will encrypt the traffic between the racks
-#
-# The passwords used in these options must match the passwords used when generating
-# the keystore and truststore. For instructions on generating these files, see:
-# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore
-#
-server_encryption_options:
- internode_encryption: none
- keystore: conf/.keystore
- keystore_password: cassandra
- truststore: conf/.truststore
- truststore_password: cassandra
- # More advanced defaults below:
- # protocol: TLS
- # algorithm: SunX509
- # store_type: JKS
- # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA]
- # require_client_auth: false
-
-# enable or disable client/server encryption.
-client_encryption_options:
- enabled: false
- keystore: conf/.keystore
- keystore_password: cassandra
- # require_client_auth: false
- # Set trustore and truststore_password if require_client_auth is true
- # truststore: conf/.truststore
- # truststore_password: cassandra
- # More advanced defaults below:
- # protocol: TLS
- # algorithm: SunX509
- # store_type: JKS
- # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA]
-
-# internode_compression controls whether traffic between nodes is
-# compressed.
-# can be: all - all traffic is compressed
-# dc - traffic between different datacenters is compressed
-# none - nothing is compressed.
-internode_compression: none
-
-# Enable or disable tcp_nodelay for inter-dc communication.
-# Disabling it will result in larger (but fewer) network packets being sent,
-# reducing overhead from the TCP protocol itself, at the cost of increasing
-# latency if you block for cross-datacenter responses.
-# inter_dc_tcp_nodelay: true
diff --git a/couchbase/example/README.md b/couchbase/example/README.md
index 3b7a2f7c1..f4e99a91d 100644
--- a/couchbase/example/README.md
+++ b/couchbase/example/README.md
@@ -4,4 +4,7 @@ This project contains samples of data access features with Spring Data (Couchbas
## Prerequisites
-The examples require a running [Couchbase Server](https://www.couchbase.com/downloads) server with the travel sample bucket imported. We assume you're running Couchbase 5 and we have `spring.couchbase.bucket.password=…` accordingly to adapt RBAC authentication.
+The examples require a running [Couchbase Server](https://www.couchbase.com/downloads) with the travel sample bucket imported. We assume you're running Couchbase 6.5 and we have updated the `application.properties` class accordingly to adapt RBAC authentication.
+
+For more information, see the [official documentation](https://docs.spring.io/spring-data/couchbase/docs/current/reference/html/#reference).
+
diff --git a/couchbase/example/pom.xml b/couchbase/example/pom.xml
index 7161616b0..de400e99e 100644
--- a/couchbase/example/pom.xml
+++ b/couchbase/example/pom.xml
@@ -5,12 +5,12 @@
spring-data-couchbase-examplesorg.springframework.data.examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOT../pom.xmlspring-data-couchbase-example
- Basic sample for Spring Data Couchbase
+ Spring Data Couchbase - Basic sampleSmall sample project showing the usage of Spring Data Couchbase.
@@ -22,6 +22,11 @@
test
+
+ jakarta.annotation
+ jakarta.annotation-api
+
+
diff --git a/couchbase/example/src/main/java/example/springdata/couchbase/model/Airline.java b/couchbase/example/src/main/java/example/springdata/couchbase/model/Airline.java
index 47f0434b6..533a8d46c 100644
--- a/couchbase/example/src/main/java/example/springdata/couchbase/model/Airline.java
+++ b/couchbase/example/src/main/java/example/springdata/couchbase/model/Airline.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,10 +17,11 @@
import lombok.Data;
+import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.core.mapping.Document;
+import org.springframework.data.couchbase.core.mapping.Field;
-import com.couchbase.client.java.repository.annotation.Field;
-import com.couchbase.client.java.repository.annotation.Id;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* A domain object representing an Airline
@@ -29,26 +30,18 @@
*/
@Data
@Document
+@JsonIgnoreProperties(ignoreUnknown = true)
public class Airline {
- @Id
- private String id;
+ private @Id String id;
- @Field
- private String type;
-
- @Field
private String name;
- @Field("iata")
- private String iataCode;
+ private String iata;
- @Field
private String icao;
- @Field
private String callsign;
- @Field
private String country;
}
diff --git a/couchbase/example/src/main/java/example/springdata/couchbase/repository/AirlineRepository.java b/couchbase/example/src/main/java/example/springdata/couchbase/repository/AirlineRepository.java
index c8312c18a..2c0d32208 100644
--- a/couchbase/example/src/main/java/example/springdata/couchbase/repository/AirlineRepository.java
+++ b/couchbase/example/src/main/java/example/springdata/couchbase/repository/AirlineRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,19 +19,16 @@
import java.util.List;
-import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed;
-import org.springframework.data.couchbase.core.query.View;
-import org.springframework.data.couchbase.core.query.ViewIndexed;
import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
/**
* Repository interface to manage {@link Airline} instances.
*
* @author Chandana Kithalagama
* @author Mark Paluch
+ * @author Denis Rosa
*/
-@N1qlPrimaryIndexed
-@ViewIndexed(designDoc = "airlines")
public interface AirlineRepository extends CrudRepository {
/**
@@ -40,13 +37,12 @@ public interface AirlineRepository extends CrudRepository {
* @param code
* @return
*/
- Airline findAirlineByIataCode(String code);
+ List findByIata(String code);
/**
* Query method using {@code airlines/all} view.
*
* @return
*/
- @View(designDocument = "airlines", viewName = "all")
List findAllBy();
}
diff --git a/couchbase/example/src/main/java/example/springdata/couchbase/repository/CouchbaseConfiguration.java b/couchbase/example/src/main/java/example/springdata/couchbase/repository/CouchbaseConfiguration.java
deleted file mode 100644
index 5d4780be5..000000000
--- a/couchbase/example/src/main/java/example/springdata/couchbase/repository/CouchbaseConfiguration.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.couchbase.repository;
-
-import example.springdata.couchbase.model.Airline;
-import lombok.RequiredArgsConstructor;
-
-import java.util.List;
-
-import javax.annotation.PostConstruct;
-
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.Bean;
-import org.springframework.data.couchbase.config.BeanNames;
-import org.springframework.data.couchbase.core.CouchbaseOperations;
-import org.springframework.data.couchbase.repository.support.IndexManager;
-
-import com.couchbase.client.java.query.N1qlQuery;
-
-/**
- * Simple configuration class.
- *
- * @author Chandana Kithalagama
- * @author Mark Paluch
- */
-@SpringBootApplication
-@RequiredArgsConstructor
-public class CouchbaseConfiguration {
-
- private final CouchbaseOperations couchbaseOperations;
-
- /**
- * Create an {@link IndexManager} that allows index creation.
- *
- * @return
- */
- @Bean(name = BeanNames.COUCHBASE_INDEX_MANAGER)
- public IndexManager indexManager() {
- return new IndexManager(true, true, false);
- }
-
- @PostConstruct
- private void postConstruct() {
-
- // Need to post-process travel data to add _class attribute
- List airlinesWithoutClassAttribute = couchbaseOperations.findByN1QL(N1qlQuery.simple( //
- "SELECT META(`travel-sample`).id AS _ID, META(`travel-sample`).cas AS _CAS, `travel-sample`.* " + //
- "FROM `travel-sample` " + //
- "WHERE type = \"airline\" AND _class IS MISSING;"),
- Airline.class);
-
- airlinesWithoutClassAttribute.forEach(couchbaseOperations::save);
- }
-}
diff --git a/couchbase/example/src/main/java/example/springdata/couchbase/repository/CouchbaseMain.java b/couchbase/example/src/main/java/example/springdata/couchbase/repository/CouchbaseMain.java
new file mode 100644
index 000000000..b2ef4ac2e
--- /dev/null
+++ b/couchbase/example/src/main/java/example/springdata/couchbase/repository/CouchbaseMain.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.couchbase.repository;
+
+import example.springdata.couchbase.model.Airline;
+import lombok.RequiredArgsConstructor;
+
+import jakarta.annotation.PostConstruct;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.couchbase.core.CouchbaseTemplate;
+
+import com.couchbase.client.java.Cluster;
+import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions;
+
+/**
+ * Main Class of this module
+ *
+ * @author Denis Rosa
+ */
+@SpringBootApplication
+@RequiredArgsConstructor
+public class CouchbaseMain {
+
+ @Autowired private final CouchbaseTemplate couchbaseTemplate;
+
+ @Autowired private Cluster cluster;
+
+ /**
+ * Add the _class field to all Airline documents
+ */
+ @PostConstruct
+ private void postConstruct() {
+ cluster.queryIndexes().createPrimaryIndex(couchbaseTemplate.getBucketName(),
+ CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true));
+
+ // Need to post-process travel data to add _class attribute
+ cluster.query("update `travel-sample` set _class='" + Airline.class.getName() + "' where type = 'airline'");
+ }
+}
diff --git a/couchbase/example/src/main/resources/application.properties b/couchbase/example/src/main/resources/application.properties
index 2d12d6197..f655413ba 100644
--- a/couchbase/example/src/main/resources/application.properties
+++ b/couchbase/example/src/main/resources/application.properties
@@ -1,9 +1,8 @@
-spring.couchbase.bucket.name=travel-sample
-spring.couchbase.bootstrap-hosts=localhost
-
-# Required for Couchbase 5
-spring.couchbase.bucket.password=password
-
# Increased timeout to fit slower environments like TravisCI
spring.couchbase.env.timeouts.view=15000
spring.couchbase.env.timeouts.query=15000
+spring.couchbase.connection-string=couchbase://127.0.0.1
+spring.couchbase.username=Administrator
+spring.couchbase.password=password
+spring.data.couchbase.bucket-name=travel-sample
+spring.data.couchbase.auto-index=true
diff --git a/couchbase/example/src/test/java/example/springdata/couchbase/repository/AirlineRepositoryIntegrationTests.java b/couchbase/example/src/test/java/example/springdata/couchbase/repository/AirlineRepositoryIntegrationTests.java
index c7f44878e..e5c630cb3 100644
--- a/couchbase/example/src/test/java/example/springdata/couchbase/repository/AirlineRepositoryIntegrationTests.java
+++ b/couchbase/example/src/test/java/example/springdata/couchbase/repository/AirlineRepositoryIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,40 +18,35 @@
import static org.assertj.core.api.Assertions.*;
import example.springdata.couchbase.model.Airline;
-import example.springdata.couchbase.util.CouchbaseAvailableRule;
+import example.springdata.couchbase.util.EnabledOnCouchbaseAvailable;
import java.util.List;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.couchbase.core.CouchbaseOperations;
-import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration tests showing basic CRUD operations through {@link AirlineRepository}.
*
- * @author Chandana Kithalagama
- * @author Mark Paluch
+ * @author Denis Rosa
*/
-@RunWith(SpringRunner.class)
@SpringBootTest
+@EnabledOnCouchbaseAvailable
public class AirlineRepositoryIntegrationTests {
- @ClassRule //
- public static CouchbaseAvailableRule COUCHBASE = CouchbaseAvailableRule.onLocalhost();
-
- @Autowired
- AirlineRepository airlineRepository;
+ @Autowired AirlineRepository airlineRepository;
@Autowired CouchbaseOperations couchbaseOperations;
- @Before
+ @BeforeEach
public void before() {
- airlineRepository.findById("LH").ifPresent(couchbaseOperations::remove);
+ if (couchbaseOperations.existsById().one("LH")) {
+ couchbaseOperations.removeById().one("LH");
+ }
}
/**
@@ -60,9 +55,8 @@ public void before() {
@Test
public void shouldFindAirlineN1ql() {
- Airline airline = airlineRepository.findAirlineByIataCode("TQ");
-
- assertThat(airline.getCallsign()).isEqualTo("TXW");
+ List airlines = airlineRepository.findByIata("TQ");
+ assertThat(airlines.get(0).getCallsign()).isEqualTo("TXW");
}
/**
@@ -73,10 +67,8 @@ public void shouldFindAirlineN1ql() {
@Test
public void shouldFindById() {
- Airline airline = airlineRepository.findAirlineByIataCode("TQ");
-
- assertThat(airlineRepository.findById(airline.getId())).contains(airline);
- assertThat(airlineRepository.findById("unknown")).isEmpty();
+ Airline airline = airlineRepository.findByIata("TQ").get(0);
+ assertThat(airlineRepository.findById(airline.getId()).isPresent());
}
/**
@@ -87,7 +79,7 @@ public void shouldFindByView() {
List airlines = airlineRepository.findAllBy();
- assertThat(airlines).hasSize(187);
+ assertThat(airlines).hasSizeGreaterThan(100);
}
/**
@@ -100,7 +92,7 @@ public void shouldCreateAirline() {
Airline airline = new Airline();
airline.setId("LH");
- airline.setIataCode("LH");
+ airline.setIata("LH");
airline.setIcao("DLH");
airline.setCallsign("Lufthansa");
airline.setName("Lufthansa");
diff --git a/couchbase/pom.xml b/couchbase/pom.xml
index cf8a01a57..07d77c31b 100644
--- a/couchbase/pom.xml
+++ b/couchbase/pom.xml
@@ -1,5 +1,6 @@
-
+4.0.0spring-data-couchbase-examples
@@ -8,7 +9,7 @@
org.springframework.data.examplesspring-data-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOTSpring Data Couchbase - Examples
@@ -18,13 +19,17 @@
examplereactive
+ transactionsutil
+
org.springframework.bootspring-boot-starter-data-couchbase
+
+
diff --git a/couchbase/reactive/README.md b/couchbase/reactive/README.md
index 299b26d77..d064773d8 100644
--- a/couchbase/reactive/README.md
+++ b/couchbase/reactive/README.md
@@ -1,51 +1,20 @@
-# Spring Data Couchbase 3.0 - Reactive examples
+# Spring Data Couchbase 4.0 - Reactive examples
This project contains samples of reactive data access features with Spring Data (Couchbase).
-## Reactive Template API usage with `RxJavaCouchbaseOperations`
-
-The main reactive Template API class is `RxJavaCouchbaseTemplate`, ideally used through its interface `RxJavaCouchbaseOperations`. It defines a basic set of reactive data access operations using [RxJava 1](https://github.com/ReactiveX/RxJava/tree/1.x) `Single` and `Observable` reactive types.
-
-```java
-Airline airline = new Airline();
-
-Observable single = operations.save(airline)
-
-Observable airlines = operations.findByView(ViewQuery.from("airlines", "all"), Airline.class);
-```
-
-The test cases in `RxJavaCouchbaseOperationsIntegrationTests` show basic Template API usage.
-Reactive data access reads and converts individual elements while processing the stream.
-
## Reactive Repository support
-Spring Data Couchbase provides reactive repository support with Project Reactor, RxJava 1 and RxJava 2 reactive types. The reactive API supports reactive type conversion between reactive types.
+Spring Data Couchbase provides reactive repository support with Project Reactor:
```java
-@N1qlPrimaryIndexed
-@ViewIndexed(designDoc = "airlines")
public interface ReactiveAirlineRepository extends ReactiveCrudRepository {
- Mono findAirlineByIataCode(String code);
+ Mono findByIata(String code);
- @View(designDocument = "airlines", viewName = "all")
Flux findAllBy();
}
```
-```java
-@N1qlPrimaryIndexed
-@ViewIndexed(designDoc = "airlines")
-public interface RxJava1AirlineRepository extends Repository {
-
- Single findAirlineByIataCode(String code);
-
- @View(designDocument = "airlines", viewName = "all")
- Observable findAllBy();
+For more information, see the [official documentation](https://docs.spring.io/spring-data/couchbase/docs/current/reference/html/#reference).
- Single findById(String id);
-
- Single save(Airline airline);
-}
-```
diff --git a/couchbase/reactive/pom.xml b/couchbase/reactive/pom.xml
index 1c488f0d9..e720d639c 100644
--- a/couchbase/reactive/pom.xml
+++ b/couchbase/reactive/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data.examplesspring-data-couchbase-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOTspring-data-couchbase-reactive
@@ -13,11 +13,6 @@
-
- org.springframework.boot
- spring-boot-starter-data-couchbase-reactive
-
-
io.projectreactorreactor-core
diff --git a/couchbase/reactive/src/main/java/example/springdata/couchbase/CouchbaseConfiguration.java b/couchbase/reactive/src/main/java/example/springdata/couchbase/CouchbaseConfiguration.java
deleted file mode 100644
index d730ad7a3..000000000
--- a/couchbase/reactive/src/main/java/example/springdata/couchbase/CouchbaseConfiguration.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.couchbase;
-
-import example.springdata.couchbase.model.Airline;
-import lombok.RequiredArgsConstructor;
-
-import java.util.List;
-
-import javax.annotation.PostConstruct;
-
-import org.springframework.beans.factory.ObjectProvider;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.Bean;
-import org.springframework.data.couchbase.config.BeanNames;
-import org.springframework.data.couchbase.core.CouchbaseOperations;
-import org.springframework.data.couchbase.repository.support.IndexManager;
-
-import com.couchbase.client.java.query.N1qlQuery;
-
-/**
- * Configuration class to configure reactive repositories.
- *
- * @author Mark Paluch
- */
-@SpringBootApplication
-@RequiredArgsConstructor
-public class CouchbaseConfiguration {
-
- private final ObjectProvider couchbaseOperationsProvider;
-
- /**
- * Create an {@link IndexManager} that allows index creation.
- *
- * @return
- */
- @Bean(name = BeanNames.COUCHBASE_INDEX_MANAGER)
- public IndexManager indexManager() {
- return new IndexManager(true, true, false);
- }
-
- @PostConstruct
- private void postConstruct() {
-
- // Need to post-process travel data to add _class attribute
-
- CouchbaseOperations couchbaseOperations = couchbaseOperationsProvider.getIfUnique();
- List airlinesWithoutClassAttribute = couchbaseOperations.findByN1QL(N1qlQuery.simple( //
- "SELECT META(`travel-sample`).id AS _ID, META(`travel-sample`).cas AS _CAS, `travel-sample`.* " + //
- "FROM `travel-sample` " + //
- "WHERE type = \"airline\" AND _class IS MISSING;"),
- Airline.class);
-
- airlinesWithoutClassAttribute.forEach(couchbaseOperations::save);
- }
-}
diff --git a/couchbase/reactive/src/main/java/example/springdata/couchbase/CouchbaseMain.java b/couchbase/reactive/src/main/java/example/springdata/couchbase/CouchbaseMain.java
new file mode 100644
index 000000000..236eb9d01
--- /dev/null
+++ b/couchbase/reactive/src/main/java/example/springdata/couchbase/CouchbaseMain.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.couchbase;
+
+import example.springdata.couchbase.model.Airline;
+import lombok.RequiredArgsConstructor;
+
+import jakarta.annotation.PostConstruct;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.couchbase.core.CouchbaseTemplate;
+
+import com.couchbase.client.java.Cluster;
+import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions;
+
+/**
+ * Main Class of the module
+ *
+ * @author Denis Rosa
+ */
+@SpringBootApplication
+@RequiredArgsConstructor
+public class CouchbaseMain {
+
+ @Autowired CouchbaseTemplate couchbaseTemplate;
+
+ @Autowired Cluster cluster;
+
+ @PostConstruct
+ private void postConstruct() {
+
+ cluster.queryIndexes().createPrimaryIndex(couchbaseTemplate.getBucketName(),
+ CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true));
+
+ // Need to post-process travel data to add _class attribute
+ cluster.query("update `travel-sample` set _class='" + Airline.class.getName() + "' where type = 'airline'");
+
+ }
+}
diff --git a/couchbase/reactive/src/main/java/example/springdata/couchbase/model/Airline.java b/couchbase/reactive/src/main/java/example/springdata/couchbase/model/Airline.java
index 8cad44ce0..8e72fc782 100644
--- a/couchbase/reactive/src/main/java/example/springdata/couchbase/model/Airline.java
+++ b/couchbase/reactive/src/main/java/example/springdata/couchbase/model/Airline.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,16 +17,15 @@
import lombok.Data;
+import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.core.mapping.Document;
-import com.couchbase.client.java.repository.annotation.Field;
-import com.couchbase.client.java.repository.annotation.Id;
-
/**
* A domain object representing an Airline
*
* @author Chandana Kithalagama
* @author Mark Paluch
+ * @author Denis Rosa
*/
@Data
@Document
@@ -34,15 +33,15 @@ public class Airline {
@Id private String id;
- @Field private String type;
+ private String type;
- @Field private String name;
+ private String name;
- @Field("iata") private String iataCode;
+ private String iata;
- @Field private String icao;
+ private String icao;
- @Field private String callsign;
+ private String callsign;
- @Field private String country;
+ private String country;
}
diff --git a/couchbase/reactive/src/main/java/example/springdata/couchbase/repository/ReactiveAirlineRepository.java b/couchbase/reactive/src/main/java/example/springdata/couchbase/repository/ReactiveAirlineRepository.java
index e8ba17a56..92a789cd6 100644
--- a/couchbase/reactive/src/main/java/example/springdata/couchbase/repository/ReactiveAirlineRepository.java
+++ b/couchbase/reactive/src/main/java/example/springdata/couchbase/repository/ReactiveAirlineRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,27 +19,24 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
-import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed;
import org.springframework.data.couchbase.core.query.View;
-import org.springframework.data.couchbase.core.query.ViewIndexed;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
/**
* Repository interface to manage {@link Airline} instances.
*
* @author Mark Paluch
+ * @author Denis Rosa
*/
-@N1qlPrimaryIndexed
-@ViewIndexed(designDoc = "airlines")
public interface ReactiveAirlineRepository extends ReactiveCrudRepository {
/**
- * Derived query selecting by {@code iataCode}.
+ * Derived query selecting by {@code iata}.
*
* @param code
* @return
*/
- Mono findAirlineByIataCode(String code);
+ Mono findByIata(String code);
/**
* Query method using {@code airlines/all} view.
diff --git a/couchbase/reactive/src/main/java/example/springdata/couchbase/repository/RxJava1AirlineRepository.java b/couchbase/reactive/src/main/java/example/springdata/couchbase/repository/RxJava1AirlineRepository.java
deleted file mode 100644
index b98f2fac6..000000000
--- a/couchbase/reactive/src/main/java/example/springdata/couchbase/repository/RxJava1AirlineRepository.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.couchbase.repository;
-
-import example.springdata.couchbase.model.Airline;
-import rx.Observable;
-import rx.Single;
-
-import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed;
-import org.springframework.data.couchbase.core.query.View;
-import org.springframework.data.couchbase.core.query.ViewIndexed;
-import org.springframework.data.repository.Repository;
-
-/**
- * Repository interface to manage {@link Airline} instances.
- *
- * @author Mark Paluch
- */
-@N1qlPrimaryIndexed
-@ViewIndexed(designDoc = "airlines")
-public interface RxJava1AirlineRepository extends Repository {
-
- /**
- * Derived query selecting by {@code iataCode}.
- *
- * @param code
- * @return
- */
- Single findAirlineByIataCode(String code);
-
- /**
- * Query method using {@code airlines/all} view.
- *
- * @return
- */
- @View(designDocument = "airlines", viewName = "all")
- Observable findAllBy();
-
- /**
- * Overloaded {@link org.springframework.data.repository.reactive.ReactiveCrudRepository#findById(Object)} method
- * returning a RxJava 1 {@link Single}.
- *
- * @param id
- * @return
- */
- Single findById(String id);
-
- /**
- * Overloaded {@link org.springframework.data.repository.reactive.ReactiveCrudRepository#save(Object)} method
- * returning a RxJava 1 {@link Single}.
- *
- * @param airline
- * @return
- */
- Single save(Airline airline);
-}
diff --git a/couchbase/reactive/src/main/resources/application.properties b/couchbase/reactive/src/main/resources/application.properties
index 2d12d6197..f655413ba 100644
--- a/couchbase/reactive/src/main/resources/application.properties
+++ b/couchbase/reactive/src/main/resources/application.properties
@@ -1,9 +1,8 @@
-spring.couchbase.bucket.name=travel-sample
-spring.couchbase.bootstrap-hosts=localhost
-
-# Required for Couchbase 5
-spring.couchbase.bucket.password=password
-
# Increased timeout to fit slower environments like TravisCI
spring.couchbase.env.timeouts.view=15000
spring.couchbase.env.timeouts.query=15000
+spring.couchbase.connection-string=couchbase://127.0.0.1
+spring.couchbase.username=Administrator
+spring.couchbase.password=password
+spring.data.couchbase.bucket-name=travel-sample
+spring.data.couchbase.auto-index=true
diff --git a/couchbase/reactive/src/test/java/example/springdata/couchbase/repository/ReactiveAirlineRepositoryIntegrationTests.java b/couchbase/reactive/src/test/java/example/springdata/couchbase/repository/ReactiveAirlineRepositoryIntegrationTests.java
index 2e61c6244..203ee6a56 100644
--- a/couchbase/reactive/src/test/java/example/springdata/couchbase/repository/ReactiveAirlineRepositoryIntegrationTests.java
+++ b/couchbase/reactive/src/test/java/example/springdata/couchbase/repository/ReactiveAirlineRepositoryIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,42 +18,34 @@
import static org.assertj.core.api.Assertions.*;
import example.springdata.couchbase.model.Airline;
-import example.springdata.couchbase.util.CouchbaseAvailableRule;
+import example.springdata.couchbase.util.EnabledOnCouchbaseAvailable;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.couchbase.core.CouchbaseOperations;
-import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration tests showing basic CRUD operations through {@link ReactiveAirlineRepository}
*
* @author Mark Paluch
*/
-@RunWith(SpringRunner.class)
@SpringBootTest
+@EnabledOnCouchbaseAvailable
public class ReactiveAirlineRepositoryIntegrationTests {
- @ClassRule //
- public static CouchbaseAvailableRule COUCHBASE = CouchbaseAvailableRule.onLocalhost();
-
@Autowired ReactiveAirlineRepository airlineRepository;
@Autowired CouchbaseOperations couchbaseOperations;
- @Before
+ @BeforeEach
public void before() {
-
- Airline toDelete = couchbaseOperations.findById("LH", Airline.class);
-
- if (toDelete != null) {
- couchbaseOperations.remove(toDelete);
+ if (couchbaseOperations.existsById().one("LH")) {
+ couchbaseOperations.removeById().one("LH");
}
}
@@ -63,10 +55,11 @@ public void before() {
@Test
public void shouldFindAirlineN1ql() {
- StepVerifier.create(airlineRepository.findAirlineByIataCode("TQ")).assertNext(it -> {
-
- assertThat(it.getCallsign()).isEqualTo("TXW");
- }).verifyComplete();
+ airlineRepository.findByIata("TQ") //
+ .as(StepVerifier::create) //
+ .assertNext(it -> {
+ assertThat(it.getCallsign()).isEqualTo("TXW");
+ }).verifyComplete();
}
/**
@@ -77,24 +70,29 @@ public void shouldFindAirlineN1ql() {
@Test
public void shouldFindById() {
- Mono airline = airlineRepository.findAirlineByIataCode("TQ") //
+ Mono airline = airlineRepository.findByIata("TQ") //
.map(Airline::getId) //
.flatMap(airlineRepository::findById);
- StepVerifier.create(airline).assertNext(it -> {
+ airline.as(StepVerifier::create) //
+ .assertNext(it -> {
- assertThat(it.getCallsign()).isEqualTo("TXW");
- }).verifyComplete();
+ assertThat(it.getCallsign()).isEqualTo("TXW");
+ }).verifyComplete();
- StepVerifier.create(airlineRepository.findById("unknown")).verifyComplete();
}
/**
* Find all {@link Airline}s applying the {@code airlines/all} view.
*/
@Test
- public void shouldFindByView() {
- StepVerifier.create(airlineRepository.findAllBy()).expectNextCount(187).verifyComplete();
+ public void shouldFindAll() {
+ airlineRepository.findAllBy().count() //
+ .as(StepVerifier::create) //
+ .assertNext(count -> {
+
+ assertThat(count).isGreaterThan(100);
+ }).verifyComplete();
}
/**
@@ -107,7 +105,7 @@ public void shouldCreateAirline() {
Airline airline = new Airline();
airline.setId("LH");
- airline.setIataCode("LH");
+ airline.setIata("LH");
airline.setIcao("DLH");
airline.setCallsign("Lufthansa");
airline.setName("Lufthansa");
@@ -117,6 +115,8 @@ public void shouldCreateAirline() {
.map(Airline::getId) //
.flatMap(airlineRepository::findById);
- StepVerifier.create(airlineMono).expectNext(airline).verifyComplete();
+ airlineMono.as(StepVerifier::create) //
+ .expectNext(airline) //
+ .verifyComplete();
}
}
diff --git a/couchbase/reactive/src/test/java/example/springdata/couchbase/repository/RxJava1AirlineRepositoryIntegrationTests.java b/couchbase/reactive/src/test/java/example/springdata/couchbase/repository/RxJava1AirlineRepositoryIntegrationTests.java
deleted file mode 100644
index af3991aeb..000000000
--- a/couchbase/reactive/src/test/java/example/springdata/couchbase/repository/RxJava1AirlineRepositoryIntegrationTests.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.couchbase.repository;
-
-import static org.assertj.core.api.Assertions.*;
-
-import example.springdata.couchbase.model.Airline;
-import example.springdata.couchbase.util.CouchbaseAvailableRule;
-import rx.Single;
-import rx.observers.AssertableSubscriber;
-
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.data.couchbase.core.CouchbaseOperations;
-import org.springframework.test.context.junit4.SpringRunner;
-
-/**
- * Integration tests showing basic CRUD operations through {@link RxJava1AirlineRepository}
- *
- * @author Mark Paluch
- */
-@RunWith(SpringRunner.class)
-@SpringBootTest
-public class RxJava1AirlineRepositoryIntegrationTests {
-
- @ClassRule //
- public static CouchbaseAvailableRule COUCHBASE = CouchbaseAvailableRule.onLocalhost();
-
- @Autowired RxJava1AirlineRepository airlineRepository;
-
- @Autowired CouchbaseOperations couchbaseOperations;
-
- @Before
- public void before() {
-
- Airline toDelete = couchbaseOperations.findById("LH", Airline.class);
-
- if (toDelete != null) {
- couchbaseOperations.remove(toDelete);
- }
- }
-
- /**
- * The derived query executes a N1QL query emitting a single element.
- */
- @Test
- public void shouldFindAirlineN1ql() {
-
- AssertableSubscriber subscriber = airlineRepository.findAirlineByIataCode("TQ") //
- .test() //
- .awaitTerminalEvent() //
- .assertCompleted();
-
- assertThat(subscriber.getValueCount()).isEqualTo(1);
- assertThat(subscriber.getOnNextEvents().get(0).getCallsign()).isEqualTo("TXW");
- }
-
- /**
- * The derived query executes a N1QL query and the emitted element is used to invoke
- * {@link org.springframework.data.repository.reactive.ReactiveCrudRepository#findById(Object)} for an Id-based
- * lookup. Queries without a result do not emit a value.
- */
- @Test
- public void shouldFindById() {
-
- Single airline = airlineRepository.findAirlineByIataCode("TQ") //
- .map(Airline::getId) //
- .flatMap(airlineRepository::findById);
-
- AssertableSubscriber subscriber = airline.test().awaitTerminalEvent().assertCompleted();
-
- assertThat(subscriber.getValueCount()).isEqualTo(1);
- assertThat(subscriber.getOnNextEvents().get(0).getCallsign()).isEqualTo("TXW");
-
- airlineRepository.findById("unknown").test().awaitTerminalEvent().assertNoValues();
- }
-
- /**
- * Find all {@link Airline}s applying the {@code airlines/all} view.
- */
- @Test
- public void shouldFindByView() {
- airlineRepository.findAllBy().test().awaitTerminalEvent().assertValueCount(187);
- }
-
- /**
- * Created elements are emitted by the
- * {@link org.springframework.data.repository.reactive.ReactiveCrudRepository#save(Object)} method.
- */
- @Test
- public void shouldCreateAirline() {
-
- Airline airline = new Airline();
-
- airline.setId("LH");
- airline.setIataCode("LH");
- airline.setIcao("DLH");
- airline.setCallsign("Lufthansa");
- airline.setName("Lufthansa");
- airline.setCountry("Germany");
-
- Single single = airlineRepository.save(airline) //
- .map(Airline::getId) //
- .flatMap(airlineRepository::findById);
-
- single.test().awaitTerminalEvent().assertResult(airline);
- }
-}
diff --git a/couchbase/reactive/src/test/java/example/springdata/couchbase/template/ReactiveJavaCouchbaseOperationsIntegrationTests.java b/couchbase/reactive/src/test/java/example/springdata/couchbase/template/ReactiveJavaCouchbaseOperationsIntegrationTests.java
new file mode 100644
index 000000000..38accec4f
--- /dev/null
+++ b/couchbase/reactive/src/test/java/example/springdata/couchbase/template/ReactiveJavaCouchbaseOperationsIntegrationTests.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.couchbase.template;
+
+import static org.assertj.core.api.Assertions.*;
+
+import example.springdata.couchbase.model.Airline;
+import example.springdata.couchbase.util.EnabledOnCouchbaseAvailable;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.couchbase.core.CouchbaseOperations;
+import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
+
+/**
+ * Integration tests showing basic CRUD operations through
+ * {@link org.springframework.data.couchbase.core.ReactiveCouchbaseOperations}.
+ *
+ * @author Mark Paluch
+ * @author Denis Rosa
+ */
+@SpringBootTest
+@EnabledOnCouchbaseAvailable
+public class ReactiveJavaCouchbaseOperationsIntegrationTests {
+
+ @Autowired ReactiveCouchbaseOperations operations;
+
+ @Autowired CouchbaseOperations couchbaseOperations;
+
+ @BeforeEach
+ public void before() {
+ if (couchbaseOperations.existsById().one("LH")) {
+ couchbaseOperations.removeById().one("LH");
+ }
+ }
+
+ /**
+ * Find all {@link Airline}s applying the _class filter .
+ */
+ @Test
+ public void shouldFindByAll() {
+ operations.findByQuery(Airline.class).all() //
+ .count() //
+ .as(StepVerifier::create) //
+ .assertNext(count -> {
+
+ assertThat(count).isGreaterThan(100);
+ }) //
+ .verifyComplete();
+ }
+
+ /**
+ * Created elements are emitted by {@link ReactiveCouchbaseOperations#upsertById(Class)} )}.
+ */
+ @Test
+ public void shouldCreateAirline() {
+ Airline airline = new Airline();
+
+ airline.setId("LH");
+ airline.setIata("LH");
+ airline.setIcao("DLH");
+ airline.setCallsign("Lufthansa");
+ airline.setName("Lufthansa");
+ airline.setCountry("Germany");
+
+ Mono airlineMono = operations.upsertById(Airline.class).one(airline) //
+ .map(Airline::getId) //
+ .flatMap(id -> operations.findById(Airline.class).one(id));
+
+ airlineMono.as(StepVerifier::create) //
+ .expectNext(airline).verifyComplete();
+ }
+}
diff --git a/couchbase/reactive/src/test/java/example/springdata/couchbase/template/RxJavaCouchbaseOperationsIntegrationTests.java b/couchbase/reactive/src/test/java/example/springdata/couchbase/template/RxJavaCouchbaseOperationsIntegrationTests.java
deleted file mode 100644
index bdb9ed6cb..000000000
--- a/couchbase/reactive/src/test/java/example/springdata/couchbase/template/RxJavaCouchbaseOperationsIntegrationTests.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.couchbase.template;
-
-import static org.assertj.core.api.Assertions.*;
-
-import example.springdata.couchbase.model.Airline;
-import example.springdata.couchbase.util.CouchbaseAvailableRule;
-import rx.Observable;
-import rx.observers.AssertableSubscriber;
-
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations;
-import org.springframework.test.context.junit4.SpringRunner;
-
-import com.couchbase.client.java.query.N1qlQuery;
-import com.couchbase.client.java.view.ViewQuery;
-
-/**
- * Integration tests showing basic CRUD operations through
- * {@link org.springframework.data.couchbase.core.RxJavaCouchbaseOperations}.
- *
- * @author Mark Paluch
- */
-@RunWith(SpringRunner.class)
-@SpringBootTest
-public class RxJavaCouchbaseOperationsIntegrationTests {
-
- @ClassRule //
- public static CouchbaseAvailableRule COUCHBASE = CouchbaseAvailableRule.onLocalhost();
-
- @Autowired RxJavaCouchbaseOperations operations;
-
- @Before
- public void before() {
- operations.findById("LH", Airline.class).flatMap(operations::remove).test().awaitTerminalEvent();
- }
-
- /**
- * The derived query executes a N1QL query emitting a single element.
- */
- @Test
- public void shouldFindAirlineN1ql() {
-
- String n1ql = "SELECT META(`travel-sample`).id AS _ID, META(`travel-sample`).cas AS _CAS, `travel-sample`.* " + //
- "FROM `travel-sample` " + //
- "WHERE (`iata` = \"TQ\") AND `_class` = \"example.springdata.couchbase.model.Airline\"";
-
- AssertableSubscriber subscriber = operations.findByN1QL(N1qlQuery.simple(n1ql), Airline.class) //
- .test() //
- .awaitTerminalEvent() //
- .assertCompleted();
-
- assertThat(subscriber.getOnNextEvents()).hasSize(1);
- assertThat(subscriber.getOnNextEvents().get(0).getCallsign()).isEqualTo("TXW");
- }
-
- /**
- * Find all {@link Airline}s applying the {@code airlines/all} view.
- */
- @Test
- public void shouldFindByView() {
-
- Observable airlines = operations.findByView(ViewQuery.from("airlines", "all"), Airline.class);
-
- airlines.test().awaitTerminalEvent().assertValueCount(187);
- }
-
- /**
- * Created elements are emitted by {@link RxJavaCouchbaseOperations#save(Object)}.
- */
- @Test
- public void shouldCreateAirline() {
-
- Airline airline = new Airline();
-
- airline.setId("LH");
- airline.setIataCode("LH");
- airline.setIcao("DLH");
- airline.setCallsign("Lufthansa");
- airline.setName("Lufthansa");
- airline.setCountry("Germany");
-
- Observable single = operations.save(airline) //
- .map(Airline::getId) //
- .flatMap(id -> operations.findById(id, Airline.class));
-
- single.test().awaitTerminalEvent().assertResult(airline);
- }
-}
diff --git a/couchbase/transactions/README.md b/couchbase/transactions/README.md
new file mode 100644
index 000000000..f77ad7d37
--- /dev/null
+++ b/couchbase/transactions/README.md
@@ -0,0 +1,17 @@
+# Getting Started
+
+### Running the Sample
+
+* `$> docker run -d --name db -p 8091-8097:8091-8097 -p 11210:11210 -p 11207:11207 -p 18091-18095:18091-18095 -p 18096:18096 -p 18097:18097 couchbase`
+* Create new _cluster_ via `http://localhost:8091/`
+* Create new [bucket](http://localhost:8091/ui/index.html#/buckets) named _travel-sample_.
+*
+
+### Reference Documentation
+For further reference, please consider the following sections:
+
+* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
+* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.5.2/maven-plugin/reference/html/)
+* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.5.2/maven-plugin/reference/html/#build-image)
+* [Spring Data Couchbase](https://docs.spring.io/spring-boot/docs/2.5.2/reference/htmlsingle/#boot-features-couchbase)
+
diff --git a/couchbase/transactions/pom.xml b/couchbase/transactions/pom.xml
new file mode 100644
index 000000000..967563c46
--- /dev/null
+++ b/couchbase/transactions/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+ spring-data-couchbase-transactions
+ Spring Data Couchbase - Transaction example
+ Transactions Demo project for Spring Data Couchbase
+
+
+ org.springframework.data.examples
+ spring-data-couchbase-examples
+ 4.0.0-SNAPSHOT
+
+
+
+
+
+ ${project.groupId}
+ spring-data-couchbase-example-utils
+ ${project.version}
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/couchbase/transactions/src/main/java/com/example/demo/AirlineGates.java b/couchbase/transactions/src/main/java/com/example/demo/AirlineGates.java
new file mode 100644
index 000000000..b91b2d887
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/AirlineGates.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012-2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Version;
+import org.springframework.data.couchbase.core.index.QueryIndexed;
+import org.springframework.data.couchbase.core.mapping.Document;
+
+/**
+ * @author Michael Reiche
+ */
+@Document
+public class AirlineGates {
+ @Id String id;
+ @Version Long version;
+ @QueryIndexed String name;
+ String iata;
+ Long gates;
+
+ public AirlineGates(String id, String name, String iata, Long gates) {
+ this.id = id;
+ this.name = name;
+ this.iata = iata;
+ this.gates = gates;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Long getGates() {
+ return gates;
+ }
+
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("{");
+ sb.append("\"id\":" + id);
+ sb.append(", \"name\":" + name);
+ sb.append(", \"iata\":" + iata);
+ sb.append(", \"gates\":" + gates);
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesRepository.java b/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesRepository.java
new file mode 100644
index 000000000..a16027042
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import org.springframework.data.couchbase.repository.CouchbaseRepository;
+import org.springframework.data.couchbase.repository.DynamicProxyable;
+
+/**
+ * @author Michael Reiche
+ */
+public interface AirlineGatesRepository
+ extends CouchbaseRepository, DynamicProxyable {
+
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesService.java b/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesService.java
new file mode 100644
index 000000000..f1196a91c
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/AirlineGatesService.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.data.couchbase.core.CouchbaseTemplate;
+import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @author Michael Reiche
+ */
+@Service
+public class AirlineGatesService {
+ CouchbaseTemplate template;
+ ReactiveCouchbaseTemplate reactiveTemplate;
+ AirlineGatesRepository airlineGatesRepository;
+
+ public AirlineGatesService(CouchbaseTemplate template, AirlineGatesRepository airlineGatesRepository) {
+ this.template = template;
+ this.reactiveTemplate = template.reactive();
+ this.airlineGatesRepository = airlineGatesRepository;
+ }
+
+ @Transactional
+ public void transferGates(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) {
+ AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId);
+ AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId);
+ toAirlineGates.gates += gatesToTransfer;
+ fromAirlineGates.gates -= gatesToTransfer;
+ template.save(fromAirlineGates);
+ if (exceptionToThrow != null) {
+ throw exceptionToThrow;
+ }
+ template.save(toAirlineGates);
+ }
+
+ @Transactional
+ public void transferGatesRepo(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) {
+ AirlineGates fromAirlineGates = airlineGatesRepository.findById(fromId).orElse(null);
+ AirlineGates toAirlineGates = airlineGatesRepository.findById(toId).orElse(null);
+ toAirlineGates.gates += gatesToTransfer;
+ fromAirlineGates.gates -= gatesToTransfer;
+ airlineGatesRepository.save(fromAirlineGates);
+ if (exceptionToThrow != null) {
+ throw exceptionToThrow;
+ }
+ airlineGatesRepository.save(toAirlineGates);
+ }
+
+ // The @Transactional annotation results in the method of the proxy for the service executing this in a transaction
+ @Transactional
+ public Mono transferGatesReactive(String fromId, String toId, int gatesToTransfer,
+ RuntimeException exceptionToThrow) {
+ return Mono.deferContextual(ctx -> {
+ AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId);
+ AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId);
+ toAirlineGates.gates += gatesToTransfer;
+ fromAirlineGates.gates -= gatesToTransfer;
+ template.save(fromAirlineGates);
+ if (exceptionToThrow != null) {
+ throw exceptionToThrow;
+ }
+ return reactiveTemplate.save(toAirlineGates).then();
+ });
+ }
+
+ // This does not have the @Transactional annotation therefore is not executed in a transaction
+ public AirlineGates save(AirlineGates airlineGates) {
+ return template.save(airlineGates);
+ }
+
+ // This does not have the @Transactional annotation therefore is not executed in a transaction
+ public AirlineGates findById(String id) {
+ return template.findById(AirlineGates.class).one(id);
+ }
+
+ // This does not have the @Transactional annotation therefore is not executed in a transaction
+ public AirlineGates saveRepo(AirlineGates airlineGates) {
+ return airlineGatesRepository.save(airlineGates);
+ }
+
+ // This does not have the @Transactional annotation therefore is not executed in a transaction
+ public AirlineGates findByIdRepo(String id) {
+ return airlineGatesRepository.findById(id).orElse(null);
+ }
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/CmdRunner.java b/couchbase/transactions/src/main/java/com/example/demo/CmdRunner.java
new file mode 100644
index 000000000..63462cebf
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/CmdRunner.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.data.couchbase.core.CouchbaseTemplate;
+import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+/**
+ * Components of the type CommandLineRunner are called right after the application start up. So the method *run* is
+ * called as soon as the application starts.
+ *
+ * @author Michael Reiche
+ */
+@Component
+public class CmdRunner implements CommandLineRunner {
+
+ @Autowired CouchbaseTemplate template;
+ @Autowired AirlineGatesService airlineGatesService;
+
+ // @Override
+ public void run(String... strings) {
+
+ try { // remove leftovers from previous run
+ template.removeById(AirlineGates.class).one("1");
+ } catch (Exception e) {}
+ try {
+ template.removeById(AirlineGates.class).one("2");
+ } catch (Exception e) {}
+
+ AirlineGates airlineGates1 = new AirlineGates("1", "American Airlines", "JFK", Long.valueOf(200)); // 1
+ AirlineGates airlineGates2 = new AirlineGates("2", "Lufthansa", "JFK", Long.valueOf(200));
+ AirlineGates saved1 = airlineGatesService.save(airlineGates1);
+ AirlineGates saved2 = airlineGatesService.save(airlineGates2);
+ AirlineGates found_a_1 = airlineGatesService.findById(saved1.getId()); // 2
+ AirlineGates found_a_2 = airlineGatesService.findById(saved2.getId());
+ System.err.println("initialized airlines");
+ System.err.println(" found before transferGates: " + found_a_1);
+ System.err.println(" found before transferGates: " + found_a_2);
+ // move 50 gates from airline1 to airline2
+ int gatesToTransfer = 50;
+ System.err.println("===============================================================");
+ System.err.println("this transferGates attempt will succeed. transferring " + gatesToTransfer);
+
+ airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), gatesToTransfer, null); // 3
+
+ AirlineGates found_b_1 = airlineGatesService.findById(airlineGates1.getId());
+ AirlineGates found_b_2 = airlineGatesService.findById(airlineGates2.getId());
+ System.err.println(" found after transferGates: " + found_b_1); // 4
+ System.err.println(" found after transferGates: " + found_b_2);
+ Assert.isTrue(found_b_1.getGates().equals(found_a_1.getGates() - gatesToTransfer), "should have transferred");
+ Assert.isTrue(found_b_2.getGates().equals(found_a_1.getGates() + gatesToTransfer), "should have transferred");
+ System.err.println("===============================================================");
+ gatesToTransfer = 44;
+ System.err.println("this transferGates attempt will fail. transferring " + gatesToTransfer);
+ // attempt to move 44 gates from airline1 to airline2, but it fails.
+ try {
+
+ airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), 44, new SimulateErrorException());
+
+ } catch (RuntimeException rte) {
+ if (!(rte instanceof TransactionSystemUnambiguousException) && rte != null
+ && rte.getCause() instanceof SimulateErrorException) {
+ throw rte;
+ }
+ System.err.println(" got exception " + rte);
+ }
+ AirlineGates found_c_1 = airlineGatesService.findById(airlineGates1.getId());
+ AirlineGates found_c_2 = airlineGatesService.findById(airlineGates2.getId());
+ System.err.println(" found after transferGates: " + found_c_1);
+ System.err.println(" found after transferGates: " + found_c_2);
+ Assert.isTrue(found_c_1.getGates().equals(found_b_1.getGates()), "should be same as previous");
+ Assert.isTrue(found_c_2.getGates().equals(found_b_2.getGates()), "should be same as previous");
+ System.err.println("===============================================================");
+ gatesToTransfer = 44;
+ System.err.println("this transferGates attempt will succeed. transferring " + gatesToTransfer);
+ try {
+
+ airlineGatesService.transferGatesReactive(airlineGates1.getId(), airlineGates2.getId(), gatesToTransfer, null)
+ .block();
+
+ } catch (RuntimeException rte) {
+ if (!(rte instanceof TransactionSystemUnambiguousException) && rte != null
+ && rte.getCause() instanceof SimulateErrorException) {
+ throw rte;
+ }
+ System.err.println(" got exception " + rte);
+ }
+ AirlineGates found_d_1 = airlineGatesService.findById(airlineGates1.getId());
+ AirlineGates found_d_2 = airlineGatesService.findById(airlineGates2.getId());
+ System.err.println(" found after transferGates: " + found_d_1);
+ System.err.println(" found after transferGates: " + found_d_2);
+ Assert.isTrue(found_d_1.getGates().equals(found_c_1.getGates() - gatesToTransfer), "should have transferred");
+ Assert.isTrue(found_d_2.getGates().equals(found_c_2.getGates() + gatesToTransfer), "should have transferred");
+ System.err.println("===============================================================");
+ }
+
+ static class SimulateErrorException extends RuntimeException {}
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/Config.java b/couchbase/transactions/src/main/java/com/example/demo/Config.java
new file mode 100644
index 000000000..8f8685a73
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/Config.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
+import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import com.couchbase.client.core.msg.kv.DurabilityLevel;
+import com.couchbase.client.java.env.ClusterEnvironment;
+import com.couchbase.client.java.transactions.config.TransactionsConfig;
+
+/**
+ * @author Michael Reiche
+ */
+@Configuration
+@EnableCouchbaseRepositories({ "com.example.demo" })
+@EnableTransactionManagement
+public class Config extends AbstractCouchbaseConfiguration {
+ @Override
+ public String getConnectionString() {
+ return "127.0.0.1";
+ }
+
+ @Override
+ public String getUserName() {
+ return "Administrator";
+ }
+
+ @Override
+ public String getPassword() {
+ return "password";
+ }
+
+ @Override
+ public String getBucketName() {
+ return "travel-sample";
+ }
+
+ @Override
+ public void configureEnvironment(ClusterEnvironment.Builder builder) {
+ builder.transactionsConfig(TransactionsConfig.durabilityLevel(DurabilityLevel.NONE));
+ }
+
+}
diff --git a/couchbase/transactions/src/main/java/com/example/demo/DemoApplication.java b/couchbase/transactions/src/main/java/com/example/demo/DemoApplication.java
new file mode 100644
index 000000000..95e8ce5d9
--- /dev/null
+++ b/couchbase/transactions/src/main/java/com/example/demo/DemoApplication.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Michael Reiche
+ */
+@SpringBootApplication
+public class DemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(DemoApplication.class, args);
+ }
+
+}
diff --git a/couchbase/transactions/src/main/resources/logback.xml b/couchbase/transactions/src/main/resources/logback.xml
new file mode 100644
index 000000000..a0a452dc7
--- /dev/null
+++ b/couchbase/transactions/src/main/resources/logback.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
+
+
+
+ "
+
+ "
+
+
+
diff --git a/couchbase/transactions/src/test/java/com/example/demo/DemoApplicationTests.java b/couchbase/transactions/src/test/java/com/example/demo/DemoApplicationTests.java
new file mode 100644
index 000000000..3cd9e2ac2
--- /dev/null
+++ b/couchbase/transactions/src/test/java/com/example/demo/DemoApplicationTests.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 the original author or authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.demo;
+
+import example.springdata.couchbase.util.EnabledOnCouchbaseAvailable;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author Michael Reiche
+ * @author Christoph Strobl
+ */
+@SpringBootTest(classes = DemoApplication.class)
+@EnabledOnCouchbaseAvailable
+class DemoApplicationTests {
+
+ @Test
+ void contextLoads() {}
+}
diff --git a/couchbase/util/pom.xml b/couchbase/util/pom.xml
index e410edd10..1ae79db0e 100644
--- a/couchbase/util/pom.xml
+++ b/couchbase/util/pom.xml
@@ -1,11 +1,12 @@
-
+4.0.0org.springframework.data.examplesspring-data-couchbase-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOT../pom.xml
@@ -15,8 +16,9 @@
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter-api
+ compile
diff --git a/couchbase/util/src/main/java/example/springdata/couchbase/util/CouchbaseAvailableRule.java b/couchbase/util/src/main/java/example/springdata/couchbase/util/CouchbaseAvailableRule.java
deleted file mode 100644
index fb4a790d6..000000000
--- a/couchbase/util/src/main/java/example/springdata/couchbase/util/CouchbaseAvailableRule.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2017-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package example.springdata.couchbase.util;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.time.Duration;
-
-import javax.net.SocketFactory;
-
-import org.junit.AssumptionViolatedException;
-import org.junit.rules.ExternalResource;
-
-import com.couchbase.client.core.env.DefaultCoreEnvironment;
-
-/**
- * Rule to check Couchbase server availability. If Couchbase is not running, tests are skipped.
- *
- * @author Mark Paluch
- */
-public class CouchbaseAvailableRule extends ExternalResource {
-
- private final String host;
- private final int port;
- private final Duration timeout = Duration.ofSeconds(1);
-
- private CouchbaseAvailableRule(String host, int port) {
- this.host = host;
- this.port = port;
- }
-
- /**
- * Create a new rule requiring Couchbase running on {@code localhost} on
- * {@link DefaultCoreEnvironment#BOOTSTRAP_HTTP_DIRECT_PORT}.
- *
- * @return the test rule.
- */
- public static CouchbaseAvailableRule onLocalhost() {
- return new CouchbaseAvailableRule("localhost", DefaultCoreEnvironment.BOOTSTRAP_HTTP_DIRECT_PORT);
- }
-
- @Override
- protected void before() throws Throwable {
-
- Socket socket = SocketFactory.getDefault().createSocket();
- try {
- socket.connect(new InetSocketAddress(host, port), Math.toIntExact(timeout.toMillis()));
- } catch (IOException e) {
- throw new AssumptionViolatedException(
- String.format("Couchbase not available on on %s:%d. Skipping tests.", host, port), e);
- } finally {
- socket.close();
- }
- }
-}
diff --git a/couchbase/util/src/main/java/example/springdata/couchbase/util/EnabledOnCouchbaseAvailable.java b/couchbase/util/src/main/java/example/springdata/couchbase/util/EnabledOnCouchbaseAvailable.java
new file mode 100644
index 000000000..82b047ac2
--- /dev/null
+++ b/couchbase/util/src/main/java/example/springdata/couchbase/util/EnabledOnCouchbaseAvailable.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.couchbase.util;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * {@code @EnabledOnCouchbaseAvailable} is used to signal that the annotated test class or test method is only enabled
+ * if Couchbase is running.
+ *
+ * When applied at the class level, all test methods within that class will be enabled.
+ *
+ * @author Mark Paluch
+ */
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+@ExtendWith(EnabledOnCouchbaseCondition.class)
+public @interface EnabledOnCouchbaseAvailable {
+
+ /**
+ * Couchbase host.
+ */
+ String host() default "localhost";
+
+ /**
+ * Couchbase port number.
+ */
+ int port() default 8091;
+}
diff --git a/couchbase/util/src/main/java/example/springdata/couchbase/util/EnabledOnCouchbaseCondition.java b/couchbase/util/src/main/java/example/springdata/couchbase/util/EnabledOnCouchbaseCondition.java
new file mode 100644
index 000000000..5f63e040c
--- /dev/null
+++ b/couchbase/util/src/main/java/example/springdata/couchbase/util/EnabledOnCouchbaseCondition.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.couchbase.util;
+
+import static org.junit.jupiter.api.extension.ConditionEvaluationResult.*;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.util.AnnotationUtils;
+
+/**
+ * {@link ExecutionCondition} for {@link EnabledOnCouchbaseCondition @EnabledOnCouchbaseAvailable}.
+ *
+ * @author Mark Paluch
+ * @see EnabledOnCouchbaseCondition
+ */
+class EnabledOnCouchbaseCondition implements ExecutionCondition {
+
+ private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled("@EnabledOnCouchbaseAvailable is not present");
+
+ @Override
+ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
+
+ var optional = AnnotationUtils.findAnnotation(context.getElement(), EnabledOnCouchbaseAvailable.class);
+
+ if (optional.isEmpty()) {
+ return ENABLED_BY_DEFAULT;
+ }
+
+ var annotation = optional.get();
+
+ try (var socket = new Socket()) {
+
+ socket.connect(new InetSocketAddress(annotation.host(), annotation.port()), 100);
+
+ return enabled(String.format("Connection successful to Couchbase at %s:%d", annotation.host(), annotation.port()));
+ } catch (IOException e) {
+ return disabled(
+ String.format("Cannot connect to Couchbase at %s:%d (%s)", annotation.host(), annotation.port(), e));
+ }
+ }
+
+}
diff --git a/elasticsearch/example/.gitignore b/elasticsearch/example/.gitignore
deleted file mode 100644
index e50bef9c7..000000000
--- a/elasticsearch/example/.gitignore
+++ /dev/null
@@ -1,13 +0,0 @@
-.project
-.classpath
-.springBeans
-.settings/
-target/
-
-#IntelliJ Stuff
-.idea
-*.iml
-
-##ignore local node data files for unit tests
-/data
-/.DS_Store
diff --git a/elasticsearch/example/README.md b/elasticsearch/example/README.md
index 01b4c6ff2..1989dc77f 100644
--- a/elasticsearch/example/README.md
+++ b/elasticsearch/example/README.md
@@ -9,6 +9,5 @@ Requirements:
To use local elasticsearch cluster:
* install elasticsearch
-* uncomment both properties in file 'application.properties'
+* Adapt 'application.properties' to point to your elasticsearch cluster
-
\ No newline at end of file
diff --git a/elasticsearch/example/pom.xml b/elasticsearch/example/pom.xml
index e4e3c4578..f69d676a7 100644
--- a/elasticsearch/example/pom.xml
+++ b/elasticsearch/example/pom.xml
@@ -3,32 +3,20 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- org.springframeworkspring-data-elasticsearch-example
- Spring Data Elasticsearch - Example
+ Spring Data Elasticsearch - High Level REST Client ExampleSample projects for Spring Data Elasticsearchhttps://github.com/spring-projects/spring-data-elasticsearchorg.springframework.data.examplesspring-data-elasticsearch-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOT
-
- org.springframework.boot
- spring-boot-starter-data-elasticsearch
-
-
- org.springframework.boot
- spring-boot-starter-logging
-
-
-
-
org.springframework.bootspring-boot-starter-log4j2
diff --git a/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java b/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java
index 5d001cbc4..c725211a8 100644
--- a/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java
+++ b/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014-2018 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,73 +16,53 @@
package example.springdata.elasticsearch.conference;
import java.util.Arrays;
-import java.util.UUID;
+import java.util.List;
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
-import org.elasticsearch.client.Client;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.data.elasticsearch.client.NodeClientFactoryBean;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
-import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
-import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
/**
* @author Artur Konczak
* @author Oliver Gierke
* @author Christoph Strobl
+ * @author Prakhar Gupta
*/
-@Configuration
-@EnableElasticsearchRepositories
+@SpringBootApplication
class ApplicationConfiguration {
@Autowired ElasticsearchOperations operations;
@Autowired ConferenceRepository repository;
- @Bean
- public NodeClientFactoryBean client() {
-
- NodeClientFactoryBean bean = new NodeClientFactoryBean(true);
- bean.setClusterName(UUID.randomUUID().toString());
- bean.setEnableHttp(false);
- bean.setPathData("target/elasticsearchTestData");
- bean.setPathHome("src/test/resources/test-home-dir");
-
- return bean;
- }
-
- @Bean
- public ElasticsearchTemplate elasticsearchTemplate(Client client) throws Exception {
- return new ElasticsearchTemplate(client);
- }
-
@PreDestroy
public void deleteIndex() {
- operations.deleteIndex(Conference.class);
+ operations.indexOps(Conference.class).delete();
}
@PostConstruct
public void insertDataSample() {
- // Remove all documents
- repository.deleteAll();
- operations.refresh(Conference.class);
+ operations.indexOps(Conference.class).refresh();
// Save data sample
- repository.save(Conference.builder().date("2014-11-06").name("Spring eXchange 2014 - London")
- .keywords(Arrays.asList("java", "spring")).location(new GeoPoint(51.500152D, -0.126236D)).build());
- repository.save(Conference.builder().date("2014-12-07").name("Scala eXchange 2014 - London")
- .keywords(Arrays.asList("scala", "play", "java")).location(new GeoPoint(51.500152D, -0.126236D)).build());
- repository.save(Conference.builder().date("2014-11-20").name("Elasticsearch 2014 - Berlin")
- .keywords(Arrays.asList("java", "elasticsearch", "kibana")).location(new GeoPoint(52.5234051D, 13.4113999))
- .build());
- repository.save(Conference.builder().date("2014-11-12").name("AWS London 2014")
- .keywords(Arrays.asList("cloud", "aws")).location(new GeoPoint(51.500152D, -0.126236D)).build());
- repository.save(Conference.builder().date("2014-10-04").name("JDD14 - Cracow")
- .keywords(Arrays.asList("java", "spring")).location(new GeoPoint(50.0646501D, 19.9449799)).build());
+
+ var documents = Arrays.asList(
+ Conference.builder().date("2014-11-06").name("Spring eXchange 2014 - London")
+ .keywords(Arrays.asList("java", "spring")).location(new GeoPoint(51.500152D, -0.126236D)).build(), //
+ Conference.builder().date("2014-12-07").name("Scala eXchange 2014 - London")
+ .keywords(Arrays.asList("scala", "play", "java")).location(new GeoPoint(51.500152D, -0.126236D)).build(), //
+ Conference.builder().date("2014-11-20").name("Elasticsearch 2014 - Berlin")
+ .keywords(Arrays.asList("java", "elasticsearch", "kibana")).location(new GeoPoint(52.5234051D, 13.4113999))
+ .build(), //
+ Conference.builder().date("2014-11-12").name("AWS London 2014").keywords(Arrays.asList("cloud", "aws"))
+ .location(new GeoPoint(51.500152D, -0.126236D)).build(), //
+ Conference.builder().date("2014-10-04").name("JDD14 - Cracow").keywords(Arrays.asList("java", "spring"))
+ .location(new GeoPoint(50.0646501D, 19.9449799)).build());
+
+ repository.saveAll(documents);
}
}
diff --git a/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/Conference.java b/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/Conference.java
index a1684ba73..ac2161c29 100644
--- a/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/Conference.java
+++ b/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/Conference.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014-2018 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,6 +17,9 @@
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
+import lombok.Builder;
+import lombok.Data;
+
import java.util.List;
import org.springframework.data.annotation.Id;
@@ -24,18 +27,14 @@
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
-import lombok.Builder;
-import lombok.Data;
-
/**
* @author Artur Konczak
* @author Oliver Gierke
- * @auhtor Christoph Strobl
+ * @author Christoph Strobl
*/
@Data
@Builder
-@Document(indexName = "conference-index", type = "geo-class-point-type", shards = 1, replicas = 0,
- refreshInterval = "-1")
+@Document(indexName = "conference-index")
public class Conference {
private @Id String id;
@@ -44,16 +43,4 @@ public class Conference {
private GeoPoint location;
private List keywords;
- // do not remove it
- public Conference() {}
-
- // do not remove it - work around for lombok generated constructor for all params
- public Conference(String id, String name, String date, GeoPoint location, List keywords) {
-
- this.id = id;
- this.name = name;
- this.date = date;
- this.location = location;
- this.keywords = keywords;
- }
}
diff --git a/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java b/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java
index 985c0378c..78024ab02 100644
--- a/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java
+++ b/elasticsearch/example/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2018 the original author or authors.
+ * Copyright 2014-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/elasticsearch/example/src/main/resources/application.properties b/elasticsearch/example/src/main/resources/application.properties
deleted file mode 100644
index 1982f98ac..000000000
--- a/elasticsearch/example/src/main/resources/application.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-# Uncomment both entries to connect with local elasticsearch cluster
-#spring.data.elasticsearch.clusterName=elasticsearch
-#spring.data.elasticsearch.clusterNodes=localhost:9300
diff --git a/elasticsearch/example/src/main/resources/log4j2.xml b/elasticsearch/example/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..feee1a0a6
--- /dev/null
+++ b/elasticsearch/example/src/main/resources/log4j2.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/elasticsearch/example/src/test/java/example/springdata/elasticsearch/conference/ElasticsearchOperationsTest.java b/elasticsearch/example/src/test/java/example/springdata/elasticsearch/conference/ElasticsearchOperationsTest.java
index 0580216b0..4a52ba8a8 100644
--- a/elasticsearch/example/src/test/java/example/springdata/elasticsearch/conference/ElasticsearchOperationsTest.java
+++ b/elasticsearch/example/src/test/java/example/springdata/elasticsearch/conference/ElasticsearchOperationsTest.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014-2018 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,22 +15,27 @@
*/
package example.springdata.elasticsearch.conference;
-import static org.hamcrest.Matchers.*;
-import static org.junit.Assert.*;
+import static org.assertj.core.api.Assertions.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
-import java.util.List;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.elasticsearch.client.ClientConfiguration;
+import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
+import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
-import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.util.Assert;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
/**
* Test case to show Spring Data Elasticsearch functionality.
@@ -38,42 +43,64 @@
* @author Artur Konczak
* @author Oliver Gierke
* @author Christoph Strobl
+ * @author Prakhar Gupta
+ * @author Peter-Josef Meisch
*/
-@RunWith(SpringRunner.class)
-@SpringBootTest(classes = ApplicationConfiguration.class)
-public class ElasticsearchOperationsTest {
+@SpringBootTest(classes = { ApplicationConfiguration.class, ElasticsearchOperationsTest.TestConfiguration.class })
+@Testcontainers
+class ElasticsearchOperationsTest {
private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+ @Container //
+ private static final ElasticsearchContainer container = new ElasticsearchContainer(
+ DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:8.7.0")) //
+ .withPassword("foobar") //
+ .withReuse(true);
+
+ @Configuration
+ static class TestConfiguration extends ElasticsearchConfiguration {
+ @Override
+ public ClientConfiguration clientConfiguration() {
+
+ Assert.notNull(container, "TestContainer is not initialized!");
+
+ return ClientConfiguration.builder() //
+ .connectedTo(container.getHttpHostAddress()) //
+ .usingSsl(container.createSslContextFromCa()) //
+ .withBasicAuth("elastic", "foobar") //
+ .build();
+ }
+ }
@Autowired ElasticsearchOperations operations;
@Test
- public void textSearch() throws ParseException {
+ void textSearch() throws ParseException {
- String expectedDate = "2014-10-29";
- String expectedWord = "java";
- CriteriaQuery query = new CriteriaQuery(
+ var expectedDate = "2014-10-29";
+ var expectedWord = "java";
+ var query = new CriteriaQuery(
new Criteria("keywords").contains(expectedWord).and(new Criteria("date").greaterThanEqual(expectedDate)));
- List result = operations.queryForList(query, Conference.class);
+ var result = operations.search(query, Conference.class);
- assertThat(result, hasSize(3));
+ assertThat(result).hasSize(3);
- for (Conference conference : result) {
- assertThat(conference.getKeywords(), hasItem(expectedWord));
- assertThat(format.parse(conference.getDate()), greaterThan(format.parse(expectedDate)));
+ for (var conference : result) {
+ assertThat(conference.getContent().getKeywords()).contains(expectedWord);
+ assertThat(format.parse(conference.getContent().getDate())).isAfter(format.parse(expectedDate));
}
}
@Test
- public void geoSpatialSearch() {
+ void geoSpatialSearch() {
- GeoPoint startLocation = new GeoPoint(50.0646501D, 19.9449799D);
- String range = "330mi"; // or 530km
- CriteriaQuery query = new CriteriaQuery(new Criteria("location").within(startLocation, range));
+ var startLocation = new GeoPoint(50.0646501D, 19.9449799D);
+ var range = "330mi"; // or 530km
+ var query = new CriteriaQuery(new Criteria("location").within(startLocation, range));
- List result = operations.queryForList(query, Conference.class);
+ var result = operations.search(query, Conference.class, IndexCoordinates.of("conference-index"));
- assertThat(result, hasSize(2));
+ assertThat(result).hasSize(2);
}
}
diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml
index 2709a282d..592c54c96 100644
--- a/elasticsearch/pom.xml
+++ b/elasticsearch/pom.xml
@@ -8,7 +8,7 @@
org.springframework.data.examplesspring-data-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOTSpring Data Elasticsearch - Examples
@@ -17,22 +17,67 @@
example
+ reactive
+
+
+
+ org.springframework.data.examples
+ spring-data-elasticsearch-example-utils
+ ${project.version}
+
+
+
+
+
org.springframework.bootspring-boot-starter-data-elasticsearch
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+
+
+
+ org.testcontainers
+ testcontainers-elasticsearch
+ test
+
+
+ org.testcontainers
+ testcontainers-junit-jupiter
+ test
+
+
-
-
- spring-data-next-releasetrain
-
- 6.5.0
-
-
-
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.apache.logging.log4j
+ log4j-core
+ ${log4j2.version}
+
+
+
+
+
+
diff --git a/elasticsearch/reactive/README.md b/elasticsearch/reactive/README.md
new file mode 100644
index 000000000..5f38d115c
--- /dev/null
+++ b/elasticsearch/reactive/README.md
@@ -0,0 +1,52 @@
+# Spring Data Elasticsearch - Reactive Examples
+
+The `ReactiveElasticsearchClient` is a client based on `WebClient`.
+It uses the request/response objects provided by the Elasticsearch core project.
+Calls are directly operated on the reactive stack, not wrapping async (thread pool bound) responses into reactive types.
+
+```java
+@SpringBootApplication
+class ApplicationConfiguration {}
+```
+
+The `ReactiveElasticsearchClient` can be used with the `ReactiveElasticsearchOperations` and `ReactiveElasticsearchRepository`.
+
+```java
+@Autowired ReactiveElasticsearchOperations operations;
+
+// ...
+
+CriteriaQuery query = new CriteriaQuery("keywords").contains("java");
+
+Flux result = operations.find(query, Conference.class);
+```
+
+```java
+interface ConferenceRepository extends ReactiveCrudRepository {
+
+ Flux findAllByKeywordsContains(String keyword);
+}
+
+// ...
+
+@Autowired ConferenceRepository repository;
+
+// ...
+
+Flux result = repository.findAllByKeywordsContains("java");
+```
+
+
+**Requirements:**
+
+ * [Maven](http://maven.apache.org/download.cgi)
+ * [Elasticsearch](https://www.elastic.co/de/downloads/elasticsearch)
+
+**Running Elasticsearch**
+
+```bash
+$ cd elasticsearch
+$ ./bin/elasticsearch
+```
+
+
diff --git a/elasticsearch/reactive/pom.xml b/elasticsearch/reactive/pom.xml
new file mode 100644
index 000000000..fc886f8b9
--- /dev/null
+++ b/elasticsearch/reactive/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+ spring-data-elasticsearch-reactive-example
+
+ Spring Data Elasticsearch - Reactive Example
+ Sample projects for Reactive Spring Data Elasticsearch
+ https://github.com/spring-projects/spring-data-elasticsearch
+
+
+ org.springframework.data.examples
+ spring-data-elasticsearch-examples
+ 4.0.0-SNAPSHOT
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-log4j2
+
+
+
+ io.projectreactor
+ reactor-test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.apache.logging.log4j
+ log4j-to-slf4j
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+
+
+
+
diff --git a/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java
new file mode 100644
index 000000000..0d852edbb
--- /dev/null
+++ b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ApplicationConfiguration.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.elasticsearch.conference;
+
+import java.util.Arrays;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.geo.GeoPoint;
+
+/**
+ * @author Christoph Strobl
+ */
+@SpringBootApplication
+class ApplicationConfiguration {
+
+ @Autowired ElasticsearchOperations operations;
+ @Autowired ConferenceRepository repository;
+
+ @PreDestroy
+ public void deleteIndex() {
+ operations.indexOps(Conference.class).delete();
+ }
+
+ @PostConstruct
+ public void insertDataSample() {
+
+ operations.indexOps(Conference.class).refresh();
+
+ // Save data sample
+
+ var documents = Arrays.asList(
+ Conference.builder().date("2014-11-06").name("Spring eXchange 2014 - London")
+ .keywords(Arrays.asList("java", "spring")).location(new GeoPoint(51.500152D, -0.126236D)).build(), //
+ Conference.builder().date("2014-12-07").name("Scala eXchange 2014 - London")
+ .keywords(Arrays.asList("scala", "play", "java")).location(new GeoPoint(51.500152D, -0.126236D)).build(), //
+ Conference.builder().date("2014-11-20").name("Elasticsearch 2014 - Berlin")
+ .keywords(Arrays.asList("java", "elasticsearch", "kibana")).location(new GeoPoint(52.5234051D, 13.4113999))
+ .build(), //
+ Conference.builder().date("2014-11-12").name("AWS London 2014").keywords(Arrays.asList("cloud", "aws"))
+ .location(new GeoPoint(51.500152D, -0.126236D)).build(), //
+ Conference.builder().date("2014-10-04").name("JDD14 - Cracow").keywords(Arrays.asList("java", "spring"))
+ .location(new GeoPoint(50.0646501D, 19.9449799)).build());
+
+ operations.save(documents);
+ operations.indexOps(Conference.class).refresh(); // ensure we have all documents properly refreshed to avoid races
+ // between tests.
+ }
+}
diff --git a/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/Conference.java b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/Conference.java
new file mode 100644
index 000000000..de395f560
--- /dev/null
+++ b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/Conference.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.elasticsearch.conference;
+
+import static org.springframework.data.elasticsearch.annotations.FieldType.*;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.elasticsearch.annotations.Document;
+import org.springframework.data.elasticsearch.annotations.Field;
+import org.springframework.data.elasticsearch.core.geo.GeoPoint;
+
+/**
+ * @author Christoph Strobl
+ */
+@Data
+@Builder
+@Document(indexName = "conference-index")
+public class Conference {
+
+ private @Id String id;
+ private String name;
+ private @Field(type = Date) String date;
+ private GeoPoint location;
+ private List keywords;
+}
diff --git a/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java
new file mode 100644
index 000000000..f5c5e468e
--- /dev/null
+++ b/elasticsearch/reactive/src/main/java/example/springdata/elasticsearch/conference/ConferenceRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.elasticsearch.conference;
+
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+import reactor.core.publisher.Flux;
+
+/**
+ * @author Christoph Strobl
+ */
+interface ConferenceRepository extends ReactiveCrudRepository {
+
+ Flux findAllByKeywordsContainsAndDateAfter(String keyword, String Date);
+}
diff --git a/elasticsearch/reactive/src/main/resources/log4j2.xml b/elasticsearch/reactive/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..feee1a0a6
--- /dev/null
+++ b/elasticsearch/reactive/src/main/resources/log4j2.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchOperationsTest.java b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchOperationsTest.java
new file mode 100644
index 000000000..d446ec15c
--- /dev/null
+++ b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchOperationsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.elasticsearch.conference;
+
+import static org.assertj.core.api.Assertions.*;
+
+import reactor.test.StepVerifier;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.elasticsearch.client.ClientConfiguration;
+import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration;
+import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.SearchHit;
+import org.springframework.data.elasticsearch.core.query.Criteria;
+import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
+import org.springframework.util.Assert;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Test case to show Spring Data Elasticsearch functionality.
+ *
+ * @author Christoph Strobl
+ * @author Prakhar Gupta
+ * @author Peter-Josef Meisch
+ */
+@SpringBootTest(
+ classes = { ApplicationConfiguration.class, ReactiveElasticsearchOperationsTest.TestConfiguration.class })
+@Testcontainers
+class ReactiveElasticsearchOperationsTest {
+
+ private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+
+ @Container //
+ private static final ElasticsearchContainer container = new ElasticsearchContainer(
+ DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:9.0.3")) //
+ .withPassword("foobar") //
+ .withReuse(true);
+
+ @Configuration
+ static class TestConfiguration extends ReactiveElasticsearchConfiguration {
+ @Override
+ public ClientConfiguration clientConfiguration() {
+
+ Assert.notNull(container, "TestContainer is not initialized!");
+
+ return ClientConfiguration.builder() //
+ .connectedTo(container.getHttpHostAddress()) //
+ .usingSsl(container.createSslContextFromCa()) //
+ .withBasicAuth("elastic", "foobar") //
+ .build();
+ }
+ }
+
+ @Autowired ReactiveElasticsearchOperations operations;
+
+ @Test
+ void textSearch() {
+
+ var expectedDate = "2014-10-29";
+ var expectedWord = "java";
+ var query = new CriteriaQuery(
+ new Criteria("keywords").contains(expectedWord).and(new Criteria("date").greaterThanEqual(expectedDate)));
+
+ operations.search(query, Conference.class) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) //
+ .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) //
+ .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) //
+ .verifyComplete();
+ }
+
+ private void verify(SearchHit hit, String expectedWord, String expectedDate) {
+
+ assertThat(hit.getContent().getKeywords()).contains(expectedWord);
+ try {
+ assertThat(format.parse(hit.getContent().getDate())).isAfter(format.parse(expectedDate));
+ } catch (ParseException e) {
+ fail("o_O", e);
+ }
+ }
+}
diff --git a/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchRepositoryTest.java b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchRepositoryTest.java
new file mode 100644
index 000000000..ca921b883
--- /dev/null
+++ b/elasticsearch/reactive/src/test/java/example/springdata/elasticsearch/conference/ReactiveElasticsearchRepositoryTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2019-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.elasticsearch.conference;
+
+import static org.assertj.core.api.Assertions.*;
+
+import reactor.test.StepVerifier;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.elasticsearch.client.ClientConfiguration;
+import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration;
+import org.springframework.util.Assert;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Test case to show reactive Spring Data Elasticsearch repository functionality.
+ *
+ * @author Christoph Strobl
+ * @author Prakhar Gupta
+ * @author Peter-Josef Meisch
+ */
+@SpringBootTest(
+ classes = { ApplicationConfiguration.class, ReactiveElasticsearchRepositoryTest.TestConfiguration.class })
+@Testcontainers
+class ReactiveElasticsearchRepositoryTest {
+
+ private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+
+ @Container //
+ private static final ElasticsearchContainer container = new ElasticsearchContainer(
+ DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:9.0.3")) //
+ .withPassword("foobar") //
+ .withReuse(true);
+
+ @Configuration
+ static class TestConfiguration extends ReactiveElasticsearchConfiguration {
+ @Override
+ public ClientConfiguration clientConfiguration() {
+
+ Assert.notNull(container, "TestContainer is not initialized!");
+
+ return ClientConfiguration.builder() //
+ .connectedTo(container.getHttpHostAddress()) //
+ .usingSsl(container.createSslContextFromCa()) //
+ .withBasicAuth("elastic", "foobar") //
+ .build();
+ }
+ }
+
+ @Autowired ConferenceRepository repository;
+
+ @Test
+ void textSearch() {
+
+ var expectedDate = "2014-10-29";
+ var expectedWord = "java";
+
+ repository.findAllByKeywordsContainsAndDateAfter(expectedWord, expectedDate) //
+ .as(StepVerifier::create) //
+ .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) //
+ .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) //
+ .consumeNextWith(it -> verify(it, expectedWord, expectedDate)) //
+ .verifyComplete();
+ }
+
+ private void verify(Conference it, String expectedWord, String expectedDate) {
+
+ assertThat(it.getKeywords()).contains(expectedWord);
+ try {
+ assertThat(format.parse(it.getDate())).isAfter(format.parse(expectedDate));
+ } catch (ParseException e) {
+ fail("o_O", e);
+ }
+ }
+}
diff --git a/jdbc/aot-optimization/README.adoc b/jdbc/aot-optimization/README.adoc
new file mode 100644
index 000000000..a5422ee94
--- /dev/null
+++ b/jdbc/aot-optimization/README.adoc
@@ -0,0 +1,97 @@
+= Spring Data JDBC - Ahead of Time Repository Optimization Example
+
+The project shows the usage of AOT Repositories.
+Ahead of Time Repositories implement query methods through code contribution and allow for debugging queries during runtime.
+Additionally, AOT repositories improve startup time and reduce memory consumption because AOT optimized query methods do not require reflective introspection.
+Each AOT repository is documented with a JSON file that describes the queries implemented by the repository.
+
+== Using AOT Repositories
+
+Repository AOT processing is enabled by default when using Spring Boot's AOT processing (see `pom.xml` for `spring-boot-maven-plugin` usage).
+AOT processing generates AOT artifacts to `target/spring-aot` and through the regular build.
+When using the JVM mode (not Graal Native Images), then you need to enable AOT mode on the JVM when running your application through `-Dspring.aot.enabled=true`.
+
+[source,bash]
+----
+$ mvn clean package
+$ java -Dspring.aot.enabled=true -jar target/spring-data-jdbc-aot-optimization-4.0.0-SNAPSHOT.jar
+----
+
+You can find more details about AOT processing in the https://docs.spring.io/spring-data/relational/reference/4.0/jdbc/aot.html[Spring Data JDBC Reference Documentation].
+
+== AOT Repository
+
+**`CategoryRepositoryImpl__AotRepository`**
+
+Excerpt from: `target/spring-aot/main/sources/example/springdata/aot/CategoryRepositoryImpl__AotRepository.java`
+
+[source,java]
+----
+@Generated
+public class CategoryRepositoryImpl__AotRepository extends AotRepositoryFragmentSupport {
+ public CategoryRepositoryImpl__AotRepository(JdbcAggregateOperations operations,
+ RowMapperFactory rowMapperFactory,
+ RepositoryFactoryBeanSupport.FragmentCreationContext context) {
+ super(operations, rowMapperFactory, context);
+ }
+
+ public List findAllByNameContaining(String name) {
+ Criteria criteria = Criteria.where("name").like("%" + escape(name) + "%");
+ StatementFactory.SelectionBuilder builder = getStatementFactory().select(Category.class).filter(criteria);
+
+ RowMapper rowMapper = getRowMapperFactory().create(Category.class);
+ List result = (List) builder.executeWith((sql, paramSource) -> getJdbcOperations().query(sql, paramSource, new RowMapperResultSetExtractor<>(rowMapper)));
+ return (List) convertMany(result, Category.class);
+ }
+
+ public List findWithDeclaredQuery(String name) {
+ class ExpressionMarker{};
+ String query = "SELECT * FROM category WHERE name = :name";
+ MapSqlParameterSource parameterSource = new MapSqlParameterSource();
+ getBindableValue(ExpressionMarker.class.getEnclosingMethod(), name, 0).bind("name", parameterSource);
+
+ RowMapper rowMapper = getRowMapperFactory().create(Category.class);
+ List result = (List) getJdbcOperations().query(query, parameterSource, new RowMapperResultSetExtractor<>(rowMapper));
+ return (List) convertMany(result, Category.class);
+ }
+}
+----
+
+== Metadata
+
+**`CategoryRepository.json`**
+
+Excerpt from: `target/spring-aot/main/resources/example/springdata/aot/CategoryRepository.json`
+
+[source,json]
+----
+{
+ "name": "example.springdata.aot.CategoryRepository",
+ "module": "JDBC",
+ "type": "IMPERATIVE",
+ "methods": [
+ {
+ "name": "findProjectedByNameContaining",
+ "signature": "public abstract java.util.List example.springdata.aot.CategoryRepository.findProjectedByNameContaining(java.lang.String)",
+ "query": {
+ "query": "SELECT \"CATEGORY\".\"NAME\" AS \"NAME\", \"CATEGORY\".\"DESCRIPTION\" AS \"DESCRIPTION\" FROM \"CATEGORY\" WHERE \"CATEGORY\".\"NAME\" LIKE :name"
+ }
+ },
+ {
+ "name": "findWithDeclaredQuery",
+ "signature": "public abstract java.util.List example.springdata.aot.CategoryRepository.findWithDeclaredQuery(java.lang.String)",
+ "query": {
+ "query": "SELECT * FROM category WHERE name = :name"
+ }
+ },
+ {
+ "name": "save",
+ "signature": "org.springframework.data.jdbc.repository.support.SimpleJdbcRepository",
+ "fragment": {
+ "interface": "org.springframework.data.jdbc.repository.support.SimpleJdbcRepository",
+ "fragment": "org.springframework.data.jdbc.repository.support.SimpleJdbcRepository"
+ }
+ }
+ ]
+}
+----
diff --git a/jdbc/aot-optimization/pom.xml b/jdbc/aot-optimization/pom.xml
new file mode 100644
index 000000000..ab6ad122b
--- /dev/null
+++ b/jdbc/aot-optimization/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-examples
+ 4.0.0-SNAPSHOT
+
+
+ spring-data-jdbc-aot-optimization
+ Spring Data JDBC - AOT Optimization Example
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ process-aot
+
+ process-aot
+
+
+
+
+
+
+
+
diff --git a/jdbc/aot-optimization/src/main/java/example/springdata/aot/AotJdbcApp.java b/jdbc/aot-optimization/src/main/java/example/springdata/aot/AotJdbcApp.java
new file mode 100644
index 000000000..3fa695ca6
--- /dev/null
+++ b/jdbc/aot-optimization/src/main/java/example/springdata/aot/AotJdbcApp.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.aot;
+
+import java.util.List;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.jdbc.core.dialect.JdbcH2Dialect;
+
+/**
+ * @author Mark Paluch
+ */
+@SpringBootApplication
+public class AotJdbcApp {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AotJdbcApp.class, args);
+ }
+
+ @Bean
+ JdbcH2Dialect dialect() {
+ return JdbcH2Dialect.INSTANCE;
+ }
+
+ @Bean
+ CommandLineRunner commandLineRunner(CategoryRepository repository) {
+
+ return args -> {
+
+ System.out.println("--------------------------------------");
+ System.out.println("-- findAllByNameContaining(\"dings\") --");
+ System.out.println("--------------------------------------");
+ List categories = repository.findAllByNameContaining("dings");
+ categories.forEach(it -> System.out
+ .println("Id: %s, Name: %s, Description: %s".formatted(it.getId(), it.getName(), it.getDescription())));
+
+ System.out.println();
+ System.out.println();
+
+ System.out.println("--------------------------------------------");
+ System.out.println("-- findProjectedByNameContaining(\"dings\") --");
+ System.out.println("--------------------------------------------");
+ List dings = repository.findProjectedByNameContaining("dings");
+ dings.forEach(it -> System.out.println("Name: %s, Description: %s, Combined: %s".formatted(it.getName(),
+ it.getDescription(), it.getNameAndDescription())));
+ };
+ }
+}
diff --git a/jdbc/aot-optimization/src/main/java/example/springdata/aot/Category.java b/jdbc/aot-optimization/src/main/java/example/springdata/aot/Category.java
new file mode 100644
index 000000000..479dbe509
--- /dev/null
+++ b/jdbc/aot-optimization/src/main/java/example/springdata/aot/Category.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.aot;
+
+import java.time.LocalDateTime;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceCreator;
+
+/**
+ * Coarse classification.
+ *
+ * @author Jens Schauder
+ */
+public class Category {
+
+ private final @Id Long id;
+ private String name, description;
+ private LocalDateTime created;
+ private Long inserted;
+
+ public Category(String name, String description) {
+
+ this.id = null;
+ this.name = name;
+ this.description = description;
+ this.created = LocalDateTime.now();
+ }
+
+ @PersistenceCreator
+ Category(Long id, String name, String description, LocalDateTime created, Long inserted) {
+ this.id = id;
+ this.name = name;
+ this.description = description;
+ this.created = created;
+ this.inserted = inserted;
+ }
+
+ public void timeStamp() {
+
+ if (inserted == 0) {
+ inserted = System.currentTimeMillis();
+ }
+ }
+
+ public Long getId() {
+ return this.id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public LocalDateTime getCreated() {
+ return this.created;
+ }
+
+ public Long getInserted() {
+ return this.inserted;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public void setCreated(LocalDateTime created) {
+ this.created = created;
+ }
+
+ public void setInserted(Long inserted) {
+ this.inserted = inserted;
+ }
+
+ public Category withId(Long id) {
+ return this.id == id ? this : new Category(id, this.name, this.description, this.created, this.inserted);
+ }
+
+ @Override
+ public String toString() {
+ return "Category(id=" + this.getId() + ", name=" + this.getName() + ", description=" + this.getDescription()
+ + ", created=" + this.getCreated() + ", inserted=" + this.getInserted() + ")";
+ }
+}
diff --git a/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryConfiguration.java b/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryConfiguration.java
new file mode 100644
index 000000000..67006ef99
--- /dev/null
+++ b/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryConfiguration.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.aot;
+
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
+import org.springframework.data.relational.core.mapping.event.RelationalEvent;
+
+/**
+ * Contains infrastructure necessary for creating repositories, listeners and EntityCallbacks.
+ *
+ * Not that a listener may change an entity without any problem.
+ *
+ * @author Jens Schauder
+ * @author Mark Paluch
+ */
+@Configuration
+public class CategoryConfiguration {
+
+ /**
+ * @return {@link ApplicationListener} for {@link RelationalEvent}s.
+ */
+ @Bean
+ public ApplicationListener> loggingListener() {
+
+ return (ApplicationListener) event -> {
+ if (event instanceof RelationalEvent) {
+ System.out.println("Received an event: " + event);
+ }
+ };
+ }
+
+ /**
+ * @return {@link BeforeSaveCallback} for {@link Category}.
+ */
+ @Bean
+ public BeforeSaveCallback timeStampingSaveTime() {
+
+ return (entity, aggregateChange) -> {
+
+ entity.timeStamp();
+
+ return entity;
+ };
+ }
+}
diff --git a/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryProjection.java b/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryProjection.java
new file mode 100644
index 000000000..3a314cc1c
--- /dev/null
+++ b/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryProjection.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.aot;
+
+/**
+ * @author Mark Paluch
+ */
+public interface CategoryProjection {
+
+ String getName();
+
+ String getDescription();
+
+ default String getNameAndDescription() {
+ return getName() + " - " + getDescription();
+ }
+}
diff --git a/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryRepository.java b/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryRepository.java
new file mode 100644
index 000000000..8efb47d70
--- /dev/null
+++ b/jdbc/aot-optimization/src/main/java/example/springdata/aot/CategoryRepository.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.aot;
+
+import java.util.List;
+
+import org.springframework.data.jdbc.repository.query.Query;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Repository for Categories.
+ *
+ * @author Mark Paluch
+ */
+interface CategoryRepository extends CrudRepository {
+
+ List findAllByNameContaining(String name);
+
+ List findProjectedByNameContaining(String name);
+
+ @Query("SELECT * FROM category WHERE name = :name")
+ List findWithDeclaredQuery(String name);
+
+}
diff --git a/jdbc/aot-optimization/src/main/resources/application.properties b/jdbc/aot-optimization/src/main/resources/application.properties
new file mode 100644
index 000000000..4e2092bc7
--- /dev/null
+++ b/jdbc/aot-optimization/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+#logging.level.org.springframework.data.repository.aot.generate.RepositoryContributor=trace
+
diff --git a/jdbc/aot-optimization/src/main/resources/data.sql b/jdbc/aot-optimization/src/main/resources/data.sql
new file mode 100644
index 000000000..f6c4d9ea3
--- /dev/null
+++ b/jdbc/aot-optimization/src/main/resources/data.sql
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+INSERT INTO category(name, description, created, inserted)
+VALUES ('Cars', 'Anything that has approximately 4 wheels.', now(), 1);
+INSERT INTO category(name, description, created, inserted)
+VALUES ('Buildings', 'Walls, anyone?', now(), 2);
+INSERT INTO category(name, description, created, inserted)
+VALUES ('Chemistry Labs', 'Heisenberg calling', now(), 3);
diff --git a/jdbc/aot-optimization/src/main/resources/schema.sql b/jdbc/aot-optimization/src/main/resources/schema.sql
new file mode 100644
index 000000000..369e89873
--- /dev/null
+++ b/jdbc/aot-optimization/src/main/resources/schema.sql
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+CREATE TABLE IF NOT EXISTS category
+(
+ id INTEGER IDENTITY PRIMARY KEY,
+ name VARCHAR(100),
+ description VARCHAR(2000),
+ created DATETIME,
+ inserted BIGINT
+);
diff --git a/jdbc/basics/README.adoc b/jdbc/basics/README.adoc
index 1ec4b6f2a..94b0af2b1 100644
--- a/jdbc/basics/README.adoc
+++ b/jdbc/basics/README.adoc
@@ -6,9 +6,9 @@ This example demonstrate basic usage of JDBC based repositories.
* The `SimpleEntityTests` demonstrate CRUD operations for an entity without references, just simple properties of various types.
-* The `CategoryContext` shows how to configure an application context so that Spring Data JDBC can create repositories.
+* The `CategoryConfiguration` shows how to configure an application context so that Spring Data JDBC can create repositories.
-* The `ApplicationListener` registered in `CategoryContext` demonstrate how to react to events published by Spring Data JDBC and how entities can get manipulated in such event listeners.
+* The `ApplicationListener` registered in `CategoryConfiguration` demonstrate how to react to events published by Spring Data JDBC and how entities can get manipulated in such event listeners.
=== AggregateTests
@@ -27,4 +27,4 @@ This is achieved by providing a custom `NamingStrategy` which maps both to the s
* `LegoSetRepository` has methods that utilize `@Query` annotations.
-* Note that `Model` is a value class, i.e. it is immutable, and doesn't have an ID.
\ No newline at end of file
+* Note that `Model` is a value class, i.e. it is immutable, and doesn't have an ID.
diff --git a/jdbc/basics/pom.xml b/jdbc/basics/pom.xml
index ec70bbce7..526ce850d 100644
--- a/jdbc/basics/pom.xml
+++ b/jdbc/basics/pom.xml
@@ -1,5 +1,6 @@
-
+4.0.0spring-data-jdbc-basics
@@ -7,11 +8,18 @@
org.springframework.data.examplesspring-data-jdbc-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOT../pom.xmlSpring Data JDBC - Basic usage examplesSample project demonstrating Spring Data JDBC features
-
\ No newline at end of file
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/AgeGroup.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/AgeGroup.java
index f4ccb4cce..64d7c3867 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/AgeGroup.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/AgeGroup.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/AggregateConfiguration.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/AggregateConfiguration.java
index 10e4c384c..e64525cbd 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/AggregateConfiguration.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/AggregateConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,26 +25,33 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.io.ClassPathResource;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
-import org.springframework.data.jdbc.repository.config.JdbcConfiguration;
-import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent;
+import org.springframework.data.relational.core.mapping.event.BeforeConvertEvent;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.datasource.init.DataSourceInitializer;
+import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.lang.Nullable;
+import javax.sql.DataSource;
+
/**
* @author Jens Schauder
* @author Mark Paluch
*/
@Configuration
@EnableJdbcRepositories
-public class AggregateConfiguration extends JdbcConfiguration {
+public class AggregateConfiguration extends AbstractJdbcConfiguration {
final AtomicInteger id = new AtomicInteger(0);
@Bean
public ApplicationListener> idSetting() {
- return (ApplicationListener) event -> {
+ return (ApplicationListener) event -> {
if (event.getEntity() instanceof LegoSet) {
setIds((LegoSet) event.getEntity());
@@ -58,7 +65,7 @@ private void setIds(LegoSet legoSet) {
legoSet.setId(id.incrementAndGet());
}
- Manual manual = legoSet.getManual();
+ var manual = legoSet.getManual();
if (manual != null) {
manual.setId((long) legoSet.getId());
@@ -86,4 +93,23 @@ public String convert(Clob clob) {
}
}));
}
+
+ @Bean
+ public NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcOperations operations) {
+ return new NamedParameterJdbcTemplate(operations);
+ }
+
+ @Bean
+ DataSourceInitializer initializer(DataSource dataSource) {
+
+ var initializer = new DataSourceInitializer();
+ initializer.setDataSource(dataSource);
+
+ var script = new ClassPathResource("schema.sql");
+ var populator = new ResourceDatabasePopulator(script);
+ initializer.setDatabasePopulator(populator);
+
+ return initializer;
+ }
+
}
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/LegoSet.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/LegoSet.java
index abbaa5ead..8d4a27380 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/LegoSet.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/LegoSet.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,8 @@
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
-import lombok.experimental.Wither;
+import lombok.With;
+import lombok.With;
import java.time.Period;
import java.time.temporal.ChronoUnit;
@@ -30,6 +31,7 @@
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.MappedCollection;
/**
* A Lego Set consisting of multiple Blocks and a manual
@@ -49,12 +51,12 @@ public class LegoSet {
* Since Manuals are part of a {@link LegoSet} and only make sense inside a {@link LegoSet} it is considered part of
* the Aggregate.
*/
- @Column("handbuch_id")
+ @Column("HANDBUCH_ID")
private Manual manual;
// You can build multiple models from one LegoSet
- @Column(keyColumn = "name")
- private final @AccessType(Type.FIELD) @Wither(AccessLevel.PACKAGE) Map models;
+ @MappedCollection(keyColumn = "NAME")
+ private final @AccessType(Type.FIELD) @With(AccessLevel.PACKAGE) Map models;
LegoSet() {
this.models = new HashMap<>();
@@ -62,7 +64,7 @@ public class LegoSet {
// conversion for custom types currently has to be done through getters/setter + marking the underlying property with
// @Transient.
- @Column("min_age")
+ @Column("MIN_AGE")
public int getIntMinimumAge() {
return toInt(this.minimumAge);
}
@@ -71,7 +73,7 @@ public void setIntMinimumAge(int years) {
minimumAge = toPeriod(years);
}
- @Column("max_age")
+ @Column("MAX_AGE")
public int getIntMaximumAge() {
return toInt(this.maximumAge);
}
@@ -90,7 +92,7 @@ private static Period toPeriod(int years) {
public void addModel(String name, String description) {
- Model model = new Model(name, description);
+ var model = new Model(name, description);
models.put(name, model);
}
}
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/LegoSetRepository.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/LegoSetRepository.java
index 5e2d8e75c..37ccd6750 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/LegoSetRepository.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/LegoSetRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,13 +15,13 @@
*/
package example.springdata.jdbc.basics.aggregate;
+import java.util.List;
+
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
-import java.util.List;
-
/**
* A repository for {@link LegoSet}.
*
@@ -29,13 +29,27 @@
*/
interface LegoSetRepository extends CrudRepository {
- @Query("SELECT m.name model_name, m.description, l.name set_name" +
- " FROM model m" +
- " JOIN lego_set l" +
- " ON m.lego_set = l.id" +
- " WHERE :age BETWEEN l.min_age and l.max_age")
+ @Query("""
+ SELECT m.name model_name, m.description, l.name set_name
+ FROM model m
+ JOIN lego_set l
+ ON m.lego_set = l.id
+ WHERE :age BETWEEN l.min_age and l.max_age
+ """)
List reportModelForAge(@Param("age") int age);
+ /**
+ * See https://stackoverflow.com/questions/52978700/how-to-write-a-custom-query-in-spring-data-jdbc
+ * @param name
+ * @return
+ */
+ @Query("""
+ select a.*, b.handbuch_id as manual_handbuch_id, b.author as manual_author, b.text as manual_text from lego_set a
+ join handbuch b on a.id = b.handbuch_id
+ where a.name = :name
+ """)
+ List findByName(@Param("name") String name);
+
@Modifying
@Query("UPDATE model set name = lower(name) WHERE name <> lower(name)")
int lowerCaseMapKeys();
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/Manual.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/Manual.java
index d38706c3e..c50ea0b07 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/Manual.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/Manual.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,10 +27,10 @@
* @author Jens Schauder
*/
@Data
-@Table("handbuch")
+@Table("HANDBUCH")
public class Manual {
- @Column("handbuch_id")
+ @Column("HANDBUCH_ID")
private @Id Long id;
private String author, text;
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/Model.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/Model.java
index 3c2d25966..3cefa9d0d 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/Model.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/Model.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,15 +17,12 @@
import lombok.AccessLevel;
import lombok.Value;
-import lombok.experimental.Wither;
+import lombok.With;
/**
* One of potentially multiple models that can be build from a single {@link LegoSet}. No getters or setters needed.
*
* @author Jens Schauder
*/
-@Value
-@Wither(AccessLevel.PACKAGE)
-public class Model {
- String name, description;
+public record Model(String name, String description) {
}
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/ModelReport.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/ModelReport.java
index 51b9849fc..c5a19dcf5 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/ModelReport.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/aggregate/ModelReport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,13 +17,10 @@
import lombok.AccessLevel;
import lombok.Value;
-import lombok.experimental.Wither;
+import lombok.With;
/**
* @author Jens Schauder
*/
-@Value
-@Wither(AccessLevel.PACKAGE)
-public class ModelReport {
- String modelName, description, setName;
+public record ModelReport(String modelName, String description, String setName) {
}
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/Category.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/Category.java
index b6bbee29a..d8232a161 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/Category.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/Category.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,12 +21,12 @@
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Setter;
-import lombok.experimental.Wither;
+import lombok.With;
import java.time.LocalDateTime;
import org.springframework.data.annotation.Id;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.PersistenceCreator;
/**
* Coarse classification for {@link LegoSet}s, like "Car", "Plane", "Building" and so on.
@@ -34,10 +34,10 @@
* @author Jens Schauder
*/
@Data
-@AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = @__(@PersistenceConstructor))
+@AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = @__(@PersistenceCreator))
public class Category {
- private final @Id @Wither Long id;
+ private final @Id @With Long id;
private String name, description;
private LocalDateTime created;
private @Setter long inserted;
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/CategoryConfiguration.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/CategoryConfiguration.java
index 38cb3857b..abb95942b 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/CategoryConfiguration.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/CategoryConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,14 +19,13 @@
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
-import org.springframework.data.jdbc.repository.config.JdbcConfiguration;
-import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent;
+import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
import org.springframework.data.relational.core.mapping.event.RelationalEvent;
/**
- * Contains infrastructure necessary for creating repositories and two listeners.
+ * Contains infrastructure necessary for creating repositories, listeners and EntityCallbacks.
*
* Not that a listener may change an entity without any problem.
*
@@ -35,9 +34,11 @@
*/
@Configuration
@EnableJdbcRepositories
-@Import(JdbcConfiguration.class)
-public class CategoryConfiguration {
+public class CategoryConfiguration extends AbstractJdbcConfiguration {
+ /**
+ * @return {@link ApplicationListener} for {@link RelationalEvent}s.
+ */
@Bean
public ApplicationListener> loggingListener() {
@@ -48,17 +49,17 @@ public ApplicationListener> loggingListener() {
};
}
+ /**
+ * @return {@link BeforeSaveCallback} for {@link Category}.
+ */
@Bean
- public ApplicationListener timeStampingSaveTime() {
+ public BeforeSaveCallback timeStampingSaveTime() {
- return event -> {
+ return (entity, aggregateChange) -> {
- Object entity = event.getEntity();
+ entity.timeStamp();
- if (entity instanceof Category) {
- Category category = (Category) entity;
- category.timeStamp();
- }
+ return entity;
};
}
}
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/CategoryRepository.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/CategoryRepository.java
index 5fb52fd7f..88bc671be 100644
--- a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/CategoryRepository.java
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/CategoryRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,4 +22,4 @@
*
* @author Jens Schauder
*/
-interface CategoryRepository extends CrudRepository {}
+interface CategoryRepository extends CrudRepository, WithInsert {}
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/WithInsert.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/WithInsert.java
new file mode 100644
index 000000000..937a0f831
--- /dev/null
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/WithInsert.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.basics.simpleentity;
+
+/**
+ * Fragment interface providing the {@link WithInsert#insert(Object)} signature.
+ *
+ * @author Jens Schauder
+ */
+public interface WithInsert {
+
+ /**
+ * Custom insert method.
+ *
+ * @param t
+ * @return
+ */
+ T insert(T t);
+}
diff --git a/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/WithInsertImpl.java b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/WithInsertImpl.java
new file mode 100644
index 000000000..a65993bf8
--- /dev/null
+++ b/jdbc/basics/src/main/java/example/springdata/jdbc/basics/simpleentity/WithInsertImpl.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.basics.simpleentity;
+
+import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
+
+/**
+ * Fragment implementation providing the {@link WithInsert#insert(Object)} implementation.
+ *
+ * @author Jens Schauder
+ */
+public class WithInsertImpl implements WithInsert {
+
+ private final JdbcAggregateTemplate template;
+
+ public WithInsertImpl(JdbcAggregateTemplate template) {
+ this.template = template;
+ }
+
+ @Override
+ public T insert(T t) {
+ return template.insert(t);
+ }
+}
diff --git a/jdbc/basics/src/main/resources/application.properties b/jdbc/basics/src/main/resources/application.properties
index 2804353df..912eeb5f4 100644
--- a/jdbc/basics/src/main/resources/application.properties
+++ b/jdbc/basics/src/main/resources/application.properties
@@ -1,2 +1,2 @@
logging.level.org.springframework.data=INFO
-logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
\ No newline at end of file
+logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
diff --git a/jdbc/basics/src/main/resources/schema.sql b/jdbc/basics/src/main/resources/schema.sql
index 766e5a7e0..1c996022d 100644
--- a/jdbc/basics/src/main/resources/schema.sql
+++ b/jdbc/basics/src/main/resources/schema.sql
@@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS category (id INTEGER IDENTITY PRIMARY KEY, name VARCHAR(100), description VARCHAR(2000), age_group VARCHAR(20), created DATETIME, inserted BIGINT);
CREATE TABLE IF NOT EXISTS Lego_Set (id INTEGER, name VARCHAR(100), min_Age INTEGER, max_Age INTEGER);
-CREATE TABLE IF NOT EXISTS Handbuch (handbuch_id INTEGER, author CHAR(100), text CLOB);
+CREATE TABLE IF NOT EXISTS Handbuch (handbuch_id INTEGER, author VARCHAR(100), text CLOB);
CREATE TABLE IF NOT EXISTS Model (name VARCHAR(100), description CLOB, lego_set INTEGER);
diff --git a/jdbc/basics/src/test/java/example/springdata/jdbc/basics/Output.java b/jdbc/basics/src/test/java/example/springdata/jdbc/basics/Output.java
index 5803b42b7..fbdb5fcbb 100644
--- a/jdbc/basics/src/test/java/example/springdata/jdbc/basics/Output.java
+++ b/jdbc/basics/src/test/java/example/springdata/jdbc/basics/Output.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,7 +32,7 @@ public class Output {
public static void list(Iterable> categories, String title) {
- StringBuilder message = new StringBuilder(String.format("==== %s ====\n", title));
+ var message = new StringBuilder(String.format("==== %s ====\n", title));
categories.forEach(category -> message.append(category.toString().replace(", ", ",\n\t")));
diff --git a/jdbc/basics/src/test/java/example/springdata/jdbc/basics/aggregate/AggregateTests.java b/jdbc/basics/src/test/java/example/springdata/jdbc/basics/aggregate/AggregateTests.java
index a548e0a38..513c98997 100644
--- a/jdbc/basics/src/test/java/example/springdata/jdbc/basics/aggregate/AggregateTests.java
+++ b/jdbc/basics/src/test/java/example/springdata/jdbc/basics/aggregate/AggregateTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -21,40 +21,38 @@
import java.time.Period;
import java.util.Arrays;
-import java.util.List;
import org.assertj.core.groups.Tuple;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc;
+import org.springframework.boot.data.jdbc.test.autoconfigure.AutoConfigureDataJdbc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
-import org.springframework.test.context.junit4.SpringRunner;
/**
* Demonstrates various possibilities to customize the behavior of a repository.
*
* @author Jens Schauder
+ * @author Divya Srivastava
*/
-@RunWith(SpringRunner.class)
@SpringBootTest(classes = AggregateConfiguration.class)
-@AutoConfigureJdbc
+@AutoConfigureDataJdbc
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
-public class AggregateTests {
+class AggregateTests {
@Autowired LegoSetRepository repository;
@Test
- public void exerciseSomewhatComplexEntity() {
+ void exerciseSomewhatComplexEntity() {
- LegoSet smallCar = createLegoSet("Small Car 01", 5, 12);
+ var smallCar = createLegoSet("Small Car 01", 5, 12);
smallCar.setManual(new Manual("Just put all the pieces together in the right order", "Jens Schauder"));
smallCar.addModel("suv", "SUV with sliding doors.");
smallCar.addModel("roadster", "Slick red roadster.");
repository.save(smallCar);
- Iterable legoSets = repository.findAll();
+ var legoSets = repository.findAll();
Output.list(legoSets, "Original LegoSet");
checkLegoSets(legoSets, "Just put all the pieces together in the right order", 2);
@@ -72,24 +70,23 @@ public void exerciseSomewhatComplexEntity() {
legoSets = repository.findAll();
Output.list(legoSets, "Manual replaced");
checkLegoSets(legoSets, "One last attempt: Just build a car! Ok?", 3);
-
}
@Test
- public void customQueries() {
+ void customQueries() {
- LegoSet smallCars = createLegoSet("Small Car - 01", 5, 10);
+ var smallCarsSetName = "Small Car - 01";
+ var smallCars = createLegoSet(smallCarsSetName, 5, 10);
smallCars.setManual(new Manual("Just put all the pieces together in the right order", "Jens Schauder"));
smallCars.addModel("SUV", "SUV with sliding doors.");
smallCars.addModel("roadster", "Slick red roadster.");
- LegoSet f1Racer = createLegoSet("F1 Racer", 6, 15);
+ var f1Racer = createLegoSet("F1 Racer", 6, 15);
f1Racer.setManual(new Manual("Build a helicopter or a plane", "M. Shoemaker"));
-
f1Racer.addModel("F1 Ferrari 2018", "A very fast red car.");
- LegoSet constructionVehicles = createLegoSet("Construction Vehicles", 3, 6);
+ var constructionVehicles = createLegoSet("Construction Vehicles", 3, 6);
constructionVehicles.setManual(
new Manual("Build a Road Roler, a Mobile Crane, a Tracked Dumper, or a Backhoe Loader ", "Bob the Builder"));
@@ -101,21 +98,23 @@ public void customQueries() {
repository.saveAll(Arrays.asList(smallCars, f1Racer, constructionVehicles));
- List report = repository.reportModelForAge(6);
+ var report = repository.reportModelForAge(6);
Output.list(report, "Model Report");
assertThat(report).hasSize(7)
- .allMatch(m -> m.getDescription() != null && m.getModelName() != null && m.getSetName() != null);
+ .allMatch(m -> m.description() != null && m.modelName() != null && m.setName() != null);
- int updated = repository.lowerCaseMapKeys();
+ var updated = repository.lowerCaseMapKeys();
// SUV, F1 Ferrari 2018 and Muck get updated
assertThat(updated).isEqualTo(3);
+ var legoSetsByName = repository.findByName(smallCarsSetName);
+ assertThat(legoSetsByName).hasSize(1);
}
private LegoSet createLegoSet(String name, int minimumAge, int maximumAge) {
- LegoSet smallCar = new LegoSet();
+ var smallCar = new LegoSet();
smallCar.setName(name);
smallCar.setMinimumAge(Period.ofYears(minimumAge));
diff --git a/jdbc/basics/src/test/java/example/springdata/jdbc/basics/simpleentity/SimpleEntityTests.java b/jdbc/basics/src/test/java/example/springdata/jdbc/basics/simpleentity/SimpleEntityTests.java
index fcd4a121f..bd2d83384 100644
--- a/jdbc/basics/src/test/java/example/springdata/jdbc/basics/simpleentity/SimpleEntityTests.java
+++ b/jdbc/basics/src/test/java/example/springdata/jdbc/basics/simpleentity/SimpleEntityTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,31 +20,30 @@
import example.springdata.jdbc.basics.Output;
import example.springdata.jdbc.basics.aggregate.AgeGroup;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc;
+import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureJdbc;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
/**
* Demonstrates simple CRUD operations with a simple entity without any references.
*
* @author Jens Schauder
+ * @author Divya Srivastava
*/
-@RunWith(SpringRunner.class)
@SpringBootTest(classes = CategoryConfiguration.class)
@AutoConfigureJdbc
-public class SimpleEntityTests {
+class SimpleEntityTests {
@Autowired CategoryRepository repository;
@Test
- public void exerciseRepositoryForSimpleEntity() {
+ void exerciseRepositoryForSimpleEntity() {
// create some categories
- Category cars = repository.save(new Category("Cars", "Anything that has approximately 4 wheels", AgeGroup._3to8));
- Category buildings = repository.save(new Category("Buildings", null, AgeGroup._12andOlder));
+ var cars = repository.save(new Category("Cars", "Anything that has approximately 4 wheels.", AgeGroup._3to8));
+ var buildings = repository.save(new Category("Buildings", null, AgeGroup._12andOlder));
// save categories
Output.list(repository.findAll(), "`Cars` and `Buildings` got saved");
@@ -61,4 +60,13 @@ public void exerciseRepositoryForSimpleEntity() {
repository.delete(cars);
Output.list(repository.findAll(), "`Cars` is gone.");
}
+
+ @Test
+ void directInsert() {
+
+ var cars = new Category("Cars", "Anything that has approximately 4 wheels.", AgeGroup._3to8).withId(23L);
+ repository.insert(cars);
+
+ Output.list(repository.findAll(), "`Cars` inserted with id 23L");
+ }
}
diff --git a/jdbc/composite-ids/README.adoc b/jdbc/composite-ids/README.adoc
new file mode 100644
index 000000000..739d1d369
--- /dev/null
+++ b/jdbc/composite-ids/README.adoc
@@ -0,0 +1,8 @@
+== Spring Data JDBC Composite Id
+
+=== EmployeeTest
+
+Demonstrates saving an entity with composite id.
+
+Once by using a direct insert, via a custom `insert` method in the repository, backed by `JdbcAggregateTemplate.insert` and once by a custom id generating callback.
+See `CompositeConfiguration.idGeneration()`.
\ No newline at end of file
diff --git a/jdbc/composite-ids/pom.xml b/jdbc/composite-ids/pom.xml
new file mode 100644
index 000000000..bd73353b3
--- /dev/null
+++ b/jdbc/composite-ids/pom.xml
@@ -0,0 +1,25 @@
+
+ 4.0.0
+
+ spring-data-jdbc-composite-ids
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-examples
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - Examples using composite ids
+ Sample project demonstrating Spring Data JDBCs support for custom ids
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/CompositeConfiguration.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/CompositeConfiguration.java
new file mode 100644
index 000000000..776d4a65a
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/CompositeConfiguration.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.compositeid;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
+import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
+import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+
+/**
+ * Configuration for the demonstration of composite ids.
+ *
+ * Registers a {@link BeforeConvertCallback} for generating ids.
+ *
+ * @author Jens Schauder
+ */
+@Configuration
+@EnableJdbcRepositories
+public class CompositeConfiguration extends AbstractJdbcConfiguration {
+
+ @Bean
+ BeforeConvertCallback idGeneration() {
+ return new BeforeConvertCallback<>() {
+ AtomicLong counter = new AtomicLong();
+
+ @Override
+ public Employee onBeforeConvert(Employee employee) {
+ if (employee.id == null) {
+ employee.id = new EmployeeId(Organization.RND, counter.addAndGet(1));
+ }
+ return employee;
+ }
+ };
+ }
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Employee.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Employee.java
new file mode 100644
index 000000000..070614f6e
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Employee.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.compositeid;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceCreator;
+
+/**
+ * A simple entity sporting a compostite id.
+ *
+ * @author Jens Schauder
+ */
+class Employee {
+
+ @Id
+ EmployeeId id;
+
+ String name;
+
+ @PersistenceCreator
+ Employee(EmployeeId id, String name) {
+
+ this.id = id;
+ this.name = name;
+ }
+
+ Employee(String name) {
+ this.name = name;
+ }
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeId.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeId.java
new file mode 100644
index 000000000..835ae6a62
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeId.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.compositeid;
+
+/**
+ * Composite id for {@link Employee} instances.
+ *
+ * @author Jens Schauder
+ */
+record EmployeeId(
+ Organization organization,
+ Long employeeNumber) {
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeRepository.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeRepository.java
new file mode 100644
index 000000000..066208a58
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/EmployeeRepository.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package example.springdata.jdbc.compositeid;
+
+import org.springframework.data.repository.Repository;
+
+
+/**
+ * Repositories for {@link Employee} instances.
+ *
+ * @author Jens Schauder
+ * @see InsertRepository
+ * @see InsertRepositoryImpl
+ */
+interface EmployeeRepository extends Repository, InsertRepository {
+ Employee findById(EmployeeId id);
+
+ Employee save(Employee employee);
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepository.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepository.java
new file mode 100644
index 000000000..562b26ffb
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.compositeid;
+
+
+/**
+ * Interface for repositories supporting an {@literal insert} operation, that always performs an insert on the database
+ * and does not check the instance.
+ *
+ * @author Jens Schauder
+ */
+interface InsertRepository {
+ E insert(E employee);
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepositoryImpl.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepositoryImpl.java
new file mode 100644
index 000000000..2735a6024
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/InsertRepositoryImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.compositeid;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
+
+/**
+ * Fragment implementing the {@literal insert} operation using a {@link JdbcAggregateTemplate}.
+ *
+ * @author Jens Schauder
+ */
+class InsertRepositoryImpl implements InsertRepository {
+
+ private JdbcAggregateTemplate template;
+
+ InsertRepositoryImpl(JdbcAggregateTemplate template) {
+ this.template = template;
+ }
+
+ @Override
+ public E insert(E employee) {
+ return template.insert(employee);
+ }
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Organization.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Organization.java
new file mode 100644
index 000000000..d53191357
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/Organization.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.compositeid;
+
+/**
+ * Just an enum to be part of the composite id, to demonstrate that one may use various datatypes.
+ *
+ * @author Jens Schauder
+ */
+enum Organization {
+ RND,
+ SALES,
+ MARKETING,
+ PURCHASING
+}
diff --git a/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/package-info.java b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/package-info.java
new file mode 100644
index 000000000..b7ef233b4
--- /dev/null
+++ b/jdbc/composite-ids/src/main/java/example/springdata/jdbc/compositeid/package-info.java
@@ -0,0 +1,4 @@
+@NonNullApi
+package example.springdata.jdbc.compositeid;
+
+import org.springframework.lang.NonNullApi;
\ No newline at end of file
diff --git a/jdbc/composite-ids/src/main/resources/application.properties b/jdbc/composite-ids/src/main/resources/application.properties
new file mode 100644
index 000000000..2804353df
--- /dev/null
+++ b/jdbc/composite-ids/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+logging.level.org.springframework.data=INFO
+logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
\ No newline at end of file
diff --git a/jdbc/composite-ids/src/main/resources/schema.sql b/jdbc/composite-ids/src/main/resources/schema.sql
new file mode 100644
index 000000000..73e9ee996
--- /dev/null
+++ b/jdbc/composite-ids/src/main/resources/schema.sql
@@ -0,0 +1,6 @@
+create table employee
+(
+ organization varchar(20),
+ employee_number int,
+ name varchar(100)
+);
diff --git a/jdbc/composite-ids/src/test/java/example/springdata/jdbc/compositeid/EmployeeTests.java b/jdbc/composite-ids/src/test/java/example/springdata/jdbc/compositeid/EmployeeTests.java
new file mode 100644
index 000000000..c59e95c32
--- /dev/null
+++ b/jdbc/composite-ids/src/test/java/example/springdata/jdbc/compositeid/EmployeeTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.compositeid;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jdbc.test.autoconfigure.AutoConfigureDataJdbc;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * Test demonstrating the use of composite ids.
+ *
+ * @author Jens Schauder
+ */
+@SpringBootTest(classes = CompositeConfiguration.class)
+@AutoConfigureDataJdbc
+class EmployeeTests {
+
+ @Autowired
+ EmployeeRepository repository;
+
+ @Test
+ void employeeDirectInsert() {
+
+ Employee employee = repository.insert(new Employee(new EmployeeId(Organization.RND, 23L), "Jens Schauder"));
+
+ Employee reloaded = repository.findById(employee.id);
+
+ assertThat(reloaded.name).isEqualTo(employee.name);
+ }
+
+ @Test
+ void employeeIdGeneration() {
+
+ Employee employee = repository.save(new Employee("Mark Paluch"));
+
+ assertThat(employee.id).isNotNull();
+ }
+}
diff --git a/jdbc/graalvm-native/README.adoc b/jdbc/graalvm-native/README.adoc
new file mode 100644
index 000000000..3aa10ed9b
--- /dev/null
+++ b/jdbc/graalvm-native/README.adoc
@@ -0,0 +1,47 @@
+== Spring Data JDBC - GraalVM native image
+
+This example compiles a basic Spring Data JDBC application into a GraalVM native image.
+
+=== Install GraalVM & native image tooling
+
+Download and install GraalVM using https://sdkman.io/[SDKMAN!].
+
+```
+$> sdk install java .r17-grl
+$> gu install native-image
+```
+
+=== Compile to native image
+
+The maven build uses a dedicated profile `native` to trigger the native image creation.
+
+```
+$> maven clean package -P native
+```
+
+This will create the native executable in the target folder.
+
+=== Run the image
+
+Run the image directly from your console as shown below.
+This will print results of crud functions invoked via a `CommandLineRunner`.
+
+```
+$> ./target/spring-data-jdbc-graalvm-native
+
+ . ____ _ __ _ _
+ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
+( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
+ \\/ ___)| |_)| | | | | || (_| | ) ) ) )
+ ' |____| .__|_| |_|_| |_\__, | / / / /
+ =========|_|==============|___/=/_/_/_/
+ :: Spring Boot :: (v3.0.0-SNAPSHOT)
+
+INFO 82562 --- [ main] e.s.j.g.GraalvmNativeApplication : Starting GraalvmNativeApplication using Java 17.0.4 with PID 82562
+INFO 82562 --- [ main] e.s.j.g.GraalvmNativeApplication : Started GraalvmNativeApplication in 0.042 seconds (process running for 0.061)
+insertAuthors(): author1 = Author{name='Josh Long'}
+insertAuthors(): author2 = Author{name='Martin Kleppmann'}
+listAllAuthors(): author = Author{name='Josh Long'}
+ Book{title='Reactive Spring'}
+...
+```
diff --git a/jdbc/graalvm-native/pom.xml b/jdbc/graalvm-native/pom.xml
new file mode 100644
index 000000000..4450cd3dc
--- /dev/null
+++ b/jdbc/graalvm-native/pom.xml
@@ -0,0 +1,54 @@
+
+ 4.0.0
+
+ spring-data-jdbc-graalvm-native
+
+
+ org.springframework.data.examples
+ spring-data-examples
+ 4.0.0-SNAPSHOT
+ ../../pom.xml
+
+
+ Spring Data JDBC - GraalVM native sample
+ Sample project demonstrating Spring Data JDBC features running as GraalVM native image
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+
+ com.h2database
+ h2
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+
+ native
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+
+
+
+
+
+
+
diff --git a/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/AuthorRepository.java b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/AuthorRepository.java
new file mode 100644
index 000000000..e9b66bb4e
--- /dev/null
+++ b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/AuthorRepository.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.graalvmnative;
+
+import java.util.Optional;
+
+import example.springdata.jdbc.graalvmnative.model.Author;
+import org.springframework.data.jdbc.repository.query.Query;
+import org.springframework.data.repository.ListCrudRepository;
+
+public interface AuthorRepository extends ListCrudRepository {
+
+ Optional findByNameContainingIgnoreCase(String partialName);
+
+ @Query("SELECT * FROM author a WHERE a.name = :name LIMIT 1")
+ Optional queryFindByName(String name);
+}
diff --git a/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/CLR.java b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/CLR.java
new file mode 100644
index 000000000..785906499
--- /dev/null
+++ b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/CLR.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.graalvmnative;
+
+import java.util.List;
+import java.util.Set;
+
+import example.springdata.jdbc.graalvmnative.model.Author;
+import example.springdata.jdbc.graalvmnative.model.Book;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Christoph Strobl
+ * @since 2022/10
+ */
+@Component
+public class CLR implements CommandLineRunner {
+
+ private final AuthorRepository authorRepository;
+
+ CLR(AuthorRepository authorRepository) {
+ this.authorRepository = authorRepository;
+ }
+
+ @Override
+ public void run(String... args) {
+ insertAuthors();
+ listAllAuthors();
+ findById();
+ findByPartialName();
+ queryFindByName();
+ deleteAll();
+ }
+
+ private void deleteAll() {
+ this.authorRepository.deleteAll();
+ long count = this.authorRepository.count();
+ System.out.printf("deleteAll(): count = %d%n", count);
+ }
+
+ private void queryFindByName() {
+ Author author1 = this.authorRepository.queryFindByName("Josh Long").orElse(null);
+ Author author2 = this.authorRepository.queryFindByName("Martin Kleppmann").orElse(null);
+
+ System.out.printf("queryFindByName(): author1 = %s%n", author1);
+ System.out.printf("queryFindByName(): author2 = %s%n", author2);
+ }
+
+ private void findByPartialName() {
+ Author author1 = this.authorRepository.findByNameContainingIgnoreCase("sh lo").orElse(null);
+ Author author2 = this.authorRepository.findByNameContainingIgnoreCase("in kl").orElse(null);
+
+ System.out.printf("findByPartialName(): author1 = %s%n", author1);
+ System.out.printf("findByPartialName(): author2 = %s%n", author2);
+ }
+
+ private void findById() {
+ Author author1 = this.authorRepository.findById(1L).orElse(null);
+ Author author2 = this.authorRepository.findById(2L).orElse(null);
+
+ System.out.printf("findById(): author1 = %s%n", author1);
+ System.out.printf("findById(): author2 = %s%n", author2);
+ }
+
+ private void listAllAuthors() {
+ List authors = this.authorRepository.findAll();
+ for (Author author : authors) {
+ System.out.printf("listAllAuthors(): author = %s%n", author);
+ for (Book book : author.getBooks()) {
+ System.out.printf("\t%s%n", book);
+ }
+ }
+ }
+
+ private void insertAuthors() {
+ Author author1 = this.authorRepository.save(new Author(null, "Josh Long",
+ Set.of(new Book(null, "Reactive Spring"), new Book(null, "Cloud Native Java"))));
+ Author author2 = this.authorRepository.save(
+ new Author(null, "Martin Kleppmann", Set.of(new Book(null, "Designing Data Intensive Applications"))));
+
+ System.out.printf("insertAuthors(): author1 = %s%n", author1);
+ System.out.printf("insertAuthors(): author2 = %s%n", author2);
+ }
+}
diff --git a/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/GraalvmNativeApplication.java b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/GraalvmNativeApplication.java
new file mode 100644
index 000000000..f78bd031b
--- /dev/null
+++ b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/GraalvmNativeApplication.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.graalvmnative;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class GraalvmNativeApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(GraalvmNativeApplication.class, args);
+ }
+}
diff --git a/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/model/Author.java b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/model/Author.java
new file mode 100644
index 000000000..29f476511
--- /dev/null
+++ b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/model/Author.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.graalvmnative.model;
+
+import java.util.Objects;
+import java.util.Set;
+
+import org.springframework.data.annotation.Id;
+
+public class Author {
+
+ @Id
+ private final Long id;
+
+ private final String name;
+
+ private final Set books;
+
+ public Author(Long id, String name, Set books) {
+ this.id = id;
+ this.name = name;
+ this.books = books;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Set getBooks() {
+ return books;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Author author = (Author) o;
+ return Objects.equals(id, author.id) && Objects.equals(name, author.name)
+ && Objects.equals(books, author.books);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, books);
+ }
+
+ @Override
+ public String toString() {
+ return "Author{" + "name='" + name + '\'' + '}';
+ }
+}
diff --git a/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/model/Book.java b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/model/Book.java
new file mode 100644
index 000000000..ea3d59163
--- /dev/null
+++ b/jdbc/graalvm-native/src/main/java/example/springdata/jdbc/graalvmnative/model/Book.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.graalvmnative.model;
+
+import java.util.Objects;
+
+import org.springframework.data.annotation.Id;
+
+public class Book {
+
+ @Id
+ private final Long id;
+
+ private final String title;
+
+ public Book(Long id, String title) {
+ this.id = id;
+ this.title = title;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Book book = (Book) o;
+ return Objects.equals(id, book.id) && Objects.equals(title, book.title);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, title);
+ }
+
+ @Override
+ public String toString() {
+ return "Book{" + "title='" + title + '\'' + '}';
+ }
+}
diff --git a/jdbc/graalvm-native/src/main/resources/application.properties b/jdbc/graalvm-native/src/main/resources/application.properties
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/jdbc/graalvm-native/src/main/resources/application.properties
@@ -0,0 +1 @@
+
diff --git a/jdbc/graalvm-native/src/main/resources/schema.sql b/jdbc/graalvm-native/src/main/resources/schema.sql
new file mode 100644
index 000000000..14e51639f
--- /dev/null
+++ b/jdbc/graalvm-native/src/main/resources/schema.sql
@@ -0,0 +1,12 @@
+create table author
+(
+ id bigint auto_increment primary key,
+ name varchar not null
+);
+
+create table book
+(
+ id bigint auto_increment primary key,
+ author bigint not null references author (id),
+ title varchar not null
+);
diff --git a/jdbc/graalvm-native/src/test/java/example/springdata/jdbc/graalvmnative/GraalvmNativeApplicationTests.java b/jdbc/graalvm-native/src/test/java/example/springdata/jdbc/graalvmnative/GraalvmNativeApplicationTests.java
new file mode 100644
index 000000000..90520e4ad
--- /dev/null
+++ b/jdbc/graalvm-native/src/test/java/example/springdata/jdbc/graalvmnative/GraalvmNativeApplicationTests.java
@@ -0,0 +1,13 @@
+package example.springdata.jdbc.graalvmnative;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class GraalvmNativeApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/jdbc/howto/README.adoc b/jdbc/howto/README.adoc
new file mode 100644
index 000000000..0be538297
--- /dev/null
+++ b/jdbc/howto/README.adoc
@@ -0,0 +1,13 @@
+== Spring Data JDBC How Tos
+
+=== ID Generation
+
+Demonstrates the various ways how the user might control generation of IDs when the default of database generated IDs is not sufficient.
+
+=== Bidirectional aggregate internal relationships.
+
+Demonstrates how to maintain a bidirectional relationship within a single aggregate.
+
+=== Bidirectional relationships between aggregates.
+
+Demonstrates how to maintain a bidirectional relationship between aggregates.
\ No newline at end of file
diff --git a/jdbc/howto/bidirectionalexternal/README.adoc b/jdbc/howto/bidirectionalexternal/README.adoc
new file mode 100644
index 000000000..96de5745f
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/README.adoc
@@ -0,0 +1,6 @@
+== Spring Data JDBC How To Bidirectional External Relationship
+
+An external relationship is one that links one aggregate from another one.
+Spring Data JDBC models such a relationship by referencing the aggregate by id, possibly wrapped in an `AggregateReference`.
+
+For a bidirectional navigation it turns out you just need an additional query method.
\ No newline at end of file
diff --git a/jdbc/howto/bidirectionalexternal/pom.xml b/jdbc/howto/bidirectionalexternal/pom.xml
new file mode 100644
index 000000000..8f4c77d81
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-how-to
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ 4.0.0
+
+ spring-data-jdbc-how-to-bidirectional-external
+
+ Spring Data JDBC - How to model bidirectional relationships between aggregates
+ Sample project for Spring Data JDBC demonstrating how to model bidirectional relationships between aggregates.
+ It serves as a source code repository for a How-To article on the Spring Blog
+ https://spring.io/spring-data-jdbc
+ 2021
+
diff --git a/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/BidirectionalApplication.java b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/BidirectionalApplication.java
new file mode 100644
index 000000000..12c3eea2d
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/BidirectionalApplication.java
@@ -0,0 +1,13 @@
+package example.springdata.jdbc.howto.bidirectionalexternal;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class BidirectionalApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(BidirectionalApplication.class, args);
+ }
+
+}
diff --git a/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/Minion.java b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/Minion.java
new file mode 100644
index 000000000..1e2411fc7
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/Minion.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalexternal;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.jdbc.core.mapping.AggregateReference;
+
+class Minion {
+ @Id
+ Long id;
+ String name;
+ AggregateReference evilMaster;
+
+ Minion(String name, AggregateReference evilMaster) {
+ this.name = name;
+ this.evilMaster = evilMaster;
+ }
+}
diff --git a/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/MinionRepository.java b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/MinionRepository.java
new file mode 100644
index 000000000..d80307efb
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/MinionRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalexternal;
+
+import java.util.Collection;
+
+import org.springframework.data.jdbc.repository.query.Query;
+import org.springframework.data.repository.CrudRepository;
+
+interface MinionRepository extends CrudRepository {
+
+ @Query("SELECT * FROM MINION WHERE EVIL_MASTER = :id")
+ Collection findByEvilMaster(Long id);
+}
diff --git a/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/Person.java b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/Person.java
new file mode 100644
index 000000000..cd0640241
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/Person.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalexternal;
+
+import org.springframework.data.annotation.Id;
+
+class Person {
+ @Id
+ Long id;
+ String name;
+
+ Person(String name) {
+ this.name = name;
+ }
+}
diff --git a/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/PersonRepository.java b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/PersonRepository.java
new file mode 100644
index 000000000..6a6f0c981
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/src/main/java/example/springdata/jdbc/howto/bidirectionalexternal/PersonRepository.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalexternal;
+
+import org.springframework.data.repository.CrudRepository;
+
+interface PersonRepository extends CrudRepository {
+
+}
diff --git a/jdbc/howto/bidirectionalexternal/src/main/resources/application.properties b/jdbc/howto/bidirectionalexternal/src/main/resources/application.properties
new file mode 100644
index 000000000..3a7895a8b
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/src/main/resources/application.properties
@@ -0,0 +1 @@
+logging.level.org.springframework.jdbc.core.JdbcTemplate=debug
diff --git a/jdbc/howto/bidirectionalexternal/src/main/resources/schema.sql b/jdbc/howto/bidirectionalexternal/src/main/resources/schema.sql
new file mode 100644
index 000000000..379fed64e
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/src/main/resources/schema.sql
@@ -0,0 +1,13 @@
+CREATE TABLE PERSON
+(
+ ID IDENTITY PRIMARY KEY,
+ NAME VARCHAR(255)
+);
+
+CREATE TABLE MINION
+(
+ ID IDENTITY PRIMARY KEY,
+ NAME VARCHAR(255),
+ EVIL_MASTER BIGINT,
+ CONSTRAINT FK_MINION_PERSON FOREIGN KEY (EVIL_MASTER) REFERENCES PERSON
+);
diff --git a/jdbc/howto/bidirectionalexternal/src/test/java/example/springdata/jdbc/howto/bidirectionalexternal/BidirectionalApplicationTests.java b/jdbc/howto/bidirectionalexternal/src/test/java/example/springdata/jdbc/howto/bidirectionalexternal/BidirectionalApplicationTests.java
new file mode 100644
index 000000000..91b0497e9
--- /dev/null
+++ b/jdbc/howto/bidirectionalexternal/src/test/java/example/springdata/jdbc/howto/bidirectionalexternal/BidirectionalApplicationTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalexternal;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Collection;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest;
+import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
+import org.springframework.data.jdbc.core.mapping.AggregateReference;
+
+@DataJdbcTest
+class BidirectionalApplicationTests {
+
+ @Autowired
+ MinionRepository minions;
+
+ @Autowired
+ PersonRepository persons;
+
+ @Autowired
+ JdbcAggregateTemplate template;
+
+ @Test
+ void bidi() {
+
+
+ AggregateReference scarletReference = AggregateReference.to(persons.save(new Person("Scarlet")).id);
+
+ Minion bob = minions.save(new Minion("Bob", scarletReference));
+ Minion kevin = minions.save(new Minion("Kevin", scarletReference));
+ Minion stuart = minions.save(new Minion("Stuart", scarletReference));
+
+ AggregateReference gruReference = AggregateReference.to(persons.save(new Person("Gru")).id);
+ Minion tim = minions.save(new Minion("Tim", gruReference));
+
+ Collection minionsOfScarlet = minions.findByEvilMaster(scarletReference.getId());
+
+ assertThat(minionsOfScarlet).extracting(m -> m.name).containsExactlyInAnyOrder("Bob", "Kevin", "Stuart");
+ }
+
+
+}
diff --git a/jdbc/howto/bidirectionalinternal/README.adoc b/jdbc/howto/bidirectionalinternal/README.adoc
new file mode 100644
index 000000000..9c33b54f8
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/README.adoc
@@ -0,0 +1,6 @@
+== Spring Data JDBC How To Bidirectional Internal Relationship
+
+An internal relationship is one that links entities within an aggregate.
+Spring Data JDBC doesn't have special mechanics to maintain such a relationship.
+Instead you may choose to maintain the reverse relationship while creating the aggregate.
+This takes care of the construction during initial creation or modification of the aggregate, and also of the construction when loading an aggregate from the database.
\ No newline at end of file
diff --git a/jdbc/howto/bidirectionalinternal/pom.xml b/jdbc/howto/bidirectionalinternal/pom.xml
new file mode 100644
index 000000000..45d91d248
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-how-to
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ 4.0.0
+
+ spring-data-jdbc-how-to-bidirectional-internal
+
+ Spring Data JDBC - How to model aggregate internal bidirectional relationships
+ Sample project for Spring Data JDBC demonstrating how to model aggregate internal bidirectional relationships.
+ It serves as a source code repository for a How-To article on the Spring Blog
+ https://spring.io/spring-data-jdbc
+ 2021
+
diff --git a/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/BiDiInternalApplication.java b/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/BiDiInternalApplication.java
new file mode 100644
index 000000000..a53130f0c
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/BiDiInternalApplication.java
@@ -0,0 +1,13 @@
+package example.springdata.jdbc.howto.bidirectionalinternal;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class BiDiInternalApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(BiDiInternalApplication.class, args);
+ }
+
+}
diff --git a/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/Minion.java b/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/Minion.java
new file mode 100644
index 000000000..a5905b26b
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/Minion.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalinternal;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceCreator;
+
+class Minion {
+ @Id
+ Long id;
+ String name;
+ final Set toys = new HashSet<>();
+
+ Minion(String name) {
+ this.name = name;
+ }
+
+ @PersistenceCreator
+ private Minion(Long id, String name, Collection toys) {
+
+ this.id = id;
+ this.name = name;
+ toys.forEach(this::addToy);
+ }
+
+ public void addToy(Toy toy) {
+ toys.add(toy);
+ toy.minion = this;
+ }
+
+ public void showYourToys() {
+ toys.forEach(Toy::sayHello);
+ }
+}
diff --git a/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/MinionRepository.java b/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/MinionRepository.java
new file mode 100644
index 000000000..4c4fb5ab7
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/MinionRepository.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalinternal;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface MinionRepository extends CrudRepository {
+}
diff --git a/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/Toy.java b/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/Toy.java
new file mode 100644
index 000000000..dc55c1f87
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/src/main/java/example/springdata/jdbc/howto/bidirectionalinternal/Toy.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalinternal;
+
+import org.springframework.data.annotation.Transient;
+
+class Toy {
+ String name;
+
+ @Transient // org.SPRINGframework.DATA...
+ Minion minion;
+
+ Toy(String name) {
+ this.name = name;
+ }
+
+ public void sayHello() {
+ System.out.println("I'm " + name + " and I'm a toy of " + minion.name);
+ }
+}
diff --git a/jdbc/howto/bidirectionalinternal/src/main/resources/application.properties b/jdbc/howto/bidirectionalinternal/src/main/resources/application.properties
new file mode 100644
index 000000000..3a7895a8b
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/src/main/resources/application.properties
@@ -0,0 +1 @@
+logging.level.org.springframework.jdbc.core.JdbcTemplate=debug
diff --git a/jdbc/howto/bidirectionalinternal/src/main/resources/schema.sql b/jdbc/howto/bidirectionalinternal/src/main/resources/schema.sql
new file mode 100644
index 000000000..7f67813ef
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/src/main/resources/schema.sql
@@ -0,0 +1,11 @@
+CREATE TABLE MINION
+(
+ ID IDENTITY PRIMARY KEY,
+ NAME VARCHAR(255),
+);
+
+CREATE TABLE TOY
+(
+ MINION BIGINT NOT NULL,
+ NAME VARCHAR(255)
+);
diff --git a/jdbc/howto/bidirectionalinternal/src/test/java/example/springdata/jdbc/howto/bidirectionalinternal/BiDiInternalApplicationTests.java b/jdbc/howto/bidirectionalinternal/src/test/java/example/springdata/jdbc/howto/bidirectionalinternal/BiDiInternalApplicationTests.java
new file mode 100644
index 000000000..999232895
--- /dev/null
+++ b/jdbc/howto/bidirectionalinternal/src/test/java/example/springdata/jdbc/howto/bidirectionalinternal/BiDiInternalApplicationTests.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.bidirectionalinternal;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest;
+
+@DataJdbcTest
+class BiDiInternalApplicationTests {
+
+ @Autowired
+ MinionRepository minions;
+
+ @Test
+ void biDi() {
+
+ Minion bob = new Minion("Bob");
+ bob.addToy(new Toy("Dolphin"));
+ bob.addToy(new Toy("Tiger Duck"));
+
+ minions.save(bob);
+
+ Minion reloaded = minions.findById(bob.id).get();
+
+ reloaded.showYourToys();
+ }
+}
diff --git a/jdbc/howto/caching/README.adoc b/jdbc/howto/caching/README.adoc
new file mode 100644
index 000000000..00a518570
--- /dev/null
+++ b/jdbc/howto/caching/README.adoc
@@ -0,0 +1,6 @@
+== Spring Data JDBC How To cache.
+
+Spring Data JDBC doesn't include any caching for entities.
+But it is trivially combined with Springs Cache abstraction.
+
+This project demonstrates this.
diff --git a/jdbc/howto/caching/pom.xml b/jdbc/howto/caching/pom.xml
new file mode 100644
index 000000000..4220eae88
--- /dev/null
+++ b/jdbc/howto/caching/pom.xml
@@ -0,0 +1,30 @@
+
+ 4.0.0
+
+ spring-data-jdbc-how-to-caching
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-how-to
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - How to do caching
+ Sample project for Spring Data JDBC demonstrating how it can be used with Springs caching
+ abstraction.
+
+ https://spring.io/spring-data-jdbc
+ 2021
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+
+
+
diff --git a/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/CachingApplication.java b/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/CachingApplication.java
new file mode 100644
index 000000000..7d7c705c7
--- /dev/null
+++ b/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/CachingApplication.java
@@ -0,0 +1,15 @@
+package example.springdata.jdbc.howto.caching;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
+
+@EnableCaching
+@SpringBootApplication
+class CachingApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(CachingApplication.class, args);
+ }
+
+}
diff --git a/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/Minion.java b/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/Minion.java
new file mode 100644
index 000000000..841756e66
--- /dev/null
+++ b/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/Minion.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.caching;
+
+import org.springframework.data.annotation.Id;
+
+public class Minion {
+ @Id
+ Long id;
+ String name;
+
+ Minion(String name) {
+ this.name = name;
+ }
+
+ public Long getId(){
+ return id;
+ }
+}
diff --git a/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/MinionRepository.java b/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/MinionRepository.java
new file mode 100644
index 000000000..9b73b0cb3
--- /dev/null
+++ b/jdbc/howto/caching/src/main/java/example.springdata/jdbc/howto/caching/MinionRepository.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.caching;
+
+import java.util.Optional;
+
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.data.repository.CrudRepository;
+
+interface MinionRepository extends CrudRepository {
+
+ @Override
+ @CacheEvict(value="minions",beforeInvocation = false,key = "#result.id")
+ S save(S s);
+
+ @Override
+ @Cacheable("minions")
+ Optional findById(Long aLong);
+}
diff --git a/jdbc/howto/caching/src/main/resources/application.properties b/jdbc/howto/caching/src/main/resources/application.properties
new file mode 100644
index 000000000..813de42f6
--- /dev/null
+++ b/jdbc/howto/caching/src/main/resources/application.properties
@@ -0,0 +1 @@
+logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
\ No newline at end of file
diff --git a/jdbc/howto/caching/src/main/resources/schema.sql b/jdbc/howto/caching/src/main/resources/schema.sql
new file mode 100644
index 000000000..5e75d2286
--- /dev/null
+++ b/jdbc/howto/caching/src/main/resources/schema.sql
@@ -0,0 +1,5 @@
+CREATE TABLE MINION
+(
+ ID IDENTITY PRIMARY KEY,
+ NAME VARCHAR(255),
+);
diff --git a/jdbc/howto/caching/src/test/java/example/springdata/jdbc/howto/caching/CachingApplicationTests.java b/jdbc/howto/caching/src/test/java/example/springdata/jdbc/howto/caching/CachingApplicationTests.java
new file mode 100644
index 000000000..4043391fc
--- /dev/null
+++ b/jdbc/howto/caching/src/test/java/example/springdata/jdbc/howto/caching/CachingApplicationTests.java
@@ -0,0 +1,39 @@
+package example.springdata.jdbc.howto.caching;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class CachingApplicationTests {
+
+ private Long bobsId;
+ @Autowired MinionRepository minions;
+
+ @BeforeEach
+ void setup() {
+
+ Minion bob = minions.save(new Minion("Bob"));
+ bobsId = bob.id;
+ }
+
+ @Test
+ void saveloadMultipleTimes() {
+
+ Optional bob = null;
+ for (int i = 0; i < 10; i++) {
+ bob = minions.findById(bobsId);
+ }
+
+ minions.save(bob.get());
+
+ for (int i = 0; i < 10; i++) {
+ bob = minions.findById(bobsId);
+ }
+
+ }
+
+}
diff --git a/jdbc/howto/idgeneration/README.adoc b/jdbc/howto/idgeneration/README.adoc
new file mode 100644
index 000000000..57a7f61c2
--- /dev/null
+++ b/jdbc/howto/idgeneration/README.adoc
@@ -0,0 +1,24 @@
+== Spring Data JDBC How To ID Generation
+
+There are multiple ways how the generation of IDs can be controlled.
+
+1. The default is to let the database create the ID by using a `AUTOINCREMENT`, `SERIAL` or `IDENTITY` column.
+ If you try to save a new aggregate root with a preset ID you will receive an exception.
+ See `IdGenerationApplicationTest.cantSaveNewAggregateWithPresetId`.
+
+ The reason is that Spring Data JDBC will inspect the aggregate root, notes that the ID is not `null` and tries to perform an update which will update 0 rows and cause an exception.
+
+2. You can manually set the id of an aggregate root to a value of your choice if you use `JdbcAggregateTemplate.insert`.
+ This bypasses the check if an update or insert is to be performed and always performs an insert.
+ See `IdGenerationApplicationTest.insertNewAggregateWithPresetIdUsingTemplate`.
+
+3. You may use a `BeforeSaveEntityCallBack` to set the id of aggregate roots with null ID.
+ This has the benefit of being transparent in to your domain code, as it should be since IDs are normally not relevant to it.
+ See `IdGenerationApplicationTest.idByCallBack` and `IdGenerationApplication.beforeSaveCallback`.
+ As long as your entity is mutable you might as well use an `BeforeSaveEntityListener`, but since the callback works for both cases it is the recommended approach.
+
+4. If you add a version attribute, i.e. one annotated with `@Version` that attribute is used to determine if the aggregate is new or not, leaving you free to set the ID as you see fit.
+ See `IdGenerationApplicationTest.determineIsNewPerVersion`.
+
+5. The final option is to let your aggregate root implement `Persistable` which allows you to define your own `isNew` method, which controls if we perform an insert or an update.
+ See `IdGenerationApplicationTest.determineIsNewPerPersistable`.
diff --git a/jdbc/howto/idgeneration/pom.xml b/jdbc/howto/idgeneration/pom.xml
new file mode 100644
index 000000000..9ac1666e7
--- /dev/null
+++ b/jdbc/howto/idgeneration/pom.xml
@@ -0,0 +1,20 @@
+
+ 4.0.0
+
+ spring-data-jdbc-how-to-id-generation
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-how-to
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - How to ID generation
+ Sample project for Spring Data JDBC demonstrating the various options to use user defined IDs in Spring Data JDBC aggregates.
+ It serves as a source code repository for a How-To article on the Spring Blog
+ https://spring.io/spring-data-jdbc
+ 2021
+
+
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/IdGenerationApplication.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/IdGenerationApplication.java
new file mode 100644
index 000000000..4b75d8d34
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/IdGenerationApplication.java
@@ -0,0 +1,28 @@
+package example.springdata.jdbc.howto.idgeneration;
+
+import java.util.UUID;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
+
+@SpringBootApplication
+class IdGenerationApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(IdGenerationApplication.class, args);
+ }
+
+ @Bean
+ BeforeConvertCallback beforeSaveCallback() {
+
+ return (minion) -> {
+ if (minion.id == null) {
+ minion.id = UUID.randomUUID().toString();
+ }
+ return minion;
+ };
+ }
+
+}
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/Minion.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/Minion.java
new file mode 100644
index 000000000..bbd921cf7
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/Minion.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import org.springframework.data.annotation.Id;
+
+class Minion {
+ @Id
+ Long id;
+ String name;
+
+ Minion(String name) {
+ this.name = name;
+ }
+}
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/MinionRepository.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/MinionRepository.java
new file mode 100644
index 000000000..1a2ee77fd
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/MinionRepository.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import org.springframework.data.repository.CrudRepository;
+
+interface MinionRepository extends CrudRepository {
+
+}
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinion.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinion.java
new file mode 100644
index 000000000..9b3a88af0
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinion.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.domain.Persistable;
+import org.springframework.data.relational.core.mapping.Table;
+
+@Table("MINION")
+class PersistableMinion implements Persistable {
+ @Id Long id;
+ String name;
+
+ PersistableMinion(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @Override
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public boolean isNew() {
+ // this implementation is most certainly not suitable for production use
+ return true;
+ }
+}
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinionRepository.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinionRepository.java
new file mode 100644
index 000000000..69311209b
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinionRepository.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import org.springframework.data.repository.CrudRepository;
+
+interface PersistableMinionRepository extends CrudRepository {
+
+}
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinion.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinion.java
new file mode 100644
index 000000000..98bf7c5d2
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinion.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import org.springframework.data.annotation.Id;
+
+class StringIdMinion {
+ @Id
+ String id;
+ String name;
+
+ StringIdMinion(String name) {
+ this.name = name;
+ }
+}
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinionRepository.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinionRepository.java
new file mode 100644
index 000000000..884ee86d8
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinionRepository.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import org.springframework.data.repository.CrudRepository;
+
+interface StringIdMinionRepository extends CrudRepository {
+
+}
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinion.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinion.java
new file mode 100644
index 000000000..7104bfe8f
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinion.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Version;
+
+class VersionedMinion {
+
+ @Id Long id;
+ String name;
+ @Version Integer version;
+
+ VersionedMinion(long id, String name) {
+
+ this.id = id;
+ this.name = name;
+ }
+}
diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinionRepository.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinionRepository.java
new file mode 100644
index 000000000..778e06f58
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinionRepository.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import org.springframework.data.repository.CrudRepository;
+
+interface VersionedMinionRepository extends CrudRepository {
+
+}
diff --git a/jdbc/howto/idgeneration/src/main/resources/schema.sql b/jdbc/howto/idgeneration/src/main/resources/schema.sql
new file mode 100644
index 000000000..42496f025
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/main/resources/schema.sql
@@ -0,0 +1,18 @@
+CREATE TABLE MINION
+(
+ ID IDENTITY PRIMARY KEY,
+ NAME VARCHAR(255),
+);
+
+CREATE TABLE STRING_ID_MINION
+(
+ ID VARCHAR(255) PRIMARY KEY,
+ NAME VARCHAR(255)
+);
+
+CREATE TABLE VERSIONED_MINION
+(
+ ID INT PRIMARY KEY,
+ NAME VARCHAR(255),
+ VERSION INT
+);
diff --git a/jdbc/howto/idgeneration/src/test/java/example/springdata/jdbc/howto/idgeneration/IdGenerationApplicationTests.java b/jdbc/howto/idgeneration/src/test/java/example/springdata/jdbc/howto/idgeneration/IdGenerationApplicationTests.java
new file mode 100644
index 000000000..de0ef9fde
--- /dev/null
+++ b/jdbc/howto/idgeneration/src/test/java/example/springdata/jdbc/howto/idgeneration/IdGenerationApplicationTests.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.idgeneration;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest;
+import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
+import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
+
+@DataJdbcTest
+class IdGenerationApplicationTests {
+
+ @Autowired
+ MinionRepository minions;
+
+ @Autowired
+ StringIdMinionRepository stringions;
+
+ @Autowired
+ VersionedMinionRepository versionedMinions;
+
+ @Autowired
+ PersistableMinionRepository persistableMinions;
+
+ @Autowired
+ JdbcAggregateTemplate template;
+
+ @Test
+ void saveWithNewIdFromDb() {
+
+ Minion before = new Minion("Bob");
+ assertThat(before.id).isNull();
+
+ Minion after = minions.save(before);
+
+ assertThat(after.id).isNotNull();
+ }
+
+ @Test
+ void cantSaveNewAggregateWithPresetId() {
+
+ Minion before = new Minion("Stuart");
+ before.id = 42L;
+
+ // We can't save this because Spring Data JDBC thinks it has to do an update.
+ assertThatThrownBy(() -> minions.save(before)).isInstanceOf(IncorrectUpdateSemanticsDataAccessException.class);
+ }
+
+ @Test
+ void insertNewAggregateWithPresetIdUsingTemplate() {
+
+ Minion before = new Minion("Stuart");
+ before.id = 42L;
+
+ template.insert(before);
+
+ Minion reloaded = minions.findById(42L).get();
+ assertThat(reloaded.name).isEqualTo("Stuart");
+ }
+
+ @Test
+ void idByCallBack() {
+
+ StringIdMinion before = new StringIdMinion("Kevin");
+
+ stringions.save(before);
+
+ assertThat(before.id).isNotNull();
+
+ StringIdMinion reloaded = stringions.findById(before.id).get();
+ assertThat(reloaded.name).isEqualTo("Kevin");
+ }
+
+ @Test
+ void determineIsNewPerVersion() {
+
+ VersionedMinion before = new VersionedMinion(23L, "Bob");
+
+ assertThat(before.id).isNotNull();
+
+ versionedMinions.save(before);
+
+ // It's saved!
+ VersionedMinion reloaded = versionedMinions.findById(before.id).get();
+ assertThat(reloaded.name).isEqualTo("Bob");
+ }
+
+ @Test
+ void determineIsNewPerPersistable() {
+
+ PersistableMinion before = new PersistableMinion(23L, "Dave");
+
+ persistableMinions.save(before);
+
+ // It's saved!
+ PersistableMinion reloaded = persistableMinions.findById(before.id).get();
+ assertThat(reloaded.name).isEqualTo("Dave");
+ }
+}
diff --git a/jdbc/howto/pom.xml b/jdbc/howto/pom.xml
new file mode 100644
index 000000000..fcdff5dbd
--- /dev/null
+++ b/jdbc/howto/pom.xml
@@ -0,0 +1,30 @@
+
+ 4.0.0
+
+ spring-data-jdbc-how-to
+ pom
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-examples
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - How to Examples
+ Sample projects for Spring Data JDBC demonstrating various features and options.
+ It serves as a source code repository for a series of How To articles on the Spring Blog
+ https://spring.io/spring-data-jdbc
+ 2021
+
+
+ bidirectionalexternal
+ bidirectionalinternal
+ caching
+ idgeneration
+ selectiveupdate
+ schema-generation
+
+
+
diff --git a/jdbc/howto/schema-generation/.gitignore b/jdbc/howto/schema-generation/.gitignore
new file mode 100644
index 000000000..a718e9947
--- /dev/null
+++ b/jdbc/howto/schema-generation/.gitignore
@@ -0,0 +1 @@
+cs-*.yaml
\ No newline at end of file
diff --git a/jdbc/howto/schema-generation/README.adoc b/jdbc/howto/schema-generation/README.adoc
new file mode 100644
index 000000000..3f27e40fe
--- /dev/null
+++ b/jdbc/howto/schema-generation/README.adoc
@@ -0,0 +1,5 @@
+== Spring Data JDBC How To Generate the Database Schema.
+
+Spring Data JDBC offers an API to generate Liquibase Changesets from your domain model and optionally an existing database.
+
+This project demonstrates how to do this.
diff --git a/jdbc/howto/schema-generation/pom.xml b/jdbc/howto/schema-generation/pom.xml
new file mode 100644
index 000000000..73e8ad543
--- /dev/null
+++ b/jdbc/howto/schema-generation/pom.xml
@@ -0,0 +1,29 @@
+
+ 4.0.0
+
+ spring-data-jdbc-how-to-schema-generation
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-how-to
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - How to do schema generation
+ Sample project for Spring Data JDBC demonstrating how it can be used to generate and update your
+ schema.
+
+ https://spring.io/spring-data-jdbc
+ 2023
+
+
+
+
+ org.liquibase
+ liquibase-core
+
+
+
+
diff --git a/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Minion.java b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Minion.java
new file mode 100644
index 000000000..0ac2e9fbf
--- /dev/null
+++ b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Minion.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.caching;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Table;
+
+
+/**
+ * Entity we use as a fixture for demonstrating Schema Generation.
+ * See SchemaGenerationTest in the test directory for the interesting stuff.
+ *
+ * @author Jens Schauder
+ * @since 3.2
+ */
+@Table
+class Minion {
+
+ @Id
+ Long id;
+
+ @Name
+ String name;
+
+ Minion(String name) {
+ this.name = name;
+ }
+
+ public Long getId() {
+ return id;
+ }
+}
diff --git a/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Name.java b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Name.java
new file mode 100644
index 000000000..91d4158fb
--- /dev/null
+++ b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Name.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.caching;
+
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Annotation to mark a property as a Name which should have a very specify database type.
+ *
+ * @author Jens Schauder
+ */
+@Varchar(20)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Name {
+}
diff --git a/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/SchemaGenerationExampleApplication.java b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/SchemaGenerationExampleApplication.java
new file mode 100644
index 000000000..3fd9634d5
--- /dev/null
+++ b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/SchemaGenerationExampleApplication.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.caching;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * Boot application that we use as a fixture for demonstrating Schema Generation.
+ * See SchemaGenerationTest in the test directory for the interesting stuff.
+ *
+ * @author Jens Schauder
+ * @since 3.2
+ */
+@SpringBootApplication
+class SchemaGenerationExampleApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SchemaGenerationExampleApplication.class, args);
+ }
+
+}
diff --git a/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Varchar.java b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Varchar.java
new file mode 100644
index 000000000..626af7e15
--- /dev/null
+++ b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Varchar.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.caching;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Marks a property as to be represented by a VARCHAR database type.
+ *
+ * @author Jens Schauder
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Varchar {
+
+ /**
+ * the size of the varchar.
+ */
+ int value();
+}
diff --git a/jdbc/howto/schema-generation/src/main/resources/application.properties b/jdbc/howto/schema-generation/src/main/resources/application.properties
new file mode 100644
index 000000000..07ddc16f1
--- /dev/null
+++ b/jdbc/howto/schema-generation/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+# Disable Liquibase when you don't have changesets configured yet.
+# spring.liquibase.enabled=false
\ No newline at end of file
diff --git a/jdbc/howto/schema-generation/src/main/resources/db/changelog/db.changelog-master.yaml b/jdbc/howto/schema-generation/src/main/resources/db/changelog/db.changelog-master.yaml
new file mode 100644
index 000000000..615ec854d
--- /dev/null
+++ b/jdbc/howto/schema-generation/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -0,0 +1,32 @@
+databaseChangeLog:
+- changeSet:
+ id: '1692725820135'
+ author: Spring Data Relational
+ objectQuotingStrategy: LEGACY
+ changes:
+ - createTable:
+ columns:
+ - column:
+ autoIncrement: true
+ constraints:
+ nullable: true
+ primaryKey: true
+ name: id
+ type: BIGINT
+ - column:
+ constraints:
+ nullable: true
+ name: firstname
+ type: VARCHAR(255 BYTE)
+ - column:
+ constraints:
+ nullable: true
+ name: lastname
+ type: VARCHAR(255 BYTE)
+ - column:
+ constraints:
+ nullable: true
+ name: special
+ type: VARCHAR(255 BYTE)
+ tableName: minion
+
diff --git a/jdbc/howto/schema-generation/src/test/java/example/springdata/jdbc/howto/caching/SchemaGenerationTest.java b/jdbc/howto/schema-generation/src/test/java/example/springdata/jdbc/howto/caching/SchemaGenerationTest.java
new file mode 100644
index 000000000..1ca47de31
--- /dev/null
+++ b/jdbc/howto/schema-generation/src/test/java/example/springdata/jdbc/howto/caching/SchemaGenerationTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.caching;
+
+import liquibase.database.Database;
+import liquibase.database.core.HsqlDatabase;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.LiquibaseException;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Collections;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.data.jdbc.core.mapping.schema.DefaultSqlTypeMapping;
+import org.springframework.data.jdbc.core.mapping.schema.LiquibaseChangeSetWriter;
+import org.springframework.data.jdbc.core.mapping.schema.SqlTypeMapping;
+import org.springframework.data.relational.core.mapping.RelationalMappingContext;
+
+/**
+ * Example code demonstrating how to use the Schema Generation Feature.
+ *
+ * @author Jens Schauder
+ * @since 3.2
+ */
+@DataJdbcTest
+class SchemaGenerationTest {
+
+ @Autowired
+ RelationalMappingContext context;
+
+ @Autowired
+ DataSource ds;
+
+ @Test
+ void minimumExample() throws IOException {
+
+ // the change set will get appended, so we delete any pre existing file.
+ new File("cs-minimum.yaml").delete();
+
+ context.setInitialEntitySet(Collections.singleton(Minion.class));
+ LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context);
+
+ writer.writeChangeSet(new FileSystemResource("cs-minimum.yaml"));
+ }
+
+
+ @Test
+ void diffing() {
+
+ // the change set will get appended, so we delete any pre existing file.
+ new File("cs-diff.yaml").delete();
+
+ context.setInitialEntitySet(Collections.singleton(Minion.class));
+ LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context);
+
+ // drop unused columns
+ writer.setDropColumnFilter((table, column) -> !column.equalsIgnoreCase("special"));
+
+ // for comparison with existing schema
+ try (Database db = new HsqlDatabase()) {
+
+ db.setConnection(new JdbcConnection(ds.getConnection()));
+
+ writer.writeChangeSet(new FileSystemResource("cs-diff.yaml"), db);
+
+ } catch (IOException | SQLException | LiquibaseException e) {
+ throw new RuntimeException("Changeset generation failed", e);
+ }
+ }
+
+ @Test
+ void customizingTypes() throws IOException {
+
+ // the change set will get appended, so we delete any pre existing file.
+ new File("cs-custom.yaml").delete();
+
+ context.setInitialEntitySet(Collections.singleton(Minion.class));
+ LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context);
+
+ writer.setSqlTypeMapping(((SqlTypeMapping) property -> {
+ if (property.getName().equalsIgnoreCase("name")) {
+ return "VARCHAR(500)";
+ }
+ return null;
+ }).and(new DefaultSqlTypeMapping()));
+
+ writer.writeChangeSet(new FileSystemResource("cs-custom.yaml"));
+
+ }
+
+ @Test
+ void customizingTypesUsingAnnotations() throws IOException {
+
+ // the change set will get appended, so we delete any pre existing file.
+ new File("cs-annotation.yaml").delete();
+
+ context.setInitialEntitySet(Collections.singleton(Minion.class));
+ LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context);
+
+ writer.setSqlTypeMapping(((SqlTypeMapping) property -> {
+
+ if (!property.getType().equals(String.class)) {
+ return null;
+ }
+
+ // findAnnotation will find meta annotations
+ Varchar varchar = property.findAnnotation(Varchar.class);
+ int value = varchar.value();
+
+ if (varchar == null) {
+ return null;
+ }
+ return "VARCHAR(" +
+ varchar.value() +
+ ")";
+
+ }).and(new DefaultSqlTypeMapping()));
+
+ writer.writeChangeSet(new FileSystemResource("cs-annotation.yaml"));
+
+ }
+}
diff --git a/jdbc/howto/selectiveupdate/README.adoc b/jdbc/howto/selectiveupdate/README.adoc
new file mode 100644
index 000000000..f3d6335ef
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/README.adoc
@@ -0,0 +1,5 @@
+== Spring Data JDBC How To perform selective updates.
+
+Spring Data JDBC normally persists complete aggregates, which is wasteful if only few things have changed.
+
+This project demonstrates alternatives that require a little more work from the developer but are much more efficient.
diff --git a/jdbc/howto/selectiveupdate/pom.xml b/jdbc/howto/selectiveupdate/pom.xml
new file mode 100644
index 000000000..8ea758760
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/pom.xml
@@ -0,0 +1,20 @@
+
+ 4.0.0
+
+ spring-data-jdbc-how-to-selective-update
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-how-to
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - How to do selective updates
+ Sample project for Spring Data JDBC demonstrating how to update only parts of an aggregate.
+
+ https://spring.io/spring-data-jdbc
+ 2022
+
+
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Color.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Color.java
new file mode 100644
index 000000000..399e66d8d
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Color.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+/**
+ * Simple enum for the color of a minion.
+ *
+ * @author Jens Schauder
+ */
+public enum Color {
+ YELLOW, PURPLE
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Minion.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Minion.java
new file mode 100644
index 000000000..cf8ed6d96
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Minion.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceCreator;
+import org.springframework.data.annotation.Version;
+
+
+/**
+ * A minion. The main entity and aggregate root for this example.
+ *
+ * @author Jens Schauder
+ */
+class Minion {
+
+ @Id Long id;
+ String name;
+ Color color = Color.YELLOW;
+ Set toys = new HashSet<>();
+ @Version int version;
+
+ Minion(String name) {
+ this.name = name;
+ }
+
+ @PersistenceCreator
+ private Minion(Long id, String name, Collection toys, int version) {
+
+ this.id = id;
+ this.name = name;
+ this.toys.addAll(toys);
+ this.version = version;
+ }
+
+ Minion addToy(Toy toy) {
+
+ toys.add(toy);
+ return this;
+ }
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/MinionRepository.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/MinionRepository.java
new file mode 100644
index 000000000..0c97d7e8e
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/MinionRepository.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+import org.springframework.data.jdbc.repository.query.Modifying;
+import org.springframework.data.jdbc.repository.query.Query;
+import org.springframework.data.repository.CrudRepository;
+
+
+/**
+ * The normal MinionRepository.
+ *
+ * @author Jens Schauder
+ */
+interface MinionRepository extends CrudRepository, PartyHatRepository {
+
+ @Modifying
+ @Query("UPDATE MINION SET COLOR ='PURPLE', VERSION = VERSION +1 WHERE ID = :id")
+ void turnPurple(Long id);
+
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepository.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepository.java
new file mode 100644
index 000000000..f2b3a4974
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+
+/**
+ * A repository fragment to perform custom logic, including a special way to update minions in the database.
+ *
+ * @author Jens Schauder
+ */
+public interface PartyHatRepository {
+
+ void addPartyHat(Minion minion);
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepositoryImpl.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepositoryImpl.java
new file mode 100644
index 000000000..b0f822c50
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepositoryImpl.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+import org.springframework.dao.OptimisticLockingFailureException;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of custom logic for a minion repository.
+ *
+ * @author Jens Schauder
+ */
+class PartyHatRepositoryImpl implements PartyHatRepository {
+
+
+ private final NamedParameterJdbcOperations template;
+
+ public PartyHatRepositoryImpl(NamedParameterJdbcOperations template) {
+ this.template = template;
+ }
+
+ @Transactional
+ @Override
+ public void addPartyHat(Minion minion) {
+
+ Map insertParams = new HashMap<>();
+ insertParams.put("id", minion.id);
+ insertParams.put("name", "Party Hat");
+ template.update("INSERT INTO TOY (MINION, NAME) VALUES (:id, :name)", insertParams);
+
+ Map updateParams = new HashMap<>();
+ updateParams.put("id", minion.id);
+ updateParams.put("version", minion.version);
+ final int updateCount = template.update("UPDATE MINION SET VERSION = :version + 1 WHERE ID = :id AND VERSION = :version", updateParams);
+ if (updateCount != 1) {
+ throw new OptimisticLockingFailureException("Minion was changed before a Party Hat was given");
+ }
+ }
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinion.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinion.java
new file mode 100644
index 000000000..4162c4e0b
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinion.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Version;
+import org.springframework.data.relational.core.mapping.Table;
+
+/**
+ * A simplified minion which only has the attributes
+ *
+ * @author Jens Schauder
+ */
+@Table("MINION")
+class PlainMinion {
+ @Id Long id;
+ String name;
+ Color color;
+ @Version int version;
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinionRepository.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinionRepository.java
new file mode 100644
index 000000000..7d8d8be23
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinionRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Repository for {@link PlainMinion}.
+ *
+ * @author Jens Schauder
+ */
+interface PlainMinionRepository extends CrudRepository {
+
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplication.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplication.java
new file mode 100644
index 000000000..d1e005f01
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplication.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class SelectiveUpdateApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SelectiveUpdateApplication.class, args);
+ }
+
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Toy.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Toy.java
new file mode 100644
index 000000000..c61a32e52
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Toy.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+import java.util.Objects;
+
+/**
+ * Toys for minions to play with.
+ *
+ * @author Jens Schauder
+ */
+class Toy {
+ String name;
+
+ Toy(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Toy toy = (Toy) o;
+ return Objects.equals(name, toy.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+}
diff --git a/jdbc/howto/selectiveupdate/src/main/resources/application.properties b/jdbc/howto/selectiveupdate/src/main/resources/application.properties
new file mode 100644
index 000000000..813de42f6
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/resources/application.properties
@@ -0,0 +1 @@
+logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
\ No newline at end of file
diff --git a/jdbc/howto/selectiveupdate/src/main/resources/schema.sql b/jdbc/howto/selectiveupdate/src/main/resources/schema.sql
new file mode 100644
index 000000000..4ebd97e41
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/main/resources/schema.sql
@@ -0,0 +1,13 @@
+CREATE TABLE MINION
+(
+ ID IDENTITY PRIMARY KEY,
+ NAME VARCHAR(255),
+ COLOR VARCHAR(10),
+ VERSION INT
+);
+
+CREATE TABLE TOY
+(
+ MINION BIGINT NOT NULL,
+ NAME VARCHAR(255)
+);
diff --git a/jdbc/howto/selectiveupdate/src/test/java/example/springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplicationTests.java b/jdbc/howto/selectiveupdate/src/test/java/example/springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplicationTests.java
new file mode 100644
index 000000000..6097e0bf9
--- /dev/null
+++ b/jdbc/howto/selectiveupdate/src/test/java/example/springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplicationTests.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.howto.selectiveupdate;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest;
+import org.springframework.dao.OptimisticLockingFailureException;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Tests demonstrating various was for partial updates of an aggregate.
+ */
+@DataJdbcTest
+class SelectiveUpdateApplicationTests {
+
+ @Autowired MinionRepository minions;
+ @Autowired PlainMinionRepository plainMinions;
+
+ @Test
+ void renameWithReducedView() {
+
+ Minion bob = new Minion("Bob").addToy(new Toy("Tiger Duck")).addToy(new Toy("Security blanket"));
+ minions.save(bob);
+
+ PlainMinion plainBob = plainMinions.findById(bob.id).orElseThrow();
+ plainBob.name = "Bob II.";
+ plainMinions.save(plainBob);
+
+ Minion bob2 = minions.findById(bob.id).orElseThrow();
+
+ assertThat(bob2.toys).containsExactly(bob.toys.toArray(new Toy[] {}));
+ assertThat(bob2.name).isEqualTo("Bob II.");
+ assertThat(bob2.color).isEqualTo(Color.YELLOW);
+ }
+
+ @Test
+ void turnPurpleByDirectUpdate() {
+
+ Minion bob = new Minion("Bob").addToy(new Toy("Tiger Duck")).addToy(new Toy("Security blanket"));
+ minions.save(bob);
+
+ minions.turnPurple(bob.id);
+
+ Minion bob2 = minions.findById(bob.id).orElseThrow();
+
+ assertThat(bob2.toys).containsExactlyElementsOf(bob.toys);
+ assertThat(bob2.name).isEqualTo("Bob");
+ assertThat(bob2.color).isEqualTo(Color.PURPLE);
+ }
+
+ @Test
+ void grantPartyHat() {
+
+ Minion bob = new Minion("Bob").addToy(new Toy("Tiger Duck")).addToy(new Toy("Security blanket"));
+ minions.save(bob);
+
+ minions.addPartyHat(bob);
+
+ Minion bob2 = minions.findById(bob.id).orElseThrow();
+
+ assertThat(bob2.toys).extracting("name").containsExactlyInAnyOrder("Tiger Duck", "Security blanket", "Party Hat");
+ assertThat(bob2.name).isEqualTo("Bob");
+ assertThat(bob2.color).isEqualTo(Color.YELLOW);
+ assertThat(bob2.version).isEqualTo(bob.version + 1);
+
+ assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> minions.addPartyHat(bob));
+ }
+
+ @Test
+ @Transactional(propagation = Propagation.NOT_SUPPORTED)
+ void cannotGrantPartyHatWhenOutOfSync() {
+
+ Minion bob = new Minion("Bob").addToy(new Toy("Tiger Duck")).addToy(new Toy("Security blanket"));
+ minions.save(bob);
+ minions.turnPurple(bob.id);
+
+ assertThat(bob.color).isEqualTo(Color.YELLOW);
+ assertThat(bob.version).isOne();
+ assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> minions.addPartyHat(bob));
+
+ Minion bob2 = minions.findById(bob.id).orElseThrow();
+
+ assertThat(bob2.name).isEqualTo("Bob");
+ assertThat(bob2.color).isEqualTo(Color.PURPLE);
+ assertThat(bob2.version).isEqualTo(bob.version + 1);
+ assertThat(bob2.toys).extracting("name").containsExactlyInAnyOrder("Tiger Duck", "Security blanket");
+ }
+
+}
diff --git a/jdbc/immutables/README.adoc b/jdbc/immutables/README.adoc
new file mode 100644
index 000000000..dcc5a1b93
--- /dev/null
+++ b/jdbc/immutables/README.adoc
@@ -0,0 +1,27 @@
+== Spring Data JDBC with Immutables
+
+This example shows how to use https://immutables.github.io/[Immutables] with Spring Data.
+The core concept of Immutables is to define an interface (or abstract class) for which Immutables generates an immutable implementation that can then be used by application code.
+
+Persisting immutable objects and associating the saved object with generated identifiers works out of the box.
+The reading side requires a redirection of the to be created object type, see `ImmutablesJdbcConfiguration`.
+
+Limitations:
+
+Immutables tends to generate additional constructors when using `@Value.Style(allParameters = true)` or `@Value.Parameter`.
+This conflicts with Spring Data's constructor resolution as Spring Data cannot identify reliably a persistence constructor.
+
+To run the tests, run:
+
+[indent=0]
+----
+ $ mvn test
+----
+
+The code generator is automatically run when executing the tests.
+If you want to rerun the code generator manually, just run the following command:
+
+[indent=0]
+----
+ $ mvn clean generate-sources
+----
diff --git a/jdbc/immutables/pom.xml b/jdbc/immutables/pom.xml
new file mode 100644
index 000000000..621e3caf0
--- /dev/null
+++ b/jdbc/immutables/pom.xml
@@ -0,0 +1,50 @@
+
+ 4.0.0
+
+ spring-data-jdbc-immutables
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-examples
+ 4.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ Spring Data JDBC - Usage with Immutables
+ Sample project demonstrating Spring Data JDBC features
+
+
+ 2.8.8
+
+
+
+
+ org.immutables
+ value
+ ${immutables.version}
+ provided
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.immutables
+ value
+ ${immutables.version}
+
+
+
+
+
+
+
+
diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java
new file mode 100644
index 000000000..47f456eb5
--- /dev/null
+++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.immutables;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory;
+import org.springframework.data.jdbc.core.convert.JdbcConverter;
+import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
+import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
+import org.springframework.data.jdbc.core.convert.RelationResolver;
+import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
+import org.springframework.data.relational.core.conversion.RowDocumentAccessor;
+import org.springframework.data.relational.core.dialect.Dialect;
+import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Configuration stub.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootApplication
+class Application {
+
+ /**
+ * Name scheme how Immutables generates implementations from interface/class definitions.
+ */
+ public static final String IMMUTABLE_IMPLEMENTATION_CLASS = "%s.Immutable%s";
+
+ @Configuration
+ static class ImmutablesJdbcConfiguration extends AbstractJdbcConfiguration {
+
+ private final ResourceLoader resourceLoader;
+
+ public ImmutablesJdbcConfiguration(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+ /**
+ * {@link JdbcConverter} that redirects entities to be instantiated towards the implementation. See
+ * {@link #IMMUTABLE_IMPLEMENTATION_CLASS} and
+ * {@link #getImplementationEntity(JdbcMappingContext, RelationalPersistentEntity)}.
+ *
+ * @param mappingContext
+ * @param operations
+ * @param relationResolver
+ * @param conversions
+ * @param dialect
+ * @return
+ */
+ @Override
+ public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext, NamedParameterJdbcOperations operations,
+ @Lazy RelationResolver relationResolver, JdbcCustomConversions conversions, Dialect dialect) {
+
+ var jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations());
+
+ return new MappingJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory) {
+
+ @Override
+ @SuppressWarnings("all")
+ protected S readAggregate(ConversionContext context, RowDocumentAccessor documentAccessor,
+ TypeInformation extends S> typeHint) {
+
+ RelationalPersistentEntity> implementationEntity = getImplementationEntity(mappingContext,
+ mappingContext.getRequiredPersistentEntity(typeHint));
+
+ return (S) super.readAggregate(context, documentAccessor, implementationEntity.getTypeInformation());
+ }
+ };
+ }
+
+ /**
+ * Returns if the entity passed as an argument is an interface the implementation provided by Immutable is provided
+ * instead. In all other cases the entity passed as an argument is returned.
+ */
+ @SuppressWarnings("unchecked")
+ private RelationalPersistentEntity getImplementationEntity(JdbcMappingContext mappingContext,
+ RelationalPersistentEntity entity) {
+
+ Class type = entity.getType();
+ if (type.isInterface()) {
+
+ var immutableClass = String.format(IMMUTABLE_IMPLEMENTATION_CLASS, type.getPackageName(), type.getSimpleName());
+ if (ClassUtils.isPresent(immutableClass, resourceLoader.getClassLoader())) {
+
+ return (RelationalPersistentEntity) mappingContext
+ .getPersistentEntity(ClassUtils.resolveClassName(immutableClass, resourceLoader.getClassLoader()));
+ }
+
+ }
+ return entity;
+ }
+ }
+
+}
diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java
new file mode 100644
index 000000000..f927fb1ae
--- /dev/null
+++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.immutables;
+
+import java.util.List;
+
+import org.immutables.value.Value;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.MappedCollection;
+import org.springframework.data.relational.core.mapping.Table;
+import org.springframework.lang.Nullable;
+
+/**
+ * @author Mark Paluch
+ */
+@Value.Immutable
+@Table("ENIGMA")
+public interface Enigma {
+
+ @Nullable
+ @Id
+ Long getId();
+
+ String getModel();
+
+ // Explicit keys to not derive key names from the implementation class.
+ @MappedCollection(idColumn = "ENIGMA_ID", keyColumn = "ROTOR_KEY")
+ List getRotors();
+
+}
diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java
new file mode 100644
index 000000000..51611fe45
--- /dev/null
+++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.immutables;
+
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Repository for {@link Enigma} instances.
+ *
+ * @author Mark Paluch
+ */
+public interface EnigmaRepository extends CrudRepository {
+
+ Enigma findByModel(String name);
+}
diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java
new file mode 100644
index 000000000..6f71ebd1f
--- /dev/null
+++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.immutables;
+
+import org.immutables.value.Value;
+
+import org.springframework.data.relational.core.mapping.Table;
+
+/**
+ * @author Mark Paluch
+ */
+@Value.Immutable
+@Table("ROTOR")
+public interface Rotor {
+
+ String getName();
+
+ String getWiring();
+
+ char getNotch();
+
+ /**
+ * Factory method for {@link Rotor} as using {@code @Value.Style} and {@code @Value.Parameter} conflicts with Spring
+ * Data's constructor discovery rules.
+ *
+ * @param name The name of the rotor, just a label to distinguish them.
+ * @param wiring A String consisting of all letters of the alphabet encoding which input letter is connected to which output letter.
+ * @param notch The current position of the rotor.
+ * @return a newly created Rotor.
+ */
+ static Rotor of(String name, String wiring, char notch) {
+ return ImmutableRotor.builder().name(name).wiring(wiring).notch(notch).build();
+ }
+
+}
diff --git a/jdbc/immutables/src/main/resources/schema.sql b/jdbc/immutables/src/main/resources/schema.sql
new file mode 100644
index 000000000..7a5faaf8c
--- /dev/null
+++ b/jdbc/immutables/src/main/resources/schema.sql
@@ -0,0 +1,14 @@
+CREATE TABLE IF NOT EXISTS ENIGMA
+(
+ ID INTEGER IDENTITY PRIMARY KEY,
+ MODEL VARCHAR(100)
+);
+
+CREATE TABLE IF NOT EXISTS ROTOR
+(
+ ENIGMA_ID INTEGER,
+ ROTOR_KEY INTEGER,
+ NAME VARCHAR(100),
+ WIRING VARCHAR(26),
+ NOTCH CHAR(1)
+);
diff --git a/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java b/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java
new file mode 100644
index 000000000..7e488a590
--- /dev/null
+++ b/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.immutables;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest;
+
+/**
+ * Integration tests using immutable types assisted by Immutables. Calling code can use the actual immutable
+ * interface/abstract class while Spring Data materializes the actual implementation.
+ *
+ * @author Mark Paluch
+ * @see https://immutables.github.io/
+ */
+@DataJdbcTest
+class ImmutablesTests {
+
+ @Autowired EnigmaRepository repository;
+
+ @Test
+ void shouldInsertAndRetrieveObject() {
+
+ var enigmaA = ImmutableEnigma.builder().model("A") //
+ .addRotors(Rotor.of("I", "DMTWSILRUYQNKFEJCAZBPGXOHV", 'Q')) //
+ .addRotors(Rotor.of("II", "HQZGPJTMOBLNCIFDYAWVEUSRXL", 'E')) //
+ .build();
+
+ var saved = repository.save(enigmaA);
+
+ assertThat(saved.getId()).isNotNull();
+
+ var loaded = repository.findByModel("A");
+
+ assertThat(loaded).isEqualTo(saved).isInstanceOf(ImmutableEnigma.class);
+ assertThat(loaded.getRotors()).hasSize(2);
+ assertThat(loaded.getRotors().get(0)).isInstanceOf(ImmutableRotor.class);
+ }
+}
diff --git a/jdbc/jmolecules/pom.xml b/jdbc/jmolecules/pom.xml
new file mode 100644
index 000000000..da1a75de4
--- /dev/null
+++ b/jdbc/jmolecules/pom.xml
@@ -0,0 +1,75 @@
+
+ 4.0.0
+
+
+ org.springframework.data.examples
+ spring-data-jdbc-examples
+ 4.0.0-SNAPSHOT
+
+
+ Spring Data JDBC - jMolecules Example
+ spring-data-jdbc-jmolecules
+
+
+
+
+ org.jmolecules
+ jmolecules-bom
+ 2023.0.0
+ pom
+ import
+
+
+
+
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+
+ org.jmolecules.integrations
+ jmolecules-spring
+
+
+
+ org.jmolecules
+ jmolecules-events
+
+
+
+ org.jmolecules.integrations
+ jmolecules-bytebuddy-nodep
+ provided
+
+
+
+
+
+
+
+ net.bytebuddy
+ byte-buddy-maven-plugin
+ ${byte-buddy.version}
+
+
+
+ transform-extended
+
+
+
+
+ true
+
+
+
+
+
+
diff --git a/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/Application.java b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/Application.java
new file mode 100644
index 000000000..4eaf25d34
--- /dev/null
+++ b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/Application.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.jmolecules;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Address.java b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Address.java
new file mode 100644
index 000000000..f6b68bbf1
--- /dev/null
+++ b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Address.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.jmolecules.customer;
+
+import org.jmolecules.ddd.types.ValueObject;
+
+public record Address(String street, String city, String zipCode) implements ValueObject {}
diff --git a/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Customer.java b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Customer.java
new file mode 100644
index 000000000..27d1a44fa
--- /dev/null
+++ b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Customer.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.jmolecules.customer;
+
+import example.springdata.jdbc.jmolecules.customer.Customer.CustomerId;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.jmolecules.ddd.types.AggregateRoot;
+import org.jmolecules.ddd.types.Identifier;
+import org.springframework.data.annotation.PersistenceCreator;
+import org.springframework.util.Assert;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@Getter
+@ToString
+@AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = @__(@PersistenceCreator))
+public class Customer implements AggregateRoot {
+
+ private final CustomerId id;
+ private @Setter String firstname, lastname;
+ private final List addresses;
+
+ public Customer(String firstname, String lastname, Address address) {
+
+ Assert.notNull(address, "Address must not be null!");
+
+ this.id = new CustomerId(UUID.randomUUID());
+
+ this.firstname = firstname;
+ this.lastname = lastname;
+
+ this.addresses = new ArrayList<>();
+ this.addresses.add(address);
+ }
+
+ public record CustomerId(UUID id) implements Identifier {}
+}
diff --git a/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Customers.java b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Customers.java
new file mode 100644
index 000000000..d7ae05635
--- /dev/null
+++ b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/customer/Customers.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.jmolecules.customer;
+
+import example.springdata.jdbc.jmolecules.customer.Customer.CustomerId;
+
+import org.jmolecules.ddd.types.Repository;
+import org.jmolecules.spring.AssociationResolver;
+
+/**
+ * @author Oliver Drotbohm
+ */
+public interface Customers extends Repository, AssociationResolver {
+ Customer save(Customer customer);
+}
diff --git a/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/LineItem.java b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/LineItem.java
new file mode 100644
index 000000000..73cfa60c0
--- /dev/null
+++ b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/LineItem.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.jmolecules.order;
+
+import org.jmolecules.ddd.types.ValueObject;
+
+public record LineItem(String description) implements ValueObject {}
diff --git a/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/Order.java b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/Order.java
new file mode 100644
index 000000000..2a377dc3e
--- /dev/null
+++ b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/Order.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.jmolecules.order;
+
+import example.springdata.jdbc.jmolecules.customer.Customer;
+import example.springdata.jdbc.jmolecules.customer.Customer.CustomerId;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.jmolecules.ddd.types.AggregateRoot;
+import org.jmolecules.ddd.types.Association;
+import org.jmolecules.ddd.types.Identifier;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@Table("MY_ORDER")
+@Getter
+@ToString
+@RequiredArgsConstructor
+public class Order implements AggregateRoot {
+
+ private final OrderId id;
+ private @Column("FOO") List lineItems;
+ private Association customer;
+
+ public Order(Customer customer) {
+
+ this.id = new OrderId(UUID.randomUUID());
+ this.customer = Association.forAggregate(customer);
+ this.lineItems = new ArrayList<>();
+ }
+
+ public Order addLineItem(String description) {
+
+ var item = new LineItem(description);
+
+ this.lineItems.add(item);
+
+ return this;
+ }
+
+ public record OrderId(UUID id) implements Identifier {}
+}
diff --git a/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/Orders.java b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/Orders.java
new file mode 100644
index 000000000..0f178d637
--- /dev/null
+++ b/jdbc/jmolecules/src/main/java/example/springdata/jdbc/jmolecules/order/Orders.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.jmolecules.order;
+
+import example.springdata.jdbc.jmolecules.order.Order.OrderId;
+
+import org.jmolecules.ddd.types.Repository;
+import org.jmolecules.spring.AssociationResolver;
+
+/**
+ * @author Oliver Drotbohm
+ */
+public interface Orders extends Repository, AssociationResolver {
+ Order save(Order order);
+}
diff --git a/jdbc/jmolecules/src/main/resources/schema.sql b/jdbc/jmolecules/src/main/resources/schema.sql
new file mode 100644
index 000000000..4a75e4b76
--- /dev/null
+++ b/jdbc/jmolecules/src/main/resources/schema.sql
@@ -0,0 +1,28 @@
+CREATE TABLE IF NOT EXISTS customer (
+ id VARCHAR(100) PRIMARY KEY,
+ firstname VARCHAR(100),
+ lastname VARCHAR(100)
+);
+
+CREATE TABLE IF NOT EXISTS address (
+ street VARCHAR(100),
+ city VARCHAR(100),
+ zip_code VARCHAR(100),
+ customer VARCHAR(100),
+ customer_key VARCHAR(100)
+);
+
+CREATE TABLE IF NOT EXISTS my_order (
+ id VARCHAR(100),
+ customer VARCHAR(100)
+);
+
+CREATE TABLE IF NOT EXISTS line_item (
+ my_order_key VARCHAR(100),
+ description VARCHAR(100),
+ foo VARCHAR(100)
+);
+
+// CREATE TABLE IF NOT EXISTS Lego_Set (id INTEGER, name VARCHAR(100), min_Age INTEGER, max_Age INTEGER);
+// CREATE TABLE IF NOT EXISTS Handbuch (handbuch_id INTEGER, author VARCHAR(100), text CLOB);
+// CREATE TABLE IF NOT EXISTS Model (name VARCHAR(100), description CLOB, lego_set INTEGER);
diff --git a/jdbc/jmolecules/src/test/java/example/springdata/jdbc/jmolecules/ApplicationIntegrationTests.java b/jdbc/jmolecules/src/test/java/example/springdata/jdbc/jmolecules/ApplicationIntegrationTests.java
new file mode 100644
index 000000000..135330c54
--- /dev/null
+++ b/jdbc/jmolecules/src/test/java/example/springdata/jdbc/jmolecules/ApplicationIntegrationTests.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.springdata.jdbc.jmolecules;
+
+import static org.assertj.core.api.Assertions.*;
+
+import example.springdata.jdbc.jmolecules.customer.Address;
+import example.springdata.jdbc.jmolecules.customer.Customer;
+import example.springdata.jdbc.jmolecules.customer.Customers;
+import example.springdata.jdbc.jmolecules.order.Order;
+import example.springdata.jdbc.jmolecules.order.Orders;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@SpringBootTest
+record ApplicationIntegrationTests(ConfigurableApplicationContext context) {
+
+ @Test
+ void exposesAssociationInMetamodel() {
+
+ var mapping = context.getBean(JdbcMappingContext.class);
+ var entity = mapping.getRequiredPersistentEntity(Order.class);
+ var customer = entity.getRequiredPersistentProperty("customer");
+
+ assertThat(customer.isAssociation()).isTrue();
+ }
+
+ @Test
+ void persistsDomainModel() {
+
+ var address = new Address("41 Greystreet", "Dreaming Tree", "2731");
+
+ var customers = context.getBean(Customers.class);
+ var customer = customers.save(new Customer("Dave", "Matthews", address));
+
+ customer.setFirstname("Carter");
+ customer = customers.save(customer);
+
+ var orders = context.getBean(Orders.class);
+
+ var order = new Order(customer)
+ .addLineItem("Foo")
+ .addLineItem("Bar");
+
+ var result = orders.save(order);
+
+ assertThat(customers.resolveRequired(result.getCustomer()));
+ }
+}
diff --git a/jdbc/jooq/pom.xml b/jdbc/jooq/pom.xml
index d9dc5da1d..6dbe76659 100644
--- a/jdbc/jooq/pom.xml
+++ b/jdbc/jooq/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data.examplesspring-data-jdbc-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOT../pom.xml
@@ -15,20 +15,17 @@
Sample project demonstrating Spring Data JDBC features
- 3.11.0
- 2.0.3.RELEASE
+ 3.19.26org.jooqjooq
- ${jooq.version}org.springframework.bootspring-boot-starter-jooq
- ${spring-boot-starter-jooq.version}org.jooq
@@ -42,7 +39,6 @@
org.jooqjooq-codegen-maven
- ${jooq.version}
@@ -71,4 +67,4 @@
-
\ No newline at end of file
+
diff --git a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/AgeGroup.java b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/AgeGroup.java
index e659fded4..0641f3251 100644
--- a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/AgeGroup.java
+++ b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/AgeGroup.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/Category.java b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/Category.java
index fb91f11da..47042114a 100644
--- a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/Category.java
+++ b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/Category.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@
import lombok.Data;
-import lombok.experimental.Wither;
+import lombok.With;
import org.springframework.data.annotation.Id;
/**
@@ -29,7 +29,7 @@
@Data
public class Category {
- @Wither
+ @With
private final @Id Long id;
private String name, description;
private AgeGroup ageGroup;
diff --git a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/CategoryConfiguration.java b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/CategoryConfiguration.java
index bc0ec7a9c..faa109233 100644
--- a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/CategoryConfiguration.java
+++ b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/CategoryConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,14 +21,10 @@
import org.jooq.impl.DefaultConfiguration;
import org.jooq.impl.DefaultDSLContext;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.ApplicationEvent;
-import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
-import org.springframework.data.jdbc.repository.config.JdbcConfiguration;
-import org.springframework.data.relational.core.mapping.event.RelationalEvent;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
/**
@@ -42,8 +38,7 @@
*/
@Configuration
@EnableJdbcRepositories
-@Import(JdbcConfiguration.class)
-public class CategoryConfiguration {
+public class CategoryConfiguration extends AbstractJdbcConfiguration {
@Autowired private DataSource dataSource;
@@ -58,7 +53,7 @@ DefaultDSLContext dsl() {
}
public DefaultConfiguration configuration() {
- DefaultConfiguration jooqConfiguration = new DefaultConfiguration();
+ var jooqConfiguration = new DefaultConfiguration();
jooqConfiguration.set(connectionProvider());
return jooqConfiguration;
}
diff --git a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/CategoryRepository.java b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/CategoryRepository.java
index 6d45ba28c..262f4ebd7 100644
--- a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/CategoryRepository.java
+++ b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/CategoryRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/JooqRepositoryImpl.java b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/JooqRepositoryImpl.java
index e3d8bef4f..e658874e6 100644
--- a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/JooqRepositoryImpl.java
+++ b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/JooqRepositoryImpl.java
@@ -11,13 +11,7 @@
*
* @author Florian Lüdiger
*/
-public class JooqRepositoryImpl implements JooqRepository {
-
- private final DSLContext dslContext;
-
- public JooqRepositoryImpl(DSLContext dslContext) {
- this.dslContext = dslContext;
- }
+public record JooqRepositoryImpl(DSLContext dslContext) implements JooqRepository {
public List getCategoriesWithAgeGroup(AgeGroup ageGroup) {
return this.dslContext.select().from(CATEGORY).where(CATEGORY.AGE_GROUP.equal(ageGroup.name()))
diff --git a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/Output.java b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/Output.java
index 6ad2006ac..e9e974275 100644
--- a/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/Output.java
+++ b/jdbc/jooq/src/main/java/example/springdata/jdbc/jooq/Output.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,7 +32,7 @@ public class Output {
public static void list(Iterable> categories, String title) {
- StringBuilder message = new StringBuilder(String.format("==== %s ====\n", title));
+ var message = new StringBuilder(String.format("==== %s ====\n", title));
categories.forEach(category -> message.append(category.toString().replace(", ", ",\n\t")));
diff --git a/jdbc/jooq/src/main/resources/application.properties b/jdbc/jooq/src/main/resources/application.properties
index 2804353df..73a53c56d 100644
--- a/jdbc/jooq/src/main/resources/application.properties
+++ b/jdbc/jooq/src/main/resources/application.properties
@@ -1,2 +1,3 @@
logging.level.org.springframework.data=INFO
-logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
\ No newline at end of file
+logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
+spring.liquibase.enabled=false
\ No newline at end of file
diff --git a/jdbc/jooq/src/main/resources/schema.sql b/jdbc/jooq/src/main/resources/schema.sql
index 600c536af..c377b0366 100644
--- a/jdbc/jooq/src/main/resources/schema.sql
+++ b/jdbc/jooq/src/main/resources/schema.sql
@@ -1,6 +1,14 @@
CREATE TABLE IF NOT EXISTS category (
- id INTEGER IDENTITY PRIMARY KEY,
- name VARCHAR(100),
+ id
+ INTEGER
+ AUTO_INCREMENT
+ PRIMARY
+ KEY,
+ name
+ VARCHAR
+(
+ 100
+),
description VARCHAR(2000),
age_group VARCHAR(20)
);
diff --git a/jdbc/jooq/src/test/java/example/springdata/jdbc/jooq/SimpleEntityTests.java b/jdbc/jooq/src/test/java/example/springdata/jdbc/jooq/SimpleEntityTests.java
index e85eed527..83b29c868 100644
--- a/jdbc/jooq/src/test/java/example/springdata/jdbc/jooq/SimpleEntityTests.java
+++ b/jdbc/jooq/src/test/java/example/springdata/jdbc/jooq/SimpleEntityTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,50 +18,47 @@
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
-import java.util.List;
+import org.junit.jupiter.api.Test;
-import org.junit.Test;
-import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc;
+import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureJdbc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
-import org.springframework.test.context.junit4.SpringRunner;
/**
* Demonstrates simple CRUD operations with a simple entity without any references.
*
* @author Jens Schauder
* @author Florian Lüdiger
+ * @author Divya Srivastava
*/
-@RunWith(SpringRunner.class)
@SpringBootTest(classes = CategoryConfiguration.class)
@AutoConfigureJdbc
@ComponentScan
-public class SimpleEntityTests {
+class SimpleEntityTests {
@Autowired CategoryRepository repository;
@Test
- public void exerciseRepositoryForSimpleEntity() {
+ void exerciseRepositoryForSimpleEntity() {
// create some categories
- Category cars = new Category(null, "Cars", "Anything that has approximately 4 wheels", AgeGroup._3to8);
+ var cars = new Category(null, "Cars", "Anything that has approximately 4 wheels", AgeGroup._3to8);
- Category buildings = new Category(null, "Buildings", null, AgeGroup._12andOlder);
+ var buildings = new Category(null, "Buildings", null, AgeGroup._12andOlder);
// save categories
- Iterable saved = repository.saveAll(asList(cars, buildings));
+ var saved = repository.saveAll(asList(cars, buildings));
Output.list(repository.findAll(), "`Cars` and `Buildings` got saved");
- assertThat(saved).extracting(c -> c.getId()).isNotNull();
+ assertThat(saved).extracting(Category::getId).isNotNull();
// update one
buildings.setDescription("Famous and impressive buildings incl. the 'bike shed'.");
repository.save(buildings);
Output.list(repository.findAll(), "`Buildings` has a description");
- List categoryList = repository.getCategoriesWithAgeGroup(AgeGroup._3to8);
+ var categoryList = repository.getCategoriesWithAgeGroup(AgeGroup._3to8);
assertThat(categoryList) //
.extracting(Category::getName, Category::getDescription, Category::getAgeGroup) //
diff --git a/jdbc/mybatis/README.adoc b/jdbc/mybatis/README.adoc
index 131224d13..d6d5b1ce7 100644
--- a/jdbc/mybatis/README.adoc
+++ b/jdbc/mybatis/README.adoc
@@ -7,4 +7,4 @@ The map of models is maintained by two statements configured in MyBatis mappings
`example.springdata.jdbc.mybatis.LegoSetMapper.findAllByProperty-models` showcases how a map can be loaded by configuring the select to return instances of `Map.Entry`
-`example.springdata.jdbc.mybatis.Model.insert` showcases how one can access the `MyBatixContext` and thereby the instance to save and the key of the parent entity.
+`example.springdata.jdbc.mybatis.Model.insert` showcases how one can access the `MyBatisContext` and thereby the instance to save and the key of the parent entity.
diff --git a/jdbc/mybatis/pom.xml b/jdbc/mybatis/pom.xml
index 0540f0e43..b693667c7 100644
--- a/jdbc/mybatis/pom.xml
+++ b/jdbc/mybatis/pom.xml
@@ -9,7 +9,7 @@
org.springframework.data.examplesspring-data-jdbc-examples
- 2.0.0.BUILD-SNAPSHOT
+ 4.0.0-SNAPSHOT../pom.xml
diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/AgeGroup.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/AgeGroup.java
index 60de294e5..c66030ae6 100644
--- a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/AgeGroup.java
+++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/AgeGroup.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSet.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSet.java
index 3dcb58dd1..4acb8dc9b 100644
--- a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSet.java
+++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSet.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,7 +38,7 @@ public class LegoSet {
public void addModel(String name, String description) {
- Model model = new Model();
+ var model = new Model();
model.name = name;
model.description = description;
models.put(name, model);
diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSetRepository.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSetRepository.java
index 06b4ec363..952c52157 100644
--- a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSetRepository.java
+++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/LegoSetRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Manual.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Manual.java
index 97376ef7a..5d17bc21d 100644
--- a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Manual.java
+++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Manual.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Model.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Model.java
index d9db6746f..6db286bbd 100644
--- a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Model.java
+++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Model.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/ModelMapEntry.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/ModelMapEntry.java
index 6c66ab51b..8e7bc301d 100644
--- a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/ModelMapEntry.java
+++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/ModelMapEntry.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/MyBatisConfiguration.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/MyBatisConfiguration.java
index 876149d62..108744dc1 100644
--- a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/MyBatisConfiguration.java
+++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/MyBatisConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,11 +19,11 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
-import org.springframework.data.jdbc.core.DataAccessStrategy;
+import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
+import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
-import org.springframework.data.jdbc.repository.config.JdbcConfiguration;
-import org.springframework.data.relational.core.conversion.RelationalConverter;
+import org.springframework.data.jdbc.repository.config.MyBatisJdbcConfiguration;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@@ -33,12 +33,5 @@
*/
@Configuration
@EnableJdbcRepositories
-@Import(JdbcConfiguration.class)
-public class MyBatisConfiguration {
-
- @Bean
- DataAccessStrategy defaultDataAccessStrategy(RelationalMappingContext context, RelationalConverter converter,
- NamedParameterJdbcOperations operations, SqlSession sqlSession) {
- return MyBatisDataAccessStrategy.createCombinedAccessStrategy(context, converter, operations, sqlSession);
- }
-}
+@Import(MyBatisJdbcConfiguration.class)
+public class MyBatisConfiguration {}
diff --git a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Output.java b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Output.java
index 3484d3a87..d1b83373c 100644
--- a/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Output.java
+++ b/jdbc/mybatis/src/main/java/example/springdata/jdbc/mybatis/Output.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2018 the original author or authors.
+ * Copyright 2017-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,7 +32,7 @@ public class Output {
public static void list(Iterable> categories, String title) {
- StringBuilder message = new StringBuilder(String.format("==== %s ====\n", title));
+ var message = new StringBuilder(String.format("==== %s ====\n", title));
categories.forEach(category -> {
message.append(category.toString().replace(", ", ",\n\t"));
diff --git a/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/LegoSet.xml b/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/LegoSet.xml
index 3ac897ea6..9578c27eb 100644
--- a/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/LegoSet.xml
+++ b/jdbc/mybatis/src/main/resources/example/springdata/jdbc/mybatis/LegoSet.xml
@@ -1,7 +1,8 @@
-