diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000..6ede4f0c9 --- /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 diff --git a/.gitignore b/.gitignore index fd2270241..7cc5f4de1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ .project .classpath .springBeans +.develocity/ .settings/ target/ +.mvn/.gradle-enterprise #IntelliJ Stuff .idea 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/README.adoc b/README.adoc index 9ce0564f8..43d26ddbd 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= Spring Data Examples += 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 Data - Examples"] 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] @@ -23,8 +23,14 @@ 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 @@ -106,8 +112,8 @@ WARNING: If you're done using it, don't forget to shut it down! == Miscellaneous -* `bom` - Example project how to use the Spring Data release train bom in non-Spring-Boot - scenarios. +* `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. diff --git a/bom/pom.xml b/bom/pom.xml index 73e301f98..507efd36c 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -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/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java index 842610747..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 @@ -18,6 +18,7 @@ import java.util.List; import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.domain.Limit; import org.springframework.data.repository.CrudRepository; /** @@ -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 25df51a0a..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 @@ -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/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java index 436b37680..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-2021 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, @@ -21,11 +21,14 @@ import example.springdata.cassandra.util.CassandraKeyspace; import example.springdata.cassandra.util.CassandraVersion; +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 com.datastax.oss.driver.api.core.CqlSession; @@ -109,7 +112,8 @@ void findByDerivedQueryMethodWithSASI() throws InterruptedException { 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* @@ -120,4 +124,25 @@ 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/reactive/src/main/java/example/springdata/cassandra/people/ReactivePersonRepository.java b/cassandra/reactive/src/main/java/example/springdata/cassandra/people/ReactivePersonRepository.java index 6c8a38402..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 @@ -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/test/java/example/springdata/cassandra/people/ReactiveCassandraTemplateIntegrationTest.java b/cassandra/reactive/src/test/java/example/springdata/cassandra/people/ReactiveCassandraTemplateIntegrationTest.java index af1858b8b..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 @@ -48,7 +48,9 @@ void setUp() { new Person("Jesse", "Pinkman", 27))) // .flatMap(template::insert); - StepVerifier.create(truncateAndInsert).expectNextCount(4).verifyComplete(); + truncateAndInsert.as(StepVerifier::create) // + .expectNextCount(4) // + .verifyComplete(); } /** @@ -67,6 +69,8 @@ void shouldInsertAndCountData() { .flatMap(v -> template.count(Person.class)) // .doOnNext(System.out::println); - StepVerifier.create(saveAndCount).expectNext(6L).verifyComplete(); + 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 bf17e8897..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 @@ -16,6 +16,7 @@ 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; @@ -49,7 +50,9 @@ void setUp() { new Person("Saul", "Goodman", 42), // new Person("Jesse", "Pinkman", 27)))); - StepVerifier.create(deleteAndInsert).expectNextCount(4).verifyComplete(); + deleteAndInsert.as(StepVerifier::create) // + .expectNextCount(4) // + .verifyComplete(); } /** @@ -66,7 +69,9 @@ void shouldInsertAndCountData() { .flatMap(v -> repository.count()) // .doOnNext(System.out::println); - StepVerifier.create(saveAndCount).expectNext(6L).verifyComplete(); + saveAndCount.as(StepVerifier::create) // + .expectNext(6L) // + .verifyComplete(); } /** @@ -76,7 +81,7 @@ void shouldInsertAndCountData() { @Test void shouldPerformConversionBeforeResultProcessing() { - StepVerifier.create(repository.findAll().doOnNext(System.out::println)) // + repository.findAll().doOnNext(System.out::println).as(StepVerifier::create) // .expectNextCount(4) // .verifyComplete(); } @@ -86,7 +91,21 @@ void shouldPerformConversionBeforeResultProcessing() { */ @Test void shouldQueryDataWithQueryDerivation() { - StepVerifier.create(repository.findByLastname("White")).expectNextCount(2).verifyComplete(); + + 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(); } /** @@ -94,7 +113,10 @@ void shouldQueryDataWithQueryDerivation() { */ @Test void shouldQueryDataWithStringQuery() { - StepVerifier.create(repository.findByFirstnameInAndLastname("Walter", "White")).expectNextCount(1).verifyComplete(); + + repository.findByFirstnameInAndLastname("Walter", "White").as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); } /** @@ -102,7 +124,10 @@ void shouldQueryDataWithStringQuery() { */ @Test void shouldQueryDataWithDeferredQueryDerivation() { - StepVerifier.create(repository.findByLastname(Mono.just("White"))).expectNextCount(2).verifyComplete(); + + repository.findByLastname(Mono.just("White")).as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); } /** @@ -111,7 +136,7 @@ void shouldQueryDataWithDeferredQueryDerivation() { @Test 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/util/pom.xml b/cassandra/util/pom.xml index adf66b173..3b72a57ef 100644 --- a/cassandra/util/pom.xml +++ b/cassandra/util/pom.xml @@ -31,7 +31,7 @@ - com.datastax.oss + org.apache.cassandra java-driver-core diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 4de2433e7..bc4fd91c6 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -57,4 +57,27 @@ + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + org.apache.logging.log4j + log4j-core + ${log4j2.version} + + + + + + + diff --git a/jdbc/graalvm-native/README.adoc b/jdbc/graalvm-native/README.adoc index be3f31b46..3aa10ed9b 100644 --- a/jdbc/graalvm-native/README.adoc +++ b/jdbc/graalvm-native/README.adoc @@ -1,6 +1,6 @@ == Spring Data JDBC - GraalVM native image -This example compiles a basic Spring Data JDBC appication into a GraalVM native image. +This example compiles a basic Spring Data JDBC application into a GraalVM native image. === Install GraalVM & native image tooling diff --git a/jdbc/howto/schema-generation/pom.xml b/jdbc/howto/schema-generation/pom.xml index 29f5c92eb..0b065532d 100644 --- a/jdbc/howto/schema-generation/pom.xml +++ b/jdbc/howto/schema-generation/pom.xml @@ -18,10 +18,6 @@ https://projects.spring.io/spring-data-jdbc 2023 - - 2023.1.0-M2 - - 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 index 3513338f6..b0f822c50 100644 --- 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 @@ -17,6 +17,7 @@ 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; @@ -35,6 +36,7 @@ public PartyHatRepositoryImpl(NamedParameterJdbcOperations template) { this.template = template; } + @Transactional @Override public void addPartyHat(Minion minion) { 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 index 9e1b01dd8..d1e005f01 100644 --- 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 @@ -17,7 +17,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication class SelectiveUpdateApplication { 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 index 5d6943496..041679138 100644 --- 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 @@ -58,7 +58,7 @@ void turnPurpleByDirectUpdate() { Minion bob2 = minions.findById(bob.id).orElseThrow(); - assertThat(bob2.toys).containsExactly(bob.toys.toArray(new Toy[] {})); + assertThat(bob2.toys).containsExactlyElementsOf(bob.toys); assertThat(bob2.name).isEqualTo("Bob"); assertThat(bob2.color).isEqualTo(Color.PURPLE); } @@ -81,4 +81,23 @@ void grantPartyHat() { assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> minions.addPartyHat(bob)); } + @Test + 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/pom.xml b/jdbc/immutables/pom.xml index 8527b31dc..8d8b00297 100644 --- a/jdbc/immutables/pom.xml +++ b/jdbc/immutables/pom.xml @@ -15,14 +15,36 @@ Spring Data JDBC - Usage with Immutables Sample project demonstrating Spring Data JDBC features + + 2.8.8 + + org.immutables value - 2.8.8 + ${immutables.version} provided + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.immutables + value + ${immutables.version} + + + + + + + diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 742457e16..81dcfbe55 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -22,6 +22,7 @@ immutables jmolecules jooq + singlequeryloading graalvm-native diff --git a/jdbc/singlequeryloading/pom.xml b/jdbc/singlequeryloading/pom.xml new file mode 100644 index 000000000..c24bd8e13 --- /dev/null +++ b/jdbc/singlequeryloading/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + singlequeryloading + + + org.springframework.data.examples + spring-data-jdbc-examples + 2.0.0.BUILD-SNAPSHOT + ../pom.xml + + + Spring Data JDBC - Demonstration of Single Query Loading + Sample project demonstrating Single Query Loading with Spring Data JDBC + + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + org.testcontainers + postgresql + + + org.postgresql + postgresql + + + diff --git a/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Cat.java b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Cat.java new file mode 100644 index 000000000..48e3e1f23 --- /dev/null +++ b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Cat.java @@ -0,0 +1,24 @@ +/* + * 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.singlequeryloading; + +/** + * A simple entity for use in a collection of {@link PetOwner}. + * + * @author Jens Schauder + */ +record Cat(String name) { +} diff --git a/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Config.java b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Config.java new file mode 100644 index 000000000..b23133d84 --- /dev/null +++ b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Config.java @@ -0,0 +1,46 @@ +/* + * 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 + * + * 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.singlequeryloading; + +import java.util.Optional; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; +import org.springframework.data.relational.RelationalManagedTypes; +import org.springframework.data.relational.core.mapping.NamingStrategy; + +/** + * Spring application context configuration that enables Single Query Loading. + * + * @author Jens Schauder + */ +@SpringBootConfiguration +@EnableJdbcRepositories +public class Config extends AbstractJdbcConfiguration { + + @Override + public JdbcMappingContext jdbcMappingContext(Optional namingStrategy, + JdbcCustomConversions customConversions, RelationalManagedTypes jdbcManagedTypes) { + + JdbcMappingContext jdbcMappingContext = super.jdbcMappingContext(namingStrategy, customConversions, + jdbcManagedTypes); + jdbcMappingContext.setSingleQueryLoadingEnabled(true); + return jdbcMappingContext; + } +} diff --git a/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Dog.java b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Dog.java new file mode 100644 index 000000000..450efe97e --- /dev/null +++ b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Dog.java @@ -0,0 +1,24 @@ +/* + * 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.singlequeryloading; + +/** + * A simple entity for use in a collection of {@link PetOwner}. + * + * @author Jens Schauder + */ +record Dog(String name) { +} \ No newline at end of file diff --git a/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Fish.java b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Fish.java new file mode 100644 index 000000000..a2f41f651 --- /dev/null +++ b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/Fish.java @@ -0,0 +1,24 @@ +/* + * 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.singlequeryloading; + +/** + * A simple entity for use in a collection of {@link PetOwner}. + * + * @author Jens Schauder + */ +record Fish(String name) { +} diff --git a/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/PetOwner.java b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/PetOwner.java new file mode 100644 index 000000000..bad882cc0 --- /dev/null +++ b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/PetOwner.java @@ -0,0 +1,63 @@ +/* + * 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 + * + * 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.singlequeryloading; + +import java.util.List; +import java.util.Objects; + +import org.springframework.data.annotation.Id; + +/** + * An aggregate with mutliple collections. + * + * @author Jens Schauder + */ +class PetOwner { + + @Id Long Id; + + String name; + + List dogs; + + List cats; + + List fish; + + public PetOwner(String name, List cats, List dogs, List fish) { + + this.name = name; + this.cats = cats; + this.dogs = dogs; + this.fish = fish; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + PetOwner petOwner = (PetOwner) o; + return Objects.equals(Id, petOwner.Id) && Objects.equals(name, petOwner.name) && Objects.equals(dogs, petOwner.dogs) + && Objects.equals(cats, petOwner.cats) && Objects.equals(fish, petOwner.fish); + } + + @Override + public int hashCode() { + return Objects.hash(Id, name, dogs, cats, fish); + } +} diff --git a/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/PetOwnerRepository.java b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/PetOwnerRepository.java new file mode 100644 index 000000000..00f2084f4 --- /dev/null +++ b/jdbc/singlequeryloading/src/main/java/example/springdata/jdbc/singlequeryloading/PetOwnerRepository.java @@ -0,0 +1,25 @@ +/* + * 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 + * + * 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.singlequeryloading; + +import org.springframework.data.repository.CrudRepository; + +/** + * Repository to access {@link PetOwner} instances. + * + * @author Jens Schauder + */ +interface PetOwnerRepository extends CrudRepository {} diff --git a/jdbc/singlequeryloading/src/main/resources/application.properties b/jdbc/singlequeryloading/src/main/resources/application.properties new file mode 100644 index 000000000..d467e12d4 --- /dev/null +++ b/jdbc/singlequeryloading/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.datasource.url=jdbc:tc:postgresql:16.0:///test +logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG +spring.sql.init.mode=always \ No newline at end of file diff --git a/jdbc/singlequeryloading/src/main/resources/schema.sql b/jdbc/singlequeryloading/src/main/resources/schema.sql new file mode 100644 index 000000000..b398d1147 --- /dev/null +++ b/jdbc/singlequeryloading/src/main/resources/schema.sql @@ -0,0 +1,27 @@ +CREATE TABLE PET_OWNER +( + ID SERIAL PRIMARY KEY, + NAME VARCHAR(255) +); + +CREATE TABLE CAT +( + PET_OWNER INT, + PET_OWNER_KEY INT, + NAME VARCHAR(255) +); + +CREATE TABLE DOG +( + PET_OWNER INT, + PET_OWNER_KEY INT, + NAME VARCHAR(255) +); + +CREATE TABLE FISH +( + PET_OWNER INT, + PET_OWNER_KEY INT, + NAME VARCHAR(255) +); + diff --git a/jdbc/singlequeryloading/src/test/java/example/springdata/jdbc/singlequeryloading/SingleQueryLoadingApplicationTests.java b/jdbc/singlequeryloading/src/test/java/example/springdata/jdbc/singlequeryloading/SingleQueryLoadingApplicationTests.java new file mode 100644 index 000000000..6733eca4e --- /dev/null +++ b/jdbc/singlequeryloading/src/test/java/example/springdata/jdbc/singlequeryloading/SingleQueryLoadingApplicationTests.java @@ -0,0 +1,80 @@ +/* + * 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 + * + * 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.singlequeryloading; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.relational.core.query.Criteria.*; +import static org.springframework.data.relational.core.query.Query.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.data.jdbc.core.JdbcAggregateTemplate; + +/** + * Run tests demonstrating the use of Single Query Loading. You'll have to observe the executed queries. + * + * @author Jens Schauder + */ +@JdbcTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class SingleQueryLoadingApplicationTests { + + @Autowired PetOwnerRepository petOwners; + @Autowired JdbcAggregateTemplate template; + + private PetOwner emil; + private PetOwner marry; + + @BeforeEach + void setup() { + + petOwners.deleteAll(); + + emil = petOwners.save(new PetOwner("Emil", // + List.of(new Cat("Edgar"), new Cat("Einstein"), new Cat("Elliot"), new Cat("Elton"), new Cat("Evan")), // + List.of(new Dog("Eric"), new Dog("Eddie"), new Dog("Eke"), new Dog("Echo")), // + List.of(new Fish("Floaty Mc Floatface")) // + )); + + marry = petOwners.save(new PetOwner("Marry", List.of(new Cat("Mars"), new Cat("Maverick"), new Cat("Max")), // + List.of(new Dog("Molly"), new Dog("Murphy"), new Dog("Madison"), new Dog("Macie")), // + List.of(new Fish("Mahi Mahi"), new Fish("Mr. Limpet")) // + )); + } + + @Test + void loadById() { + + PetOwner emilReloaded = petOwners.findById(emil.Id).orElseThrow(); + + assertThat(emilReloaded).isEqualTo(emil); + } + + @Test + void loadByNameUsingTemplate() { + + List marries = (List) template.findAll(query(where("name").is("Marry")), PetOwner.class); + + assertThat(marries).containsExactly(marry); + } + +} diff --git a/jpa/aot-optimization/pom.xml b/jpa/aot-optimization/pom.xml new file mode 100644 index 000000000..67ea47db1 --- /dev/null +++ b/jpa/aot-optimization/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + org.springframework.data.examples + spring-data-jpa-examples + 2.0.0.BUILD-SNAPSHOT + + + org.example + spring-data-jpa-aot-optimization + + + UTF-8 + 7.0.0.CR2 + 2025.1.0-M3 + + + + + org.jspecify + jspecify + 1.0.0 + + + + jakarta.persistence + jakarta.persistence-api + 3.2.0 + + + + com.querydsl + querydsl-jpa + 5.1.0 + jakarta + + + + com.querydsl + querydsl-apt + 5.1.0 + jakarta + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + com.querydsl + querydsl-jpa + 5.1.0 + jakarta + + + com.querydsl + querydsl-apt + 5.1.0 + jakarta + + + jakarta.persistence + jakarta.persistence-api + 3.2.0 + + + + + target/generated-test-sources + + target/generated-sources + + + + + org.springframework.boot + spring-boot-maven-plugin + + + process-aot + + process-aot + + + + + + + + diff --git a/jpa/aot-optimization/src/main/java/example/springdata/aot/AotJpaApp.java b/jpa/aot-optimization/src/main/java/example/springdata/aot/AotJpaApp.java new file mode 100644 index 000000000..66235ab8c --- /dev/null +++ b/jpa/aot-optimization/src/main/java/example/springdata/aot/AotJpaApp.java @@ -0,0 +1,31 @@ +/* + * 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.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Christoph Strobl + */ +@SpringBootApplication +public class AotJpaApp { + + public static void main(String[] args) { + SpringApplication.run(AotJpaApp.class, args); + } + +} diff --git a/jpa/aot-optimization/src/main/java/example/springdata/aot/CLR.java b/jpa/aot-optimization/src/main/java/example/springdata/aot/CLR.java new file mode 100644 index 000000000..8272939a9 --- /dev/null +++ b/jpa/aot-optimization/src/main/java/example/springdata/aot/CLR.java @@ -0,0 +1,125 @@ +/* + * 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.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Component; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@Component +public class CLR implements CommandLineRunner { + + @Autowired UserRepository repository; + + @Override + public void run(String... args) throws Exception { + + User luke = new User("id-1", "luke"); + luke.setFirstname("Luke"); + luke.setLastname("Skywalker"); + // Post lukeP1 = new Post("I have a bad feeling about this."); + // em.persist(lukeP1); + // luke.setPosts(List.of(lukeP1)); + + User leia = new User("id-2", "leia"); + leia.setFirstname("Leia"); + leia.setLastname("Organa"); + + User han = new User("id-3", "han"); + han.setFirstname("Han"); + han.setLastname("Solo"); + // Post hanP1 = new Post("It's the ship that made the Kessel Run in less than 12 Parsecs."); + // em.persist(hanP1); + // han.setPosts(List.of(hanP1)); + + User chewbacca = new User("id-4", "chewbacca"); + User yoda = new User("id-5", "yoda"); + Post yodaP1 = new Post("Do. Or do not. There is no try."); + Post yodaP2 = new Post( + "Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered."); + // em.persist(yodaP1); + // em.persist(yodaP2); + // yoda.setPosts(List.of(yodaP1, yodaP2)); + + User vader = new User("id-6", "vader"); + vader.setFirstname("Anakin"); + vader.setLastname("Skywalker"); + // Post vaderP1 = new Post("I am your father"); + // em.persist(vaderP1); + // vader.setPosts(List.of(vaderP1)); + + User kylo = new User("id-7", "kylo"); + kylo.setFirstname("Ben"); + kylo.setLastname("Solo"); + + repository.saveAll(List.of(luke, leia, han, chewbacca, yoda, vader, kylo)); + + System.out.println("------- annotated multi -------"); + System.out.println(repository.usersWithUsernamesStartingWith("l")); + + System.out.println("------- derived single -------"); + System.out.println(repository.findUserByUsername("yoda")); + + // System.out.println("------- derived nested.path -------"); + // System.out.println(repository.findUserByPostsMessageLike("father")); + + System.out.println("------- derived optional -------"); + System.out.println(repository.findOptionalUserByUsername("yoda")); + + System.out.println("------- derived count -------"); + Long count = repository.countUsersByLastnameLike("Sky"); + System.out.println("user count " + count); + + System.out.println("------- derived exists -------"); + Boolean exists = repository.existsByUsername("vader"); + System.out.println("user exists " + exists); + + System.out.println("------- derived multi -------"); + System.out.println(repository.findUserByLastnameStartingWith("Sky")); + + System.out.println("------- derived sorted -------"); + System.out.println(repository.findUserByLastnameStartingWithOrderByFirstname("Sky")); + + System.out.println("------- derived page -------"); + Page page0 = repository.findUserByLastnameStartingWith("S", PageRequest.of(0, 2)); + System.out.println("page0: " + page0); + System.out.println("page0.content: " + page0.getContent()); + + Page page1 = repository.findUserByLastnameStartingWith("S", PageRequest.of(1, 2)); + System.out.println("page1: " + page1); + System.out.println("page1.content: " + page1.getContent()); + + System.out.println("------- derived slice -------"); + Slice slice0 = repository.findUserByUsernameAfter("luke", PageRequest.of(0, 2)); + System.out.println("slice0: " + slice0); + System.out.println("slice0.content: " + slice0.getContent()); + + System.out.println("------- derived top -------"); + System.out.println(repository.findTop2UsersByLastnameStartingWith("S")); + + // System.out.println("------- derived with fields -------"); + // System.out.println(repository.findJustUsernameBy()); + } +} diff --git a/jpa/aot-optimization/src/main/java/example/springdata/aot/Post.java b/jpa/aot-optimization/src/main/java/example/springdata/aot/Post.java new file mode 100644 index 000000000..6c6e3e82d --- /dev/null +++ b/jpa/aot-optimization/src/main/java/example/springdata/aot/Post.java @@ -0,0 +1,66 @@ +/* + * 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 jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Random; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@Entity +public class Post { + + @Id + @GeneratedValue private Long id; + + private String message; + private Instant date; + + public Post() {} + + public Post(String message) { + this.message = message; + this.date = Instant.now().minus(new Random().nextLong(100), ChronoUnit.MINUTES); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Instant getDate() { + return date; + } + + public void setDate(Instant date) { + this.date = date; + } + + @Override + public String toString() { + return message; + } +} diff --git a/jpa/aot-optimization/src/main/java/example/springdata/aot/User.java b/jpa/aot-optimization/src/main/java/example/springdata/aot/User.java new file mode 100644 index 000000000..b9c43ff05 --- /dev/null +++ b/jpa/aot-optimization/src/main/java/example/springdata/aot/User.java @@ -0,0 +1,105 @@ +/* + * 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 jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import java.time.Instant; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@Entity(name = "users") +public class User { + + @Id private String id; + private String username; + + @Column(name = "first_name") String firstname; + @Column(name = "last_name") String lastname; + + // @OneToMany + // private List posts; + + Instant registrationDate; + Instant lastSeen; + + public User() {} + + public User(String id, String username) { + this.id = id; + this.username = username; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Instant getRegistrationDate() { + return registrationDate; + } + + public void setRegistrationDate(Instant registrationDate) { + this.registrationDate = registrationDate; + } + + public Instant getLastSeen() { + return lastSeen; + } + + public void setLastSeen(Instant lastSeen) { + this.lastSeen = lastSeen; + } + + // public List getPosts() { + // return posts; + // } + // + // public void setPosts(List posts) { + // this.posts = posts; + // } + + @Override + public String toString() { + return "User{" + "id='" + id + '\'' + ", username='" + username + '\'' + ", firstname='" + firstname + '\'' + + ", lastname='" + lastname + '\'' + ", registrationDate=" + registrationDate + ", lastSeen=" + lastSeen + + // ", posts=" + posts + + '}'; + } +} diff --git a/jpa/aot-optimization/src/main/java/example/springdata/aot/UserProjection.java b/jpa/aot-optimization/src/main/java/example/springdata/aot/UserProjection.java new file mode 100644 index 000000000..8ac144a12 --- /dev/null +++ b/jpa/aot-optimization/src/main/java/example/springdata/aot/UserProjection.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.aot; + +import java.time.Instant; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public record UserProjection(String username, Instant registrationDate) { + +} diff --git a/jpa/aot-optimization/src/main/java/example/springdata/aot/UserRepository.java b/jpa/aot-optimization/src/main/java/example/springdata/aot/UserRepository.java new file mode 100644 index 000000000..1da15b360 --- /dev/null +++ b/jpa/aot-optimization/src/main/java/example/springdata/aot/UserRepository.java @@ -0,0 +1,57 @@ +/* + * 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 java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserRepository extends CrudRepository, QuerydslPredicateExecutor { + + User findUserByUsername(String username); + + Optional findOptionalUserByUsername(String username); + + Long countUsersByLastnameLike(String lastname); + + Boolean existsByUsername(String username); + + List findUserByLastnameLike(String lastname); + + List findUserByLastnameStartingWithOrderByFirstname(String lastname); + + List findTop2UsersByLastnameStartingWith(String lastname); + + Slice findUserByUsernameAfter(String username, Pageable pageable); + + List findUserByLastnameStartingWith(String lastname); + + Page findUserByLastnameStartingWith(String lastname, Pageable page); + + @Query("SELECT u FROM example.springdata.aot.User u WHERE u.username LIKE ?1%") + List usersWithUsernamesStartingWith(String username); + +} diff --git a/jpa/aot-optimization/src/main/resources/application.properties b/jpa/aot-optimization/src/main/resources/application.properties new file mode 100644 index 000000000..ee6779653 --- /dev/null +++ b/jpa/aot-optimization/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.jpa.defer-datasource-initialization=true +spring.aot.repositories.enabled=true +#spring.aot.jpa.repositories.use-entitymanager=true +#logging.level.org.springframework.data.repository.aot.generate.RepositoryContributor=trace + diff --git a/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleUserRepository.java b/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleUserRepository.java index 75bfcc659..9f5c77a85 100644 --- a/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleUserRepository.java +++ b/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleUserRepository.java @@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; +import org.springframework.data.domain.Limit; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; @@ -63,6 +64,17 @@ public interface SimpleUserRepository extends ListCrudRepository { */ List findByLastname(String lastname); + /** + * Find at most the number of users defined via maxResults with the given lastname. + * This method will be translated into a query by constructing it directly from the method name as there is no other + * query declared. + * + * @param lastname + * @param maxResults the maximum number of results returned. + * @return + */ + List findByLastname(String lastname, Limit maxResults); + /** * Returns all users with the given firstname. This method will be translated into a query using the one declared in * the {@link Query} annotation declared one. @@ -73,6 +85,17 @@ public interface SimpleUserRepository extends ListCrudRepository { @Query("select u from User u where u.firstname = :firstname") List findByFirstname(String firstname); + /** + * Returns at most the number of users defined via {@link Limit} with the given firstname. This method will be + * translated into a query using the one declared in the {@link Query} annotation declared one. + * + * @param firstname + * @param maxResults the maximum number of results returned. + * @return + */ + @Query("select u from User u where u.firstname = :firstname") + List findByFirstname(String firstname, Limit maxResults); + /** * Returns all users with the given name as first- or lastname. This makes the query to method relation much more * refactoring-safe as the order of the method parameters is completely irrelevant. diff --git a/jpa/example/src/test/java/example/springdata/jpa/simple/SimpleUserRepositoryTests.java b/jpa/example/src/test/java/example/springdata/jpa/simple/SimpleUserRepositoryTests.java index 9c4da22d6..5fcfdc773 100644 --- a/jpa/example/src/test/java/example/springdata/jpa/simple/SimpleUserRepositoryTests.java +++ b/jpa/example/src/test/java/example/springdata/jpa/simple/SimpleUserRepositoryTests.java @@ -27,6 +27,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; @@ -36,6 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Limit; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Propagation; @@ -83,6 +85,22 @@ void findSavedUserByLastname() { assertThat(repository.findByLastname("lastname")).contains(user); } + @Test + void findLimitedNumberOfUsersViaDerivedQuery() { + + IntStream.range(0, 10).forEach($ -> repository.save(new User(user.getFirstname(), user.getLastname()))); + + assertThat(repository.findByLastname("lastname", Limit.of(5))).hasSize(5); + } + + @Test + void findLimitedNumberOfUsersViaAnnotatedQuery() { + + IntStream.range(0, 10).forEach($ -> repository.save(new User(user.getFirstname(), user.getLastname()))); + + assertThat(repository.findByFirstname(user.getFirstname(), Limit.of(5))).hasSize(5); + } + @Test void findByFirstnameOrLastname() { diff --git a/jpa/example/src/test/java/example/springdata/jpa/simple/VirtualThreadsTests.java b/jpa/example/src/test/java/example/springdata/jpa/simple/VirtualThreadsTests.java index af581aa3f..915d2131d 100644 --- a/jpa/example/src/test/java/example/springdata/jpa/simple/VirtualThreadsTests.java +++ b/jpa/example/src/test/java/example/springdata/jpa/simple/VirtualThreadsTests.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; import org.springframework.beans.factory.annotation.Autowired; @@ -41,7 +41,7 @@ */ @Transactional @SpringBootTest(properties = "spring.threads.virtual.enabled=true") -@EnabledOnJre(JRE.JAVA_21) +@EnabledForJreRange(min = JRE.JAVA_21) class VirtualThreadsTests { @Autowired SimpleUserRepository repository; diff --git a/jpa/graalvm-native/pom.xml b/jpa/graalvm-native/pom.xml index a91305c6c..839326d50 100644 --- a/jpa/graalvm-native/pom.xml +++ b/jpa/graalvm-native/pom.xml @@ -37,7 +37,7 @@ org.hibernate.orm.tooling hibernate-enhance-maven-plugin - 6.1.4.Final + ${hibernate.version} diff --git a/jpa/interceptors/src/main/java/example/springdata/jpa/interceptors/Customer.java b/jpa/interceptors/src/main/java/example/springdata/jpa/interceptors/Customer.java index e3591c461..02b026b30 100644 --- a/jpa/interceptors/src/main/java/example/springdata/jpa/interceptors/Customer.java +++ b/jpa/interceptors/src/main/java/example/springdata/jpa/interceptors/Customer.java @@ -22,9 +22,19 @@ @Entity public class Customer { - @Id @GeneratedValue Long id; + @Id + @GeneratedValue + Long id; String firstname; String lastname; + @Override + public String toString() { + return "Customer{" + + "id=" + id + + ", firstname='" + firstname + '\'' + + ", lastname='" + lastname + '\'' + + '}'; + } } diff --git a/jpa/interceptors/src/test/java/example/springdata/jpa/interceptors/InterceptorIntegrationTest.java b/jpa/interceptors/src/test/java/example/springdata/jpa/interceptors/InterceptorIntegrationTest.java index 6ccace00c..976c07f74 100644 --- a/jpa/interceptors/src/test/java/example/springdata/jpa/interceptors/InterceptorIntegrationTest.java +++ b/jpa/interceptors/src/test/java/example/springdata/jpa/interceptors/InterceptorIntegrationTest.java @@ -30,12 +30,13 @@ class InterceptorIntegrationTest { @Autowired CustomerRepository repository; @Test - void foo() { + void demonstrateInterceptor() { var customer = new Customer(); customer.firstname = "Dave"; customer.lastname = "Matthews"; + // observer Log output from ApplicationConfiguration.interceptor repository.save(customer); } } diff --git a/jpa/multitenant/README.adoc b/jpa/multitenant/README.adoc index 0c7511491..d4f21f927 100644 --- a/jpa/multitenant/README.adoc +++ b/jpa/multitenant/README.adoc @@ -8,3 +8,5 @@ Each uses a different strategy to separate data by tenant: 2. Use a separate schema per tenant 3. Use a separate database per tenant. +_The contained projects are only examples how to use Hibernates Multitenant feature with Spring Data JPA. +For any real application a decision has to be made how to scope a tenant. Storing it in a singleton as in the examples is for most cases not an appropriate solution._ diff --git a/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/NoOpConnectionProvider.java b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/NoOpConnectionProvider.java index 7e3a48b09..d4e4548ae 100644 --- a/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/NoOpConnectionProvider.java +++ b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/NoOpConnectionProvider.java @@ -42,17 +42,6 @@ public void releaseAnyConnection(Connection connection) throws SQLException { connection.close(); } - @Override - public Connection getConnection(String schema) throws SQLException { - - return dataSource.getConnection(); - } - - @Override - public void releaseConnection(String s, Connection connection) throws SQLException { - connection.close(); - } - @Override public boolean supportsAggressiveRelease() { return false; @@ -72,4 +61,18 @@ public T unwrap(Class aClass) { public void customize(Map hibernateProperties) { hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this); } + + @Override + public Connection getConnection(Object tenantIdentifier) throws SQLException { + return dataSource.getConnection(); + } + + /* + * (non-Javadoc) + * @see org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider#releaseConnection(java.lang.Object, java.sql.Connection) + */ + @Override + public void releaseConnection(Object tenantIdentifier, Connection connection) throws SQLException { + connection.close(); + } } diff --git a/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/ExampleConnectionProvider.java b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/ExampleConnectionProvider.java index c482c5f8c..cacd066d8 100644 --- a/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/ExampleConnectionProvider.java +++ b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/ExampleConnectionProvider.java @@ -43,14 +43,20 @@ public void releaseAnyConnection(Connection connection) throws SQLException { } @Override - public Connection getConnection(String schema) throws SQLException { + public Connection getConnection(Object tenantIdentifier) throws SQLException { + final Connection connection = dataSource.getConnection(); - connection.setSchema(schema); + connection.setSchema(tenantIdentifier.toString()); return connection; } + /* + * (non-Javadoc) + * @see org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider#releaseConnection(java.lang.Object, java.sql.Connection) + */ @Override - public void releaseConnection(String s, Connection connection) throws SQLException { + public void releaseConnection(Object tenantIdentifier, Connection connection) throws SQLException { + connection.setSchema("PUBLIC"); connection.close(); } diff --git a/jpa/pom.xml b/jpa/pom.xml index a70186076..9b94d5869 100644 --- a/jpa/pom.xml +++ b/jpa/pom.xml @@ -17,6 +17,7 @@ 2011 + aot-optimization deferred envers example @@ -31,26 +32,6 @@ graalvm-native - - - hibernate-53 - - 5.3.0.Final - - - - java-next - - - [8,14] - - - - eclipselink - - - - @@ -58,6 +39,21 @@ spring-boot-starter-data-jpa + + org.springframework.data + spring-data-commons + + + + org.springframework.data + spring-data-jpa + + + + jakarta.persistence + jakarta.persistence-api + + org.hsqldb hsqldb diff --git a/mongodb/aot-optimization/pom.xml b/mongodb/aot-optimization/pom.xml new file mode 100644 index 000000000..fb918c289 --- /dev/null +++ b/mongodb/aot-optimization/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + org.springframework.data.examples + spring-data-mongodb-examples + 2.0.0.BUILD-SNAPSHOT + + + org.example + spring-data-mongodb-aot-optimization + + + 21 + 21 + UTF-8 + 2025.1.0-M3 + + + + + org.jspecify + jspecify + 1.0.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + process-aot + + process-aot + + + + + + + + diff --git a/mongodb/aot-optimization/src/main/java/example/springdata/aot/App.java b/mongodb/aot-optimization/src/main/java/example/springdata/aot/App.java new file mode 100644 index 000000000..1f3c478af --- /dev/null +++ b/mongodb/aot-optimization/src/main/java/example/springdata/aot/App.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; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Christoph Strobl + */ +@SpringBootApplication +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/mongodb/aot-optimization/src/main/java/example/springdata/aot/CLR.java b/mongodb/aot-optimization/src/main/java/example/springdata/aot/CLR.java new file mode 100644 index 000000000..1ccacfaa2 --- /dev/null +++ b/mongodb/aot-optimization/src/main/java/example/springdata/aot/CLR.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.aot; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Component; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@Component +public class CLR implements CommandLineRunner { + + @Autowired UserRepository repository; + + @Override + public void run(String... args) throws Exception { + + User luke = new User("id-1", "luke"); + luke.setFirstname("Luke"); + luke.setLastname("Skywalker"); + luke.setPosts(List.of(new Post("I have a bad feeling about this."))); + + User leia = new User("id-2", "leia"); + leia.setFirstname("Leia"); + leia.setLastname("Organa"); + + User han = new User("id-3", "han"); + han.setFirstname("Han"); + han.setLastname("Solo"); + han.setPosts(List.of(new Post("It's the ship that made the Kessel Run in less than 12 Parsecs."))); + + User chewbacca = new User("id-4", "chewbacca"); + User yoda = new User("id-5", "yoda"); + yoda.setPosts(List.of(new Post("Do. Or do not. There is no try."), new Post("Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered."))); + + User vader = new User("id-6", "vader"); + vader.setFirstname("Anakin"); + vader.setLastname("Skywalker"); + vader.setPosts(List.of(new Post("I am your father"))); + + User kylo = new User("id-7", "kylo"); + kylo.setFirstname("Ben"); + kylo.setLastname("Solo"); + + repository.saveAll(List.of(luke, leia, han, chewbacca, yoda, vader, kylo)); + + System.out.println("------- annotated multi -------"); + System.out.println(repository.usersWithUsernamesStartingWith("l")); + + System.out.println("------- derived single -------"); + System.out.println(repository.findUserByUsername("yoda")); + + System.out.println("------- derived nested.path -------"); + System.out.println(repository.findUserByPostsMessageLike("father")); + + System.out.println("------- derived optional -------"); + System.out.println(repository.findOptionalUserByUsername("yoda")); + + System.out.println("------- derived count -------"); + Long count = repository.countUsersByLastnameLike("Sky"); + System.out.println("user count " + count); + + System.out.println("------- derived exists -------"); + Boolean exists = repository.existsByUsername("vader"); + System.out.println("user exists " + exists); + + System.out.println("------- derived multi -------"); + System.out.println(repository.findUserByLastnameLike("Sky")); + + System.out.println("------- derived sorted -------"); + System.out.println(repository.findUserByLastnameLikeOrderByFirstname("Sky")); + + System.out.println("------- derived page -------"); + Page page0 = repository.findUserByLastnameStartingWith("S", PageRequest.of(0, 2)); + System.out.println("page0: " + page0); + System.out.println("page0.content: " + page0.getContent()); + + Page page1 = repository.findUserByLastnameStartingWith("S", PageRequest.of(1, 2)); + System.out.println("page1: " + page1); + System.out.println("page1.content: " + page1.getContent()); + + System.out.println("------- derived slice -------"); + Slice slice0 = repository.findUserByUsernameAfter("luke", PageRequest.of(0, 2)); + System.out.println("slice0: " + slice0); + System.out.println("slice0.content: " + slice0.getContent()); + + System.out.println("------- derived top -------"); + System.out.println(repository.findTop2UsersByLastnameLike("S")); + + System.out.println("------- derived with fields -------"); + System.out.println(repository.findJustUsernameBy()); + } +} diff --git a/mongodb/aot-optimization/src/main/java/example/springdata/aot/Post.java b/mongodb/aot-optimization/src/main/java/example/springdata/aot/Post.java new file mode 100644 index 000000000..10f5068ba --- /dev/null +++ b/mongodb/aot-optimization/src/main/java/example/springdata/aot/Post.java @@ -0,0 +1,56 @@ +/* + * 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.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Random; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class Post { + + private String message; + private Instant date; + + public Post(String message) { + this.message = message; + this.date = Instant.now().minus(new Random().nextLong(100), ChronoUnit.MINUTES); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Instant getDate() { + return date; + } + + public void setDate(Instant date) { + this.date = date; + } + + @Override + public String toString() { + return message; + } +} diff --git a/mongodb/aot-optimization/src/main/java/example/springdata/aot/User.java b/mongodb/aot-optimization/src/main/java/example/springdata/aot/User.java new file mode 100644 index 000000000..56317490e --- /dev/null +++ b/mongodb/aot-optimization/src/main/java/example/springdata/aot/User.java @@ -0,0 +1,105 @@ +/* + * 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.Instant; +import java.util.List; + +import org.springframework.data.mongodb.core.mapping.Field; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class User { + + private final String id; + private final String username; + + @Field("first_name") String firstname; + @Field("last_name") String lastname; + + private List posts; + + Instant registrationDate; + Instant lastSeen; + + public User(String id, String username) { + this.id = id; + this.username = username; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Instant getRegistrationDate() { + return registrationDate; + } + + public void setRegistrationDate(Instant registrationDate) { + this.registrationDate = registrationDate; + } + + public Instant getLastSeen() { + return lastSeen; + } + + public void setLastSeen(Instant lastSeen) { + this.lastSeen = lastSeen; + } + + public List getPosts() { + return posts; + } + + public void setPosts(List posts) { + this.posts = posts; + } + + @Override + public String toString() { + return "User{" + + "id='" + id + '\'' + + ", username='" + username + '\'' + + ", firstname='" + firstname + '\'' + + ", lastname='" + lastname + '\'' + + ", registrationDate=" + registrationDate + + ", lastSeen=" + lastSeen + + ", posts=" + posts + + '}'; + } +} diff --git a/mongodb/aot-optimization/src/main/java/example/springdata/aot/UserProjection.java b/mongodb/aot-optimization/src/main/java/example/springdata/aot/UserProjection.java new file mode 100644 index 000000000..d243ba3a8 --- /dev/null +++ b/mongodb/aot-optimization/src/main/java/example/springdata/aot/UserProjection.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 + * + * 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.Instant; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserProjection { + + String getUsername(); + Instant getRegistrationDate(); +} diff --git a/mongodb/aot-optimization/src/main/java/example/springdata/aot/UserRepository.java b/mongodb/aot-optimization/src/main/java/example/springdata/aot/UserRepository.java new file mode 100644 index 000000000..6a935551e --- /dev/null +++ b/mongodb/aot-optimization/src/main/java/example/springdata/aot/UserRepository.java @@ -0,0 +1,59 @@ +/* + * 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 java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserRepository extends CrudRepository { + + User findUserByUsername(String username); + + Optional findOptionalUserByUsername(String username); + + Long countUsersByLastnameLike(String lastname); + + Boolean existsByUsername(String username); + + List findUserByLastnameLike(String lastname); + + List findUserByPostsMessageLike(String part); + + List findUserByLastnameLikeOrderByFirstname(String lastname); + + List findTop2UsersByLastnameLike(String lastname); + + Slice findUserByUsernameAfter(String username, Pageable pageable); + + Page findUserByLastnameStartingWith(String lastname, Pageable page); + + @Query("{ 'username' : { $regex: '?0.*', $options: 'i' } }") + List usersWithUsernamesStartingWith(String username); + + @Query(fields = "{ 'username' : 1 }", sort = "{ 'username' : -1 }") + List findJustUsernameBy(); + +} diff --git a/mongodb/aot-optimization/src/main/resources/application.properties b/mongodb/aot-optimization/src/main/resources/application.properties new file mode 100644 index 000000000..fb68fc02e --- /dev/null +++ b/mongodb/aot-optimization/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.aot.repositories.enabled=true diff --git a/mongodb/aot-optimization/src/main/resources/logback.xml b/mongodb/aot-optimization/src/main/resources/logback.xml new file mode 100644 index 000000000..1be6becb6 --- /dev/null +++ b/mongodb/aot-optimization/src/main/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + + + + + + + + + + + diff --git a/mongodb/change-streams/src/test/java/example/springdata/mongodb/ChangeStreamsTests.java b/mongodb/change-streams/src/test/java/example/springdata/mongodb/ChangeStreamsTests.java index dd8456c45..9714658a7 100644 --- a/mongodb/change-streams/src/test/java/example/springdata/mongodb/ChangeStreamsTests.java +++ b/mongodb/change-streams/src/test/java/example/springdata/mongodb/ChangeStreamsTests.java @@ -200,28 +200,31 @@ public void reactiveChangeEvents() { ChangeStreamOptions.builder().filter(newAggregation(match(where("operationType").is("insert")))).build(), Person.class); - StepVerifier.create(changeStream) // + changeStream.as(StepVerifier::create) // .expectSubscription() // .expectNoEvent(Duration.ofMillis(200)) // wait till change streams becomes active // Save documents and await their change events .then(() -> { - StepVerifier.create(reactiveTemplate.save(gabriel)).expectNextCount(1).verifyComplete(); - StepVerifier.create(reactiveTemplate.save(ash)).expectNextCount(1).verifyComplete(); + reactiveTemplate.save(gabriel).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + reactiveTemplate.save(ash).as(StepVerifier::create).expectNextCount(1).verifyComplete(); }).expectNextCount(2) // // Update a document .then(() -> { - StepVerifier.create(reactiveTemplate.update(Person.class) // + reactiveTemplate.update(Person.class) // .matching(query(where("id").is(ash.id()))) // .apply(update("age", 40)) // - .first()).expectNextCount(1).verifyComplete(); + .first() // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); }).expectNoEvent(Duration.ofMillis(200)) // updates are skipped // Save another document and await its change event .then(() -> { - StepVerifier.create(reactiveTemplate.save(michael)).expectNextCount(1).verifyComplete(); + reactiveTemplate.save(michael).as(StepVerifier::create).expectNextCount(1).verifyComplete(); }).expectNextCount(1) // there we go, all events received. .thenCancel() // change streams are infinite streams, at some point we need to unsubscribe diff --git a/mongodb/example/src/main/java/example/springdata/mongodb/customer/CustomerRepository.java b/mongodb/example/src/main/java/example/springdata/mongodb/customer/CustomerRepository.java index ef239160b..8a94fa5ef 100644 --- a/mongodb/example/src/main/java/example/springdata/mongodb/customer/CustomerRepository.java +++ b/mongodb/example/src/main/java/example/springdata/mongodb/customer/CustomerRepository.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.stream.Stream; +import org.springframework.data.domain.Limit; import org.springframework.data.domain.Sort; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; @@ -41,6 +42,15 @@ public interface CustomerRepository extends CrudRepository { */ List findByLastname(String lastname, Sort sort); + /** + * Derived query reducing result size to a given {@link Limit}. + * + * @param lastname + * @param maxResults the maximum number of results returned. + * @return + */ + List findByLastname(String lastname, Limit maxResults); + /** * Showcase for a repository query using geospatial functionality. * diff --git a/mongodb/example/src/test/java/example/springdata/mongodb/advanced/ServersideScriptTests.java b/mongodb/example/src/test/java/example/springdata/mongodb/advanced/ServersideScriptTests.java deleted file mode 100644 index c11cbf441..000000000 --- a/mongodb/example/src/test/java/example/springdata/mongodb/advanced/ServersideScriptTests.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2015-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.mongodb.advanced; - -import static org.assertj.core.api.Assertions.*; - -import example.springdata.mongodb.customer.Customer; - -import java.util.Map; - -import org.bson.Document; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.script.ExecutableMongoScript; -import org.springframework.data.mongodb.core.script.NamedMongoScript; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.MongoDBContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; - -/** - * @author Christoph Strobl - * @author Oliver Gierke - */ -@Testcontainers -@DataMongoTest -class ServersideScriptTests { - - @Container // - private static MongoDBContainer mongoDBContainer = new MongoDBContainer( - DockerImageName.parse("mongo:3.6")); - - @DynamicPropertySource - static void setProperties(DynamicPropertyRegistry registry) { - registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); - } - - @Autowired AdvancedRepository repository; - @Autowired MongoOperations operations; - - @BeforeEach - void setUp() { - - if (!operations.collectionExists(Customer.class)) { - operations.createCollection(Customer.class); - } - - // just make sure we remove everything properly - operations.getCollection("system.js").deleteMany(new Document()); - repository.deleteAll(); - } - - /** - * Store and call an arbitrary JavaScript function (in this case a simple echo script) via its name. - */ - @Test - void saveAndCallScriptViaName() { - - operations.scriptOps() - .register(new NamedMongoScript("echoScript", new ExecutableMongoScript("function(x) { return x; }"))); - - assertThat(operations.scriptOps().call("echoScript", "Hello echo...!")).isEqualTo("Hello echo...!"); - } - - /** - * Use a script execution to create an atomic put-if-absent operation that fulfills the contract of - * {@link Map#putIfAbsent(Object, Object)} - */ - @Test - @Disabled - void complexScriptExecutionSimulatingPutIfAbsent() { - - var ned = new Customer("Ned", "Stark"); - ned.setId("ned-stark"); - - // #1: on first insert null has to be returned - assertThat(operations.scriptOps().execute(createExecutablePutIfAbsentScript(ned))).isNotNull(); - - // #2: change the firstname and put the object again, we expect a return value. - ned.setFirstname("Eddard"); - assertThat(operations.scriptOps().execute(createExecutablePutIfAbsentScript(ned))).isNotNull(); - - // #3: make sure the entity has not been altered by #2 - assertThat(repository.findById(ned.getId())) - .hasValueSatisfying(it -> assertThat(it.getFirstname()).isEqualTo("Ned")); - assertThat(repository.count()).isEqualTo(1L); - } - - private ExecutableMongoScript createExecutablePutIfAbsentScript(Customer customer) { - - var collectionName = operations.getCollectionName(Customer.class); - var id = operations.getConverter().getMappingContext().getRequiredPersistentEntity(Customer.class) - .getIdentifierAccessor(customer).getIdentifier(); - - var document = new Document(); - operations.getConverter().write(customer, document); - - var scriptString = String.format( - "object = db.%1$s.findOne({\"_id\": \"%2$s\"}); if (object == null) { db.%1s.insert(%3$s); return null; } else { return object; }", - collectionName, id, document); - - return new ExecutableMongoScript(scriptString); - } -} diff --git a/mongodb/example/src/test/java/example/springdata/mongodb/customer/CustomerRepositoryIntegrationTest.java b/mongodb/example/src/test/java/example/springdata/mongodb/customer/CustomerRepositoryIntegrationTest.java index 2c2352d3f..5a3e76dd6 100644 --- a/mongodb/example/src/test/java/example/springdata/mongodb/customer/CustomerRepositoryIntegrationTest.java +++ b/mongodb/example/src/test/java/example/springdata/mongodb/customer/CustomerRepositoryIntegrationTest.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; +import org.springframework.data.domain.Limit; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; @@ -96,6 +97,17 @@ void findCustomersUsingQuerydslSort() { assertThat(result.get(1)).isEqualTo(oliver); } + /** + * Test case to show how to reduce result size with dynamic {@link Limit}. + */ + @Test + void limitResultSize() { + + var result = repository.findByLastname("Matthews", Limit.of(1)); + + assertThat(result).hasSize(1); + } + /** * Test case to show the usage of Java {@link Stream}. */ diff --git a/mongodb/fragment-spi/README.adoc b/mongodb/fragment-spi/README.adoc new file mode 100644 index 000000000..95eb32063 --- /dev/null +++ b/mongodb/fragment-spi/README.adoc @@ -0,0 +1,40 @@ += Spring Data - Fragment SPI Example + +This project contains a sample using `spring.factories` to register implementation details for a repository extension for MongoDB Vector Search that lives outside the project namespace. + +The project is divided into the `atlas-api`, providing the extension, and the `sample` using it. + +== atlas-api + +The `AtlasRepository` is the base interface containing a `vectorSearch` method that is implemented in `AtlasRepositoryFragment`. The configuration in `src/main/resources/META-INF/spring.factories` makes sure it is picked up by the spring data infrastructure. + +The implementation leverages `RepositoryMethodContext` to get hold of method invocation metadata to determine the collection name derived from the repositories domain type ``. +Since providing the metadata needs to be explicitly activated the `AtlasRepositoryFragment` uses the additional marker interface `RepositoryMetadataAccess` enabling the features for repositories extending the `AtlasRepository`. + +== sample + +The `MovieRepository` extends the `AtlasRepository` from the api project using a `Movie` type targeting the `movies` collection. No further configuration is needed to use the provided `vectorSearch` within the `MovieRepositoryTests`. + +The `Movies` class in `src/main/test` takes care of setting up required test data and indexes. + +== Running the sample + +The is using a local MongoDB Atlas instance bootstrapped by Testcontainers. +Running the `MovieRepositoryTests` the `test/movies` collection will be populated with about 400 entries from the `mflix.embedded_movies.json.gz` file. +Please be patient while data is loaded into the database and the index created afterward. +Progress information will be printed to the log. + +[source,log] +---- +INFO - com.example.data.mongodb.Movies: 73 - Loading movies from class path resource [mflix.embedded_movies.json.gz] +INFO - com.example.data.mongodb.Movies: 90 - Created 420 movies in test.movies +INFO - com.example.data.mongodb.Movies: 65 - creating vector index +INFO - com.example.data.mongodb.Movies: 68 - index 'plot_vector_index' created +---- + +Once data and index are available search result will be printed: + +[source,log] +---- +INFO - ...mongodb.MovieRepositoryTests: 183 - Movie{id='66d6ee0937e07b74aa2939cc', ... +---- diff --git a/mongodb/fragment-spi/atlas-api/pom.xml b/mongodb/fragment-spi/atlas-api/pom.xml new file mode 100644 index 000000000..d7b01df81 --- /dev/null +++ b/mongodb/fragment-spi/atlas-api/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + + + org.springframework.data.examples + spring-data-mongodb-fragment-spi + 2.0.0.BUILD-SNAPSHOT + + + spring-data-mongodb-fragment-spi-atlas + Spring Data MongoDB - Reusable Fragments - Vector Search Fragment + + diff --git a/mongodb/fragment-spi/atlas-api/src/main/java/com/example/spi/mongodb/atlas/AtlasRepository.java b/mongodb/fragment-spi/atlas-api/src/main/java/com/example/spi/mongodb/atlas/AtlasRepository.java new file mode 100644 index 000000000..1dc0f8e47 --- /dev/null +++ b/mongodb/fragment-spi/atlas-api/src/main/java/com/example/spi/mongodb/atlas/AtlasRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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.spi.mongodb.atlas; + +import java.util.List; + + +/** + * @author Christoph Strobl + */ +public interface AtlasRepository { + + List vectorSearch(String index, String path, List vector); +} diff --git a/mongodb/fragment-spi/atlas-api/src/main/java/com/example/spi/mongodb/atlas/AtlasRepositoryFragment.java b/mongodb/fragment-spi/atlas-api/src/main/java/com/example/spi/mongodb/atlas/AtlasRepositoryFragment.java new file mode 100644 index 000000000..f8f45c865 --- /dev/null +++ b/mongodb/fragment-spi/atlas-api/src/main/java/com/example/spi/mongodb/atlas/AtlasRepositoryFragment.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 com.example.spi.mongodb.atlas; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ResolvableType; +import org.springframework.data.domain.Limit; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.RepositoryMethodContext; +import org.springframework.data.repository.core.support.RepositoryMetadataAccess; + +class AtlasRepositoryFragment implements AtlasRepository, RepositoryMetadataAccess { + + private final MongoOperations mongoOperations; + + public AtlasRepositoryFragment(@Autowired MongoOperations mongoOperations) { + this.mongoOperations = mongoOperations; + } + + @Override + @SuppressWarnings("unchecked") + public List vectorSearch(String index, String path, List vector) { + + RepositoryMethodContext methodContext = RepositoryMethodContext.getContext(); + + Class domainType = resolveDomainType(methodContext.getMetadata()); + + VectorSearchOperation $vectorSearch = VectorSearchOperation.search(index).path(path).vector(vector) + .limit(Limit.of(10)).numCandidates(150); + + Aggregation aggregation = Aggregation.newAggregation($vectorSearch); + + return (List) mongoOperations.aggregate(aggregation, mongoOperations.getCollectionName(domainType), domainType).getMappedResults(); + } + + @SuppressWarnings("unchecked") + private static Class resolveDomainType(RepositoryMetadata metadata) { + + // resolve the actual generic type argument of the AtlasRepository. + return (Class) ResolvableType.forClass(metadata.getRepositoryInterface()) + .as(AtlasRepository.class) + .getGeneric(0) + .resolve(); + } + +} diff --git a/mongodb/fragment-spi/atlas-api/src/main/resources/META-INF/spring.factories b/mongodb/fragment-spi/atlas-api/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..cddeae58c --- /dev/null +++ b/mongodb/fragment-spi/atlas-api/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +com.example.spi.mongodb.atlas.AtlasRepository=com.example.spi.mongodb.atlas.AtlasRepositoryFragment diff --git a/mongodb/fragment-spi/pom.xml b/mongodb/fragment-spi/pom.xml new file mode 100644 index 000000000..e6d1b8675 --- /dev/null +++ b/mongodb/fragment-spi/pom.xml @@ -0,0 +1,19 @@ + + 4.0.0 + + + org.springframework.data.examples + spring-data-mongodb-examples + 2.0.0.BUILD-SNAPSHOT + + + spring-data-mongodb-fragment-spi + Spring Data MongoDB - Reusable Fragments + pom + + + atlas-api + sample + + diff --git a/mongodb/fragment-spi/sample/pom.xml b/mongodb/fragment-spi/sample/pom.xml new file mode 100644 index 000000000..0246d9fe2 --- /dev/null +++ b/mongodb/fragment-spi/sample/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + + org.springframework.data.examples + spring-data-mongodb-fragment-spi + 2.0.0.BUILD-SNAPSHOT + + + spring-data-mongodb-fragment-spi-usage + Spring Data MongoDB - Reusable Fragments - Fragment Usage + + + + org.springframework.data.examples + spring-data-mongodb-fragment-spi-atlas + ${project.version} + + + org.springframework.data.examples + spring-data-mongodb-example-utils + test + + + diff --git a/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/ApplicationConfiguration.java b/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/ApplicationConfiguration.java new file mode 100644 index 000000000..0d553155d --- /dev/null +++ b/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/ApplicationConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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.data.mongodb; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Christoph Strobl + */ +@SpringBootApplication +public class ApplicationConfiguration { + +} diff --git a/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/Movie.java b/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/Movie.java new file mode 100644 index 000000000..6af282b00 --- /dev/null +++ b/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/Movie.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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.data.mongodb; + +import org.springframework.data.mongodb.core.mapping.Document; + +/** + * @author Christoph Strobl + */ +@Document("movies") +public class Movie { + + private String id; + private String title; + private String plot; + + public String getPlot() { + return plot; + } + + public void setPlot(String plot) { + this.plot = plot; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return "Movie{" + + "id='" + id + '\'' + + ", title='" + title + '\'' + + ", plot='" + plot + '\'' + + '}'; + } +} diff --git a/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/MovieRepository.java b/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/MovieRepository.java new file mode 100644 index 000000000..4ef73c58f --- /dev/null +++ b/mongodb/fragment-spi/sample/src/main/java/com/example/data/mongodb/MovieRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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.data.mongodb; + +import com.example.spi.mongodb.atlas.AtlasRepository; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + */ +public interface MovieRepository extends CrudRepository, AtlasRepository { + +} diff --git a/mongodb/fragment-spi/sample/src/main/resources/application.properties b/mongodb/fragment-spi/sample/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/mongodb/fragment-spi/sample/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/mongodb/fragment-spi/sample/src/test/java/com/example/data/mongodb/MovieRepositoryTests.java b/mongodb/fragment-spi/sample/src/test/java/com/example/data/mongodb/MovieRepositoryTests.java new file mode 100644 index 000000000..747cbcbe0 --- /dev/null +++ b/mongodb/fragment-spi/sample/src/test/java/com/example/data/mongodb/MovieRepositoryTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2024 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 com.example.data.mongodb; + +import example.springdata.mongodb.util.AtlasContainer; +import example.springdata.mongodb.util.MongoContainers; + +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.Resource; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.mongodb.client.MongoClient; + +/** + * Integration test for MongoDB Atlas Vector Search. + * + * @author Christoph Strobl + */ +@SpringBootTest +@Testcontainers +class MovieRepositoryTests { + + private static final Logger log = LoggerFactory.getLogger(MovieRepositoryTests.class); + + private static @Container AtlasContainer atlasLocal = MongoContainers.getAtlasContainer(); + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.uri", atlasLocal::getConnectionString); + } + + @Value("classpath:/mflix.embedded_movies.json.gz") Resource moviesResource; + @Autowired MovieRepository repository; + @Autowired MongoClient client; + + @BeforeEach + void setUp() { + + Movies movies = new Movies(client); + if (!movies.alreadyInitialized()) { + movies.initialize(moviesResource); + } + } + + @Test + void testVectorSearch() { + + List result = repository.vectorSearch("plot_vector_index", "plot_embedding", List.of(vectors)); + result.stream().map(Objects::toString).forEach(log.atLevel(Level.INFO)::log); + } + + private static final Double[] vectors = { -0.0016261312, -0.028070757, -0.011342932, -0.012775794, -0.0027440966, + 0.008683807, -0.02575152, -0.02020668, -0.010283281, -0.0041719596, 0.021392956, 0.028657231, -0.006634482, + 0.007490867, 0.018593878, 0.0038187427, 0.029590257, -0.01451522, 0.016061379, 0.00008528442, -0.008943722, + 0.01627464, 0.024311995, -0.025911469, 0.00022596726, -0.008863748, 0.008823762, -0.034921836, 0.007910728, + -0.01515501, 0.035801545, -0.0035688248, -0.020299982, -0.03145631, -0.032256044, -0.028763862, -0.0071576433, + -0.012769129, 0.012322609, -0.006621153, 0.010583182, 0.024085402, -0.001623632, 0.007864078, -0.021406285, + 0.002554159, 0.012229307, -0.011762793, 0.0051682983, 0.0048484034, 0.018087378, 0.024325324, -0.037694257, + -0.026537929, -0.008803768, -0.017767483, -0.012642504, -0.0062712682, 0.0009771782, -0.010409906, 0.017754154, + -0.004671795, -0.030469967, 0.008477209, -0.005218282, -0.0058480743, -0.020153364, -0.0032805866, 0.004248601, + 0.0051449724, 0.006791097, 0.007650814, 0.003458861, -0.0031223053, -0.01932697, -0.033615597, 0.00745088, + 0.006321252, -0.0038154104, 0.014555207, 0.027697546, -0.02828402, 0.0066711367, 0.0077107945, 0.01794076, + 0.011349596, -0.0052715978, 0.014755142, -0.019753495, -0.011156326, 0.011202978, 0.022126047, 0.00846388, + 0.030549942, -0.0041386373, 0.018847128, -0.00033655585, 0.024925126, -0.003555496, -0.019300312, 0.010749794, + 0.0075308536, -0.018287312, -0.016567878, -0.012869096, -0.015528221, 0.0078107617, -0.011156326, 0.013522214, + -0.020646535, -0.01211601, 0.055928253, 0.011596181, -0.017247654, 0.0005939711, -0.026977783, -0.003942035, + -0.009583511, -0.0055248477, -0.028737204, 0.023179034, 0.003995351, 0.0219661, -0.008470545, 0.023392297, + 0.010469886, -0.015874773, 0.007890735, -0.009690142, -0.00024970944, 0.012775794, 0.0114762215, 0.013422247, + 0.010429899, -0.03686786, -0.006717788, -0.027484283, 0.011556195, -0.036068123, -0.013915418, -0.0016327957, + 0.0151016945, -0.020473259, 0.004671795, -0.012555866, 0.0209531, 0.01982014, 0.024485271, 0.0105431955, + -0.005178295, 0.033162415, -0.013795458, 0.007150979, 0.010243294, 0.005644808, 0.017260984, -0.0045618312, + 0.0024725192, 0.004305249, -0.008197301, 0.0014203656, 0.0018460588, 0.005015015, -0.011142998, 0.01439526, + 0.022965772, 0.02552493, 0.007757446, -0.0019726837, 0.009503538, -0.032042783, 0.008403899, -0.04609149, + 0.013808787, 0.011749465, 0.036388017, 0.016314628, 0.021939443, -0.0250051, -0.017354285, -0.012962398, + 0.00006107364, 0.019113706, 0.03081652, -0.018114036, -0.0084572155, 0.009643491, -0.0034721901, 0.0072642746, + -0.0090636825, 0.01642126, 0.013428912, 0.027724205, 0.0071243206, -0.6858542, -0.031029783, -0.014595194, + -0.011449563, 0.017514233, 0.01743426, 0.009950057, 0.0029706885, -0.015714826, -0.001806072, 0.011856096, + 0.026444625, -0.0010663156, -0.006474535, 0.0016161345, -0.020313311, 0.0148351155, -0.0018393943, 0.0057347785, + 0.018300641, -0.018647194, 0.03345565, -0.008070676, 0.0071443142, 0.014301958, 0.0044818576, 0.003838736, + -0.007350913, -0.024525259, -0.001142124, -0.018620536, 0.017247654, 0.007037683, 0.010236629, 0.06046009, + 0.0138887605, -0.012122675, 0.037694257, 0.0055081863, 0.042492677, 0.00021784494, -0.011656162, 0.010276617, + 0.022325981, 0.005984696, -0.009496873, 0.013382261, -0.0010563189, 0.0026507939, -0.041639622, 0.008637156, + 0.026471283, -0.008403899, 0.024858482, -0.00066686375, -0.0016252982, 0.027590916, 0.0051449724, 0.0058647357, + -0.008743787, -0.014968405, 0.027724205, -0.011596181, 0.0047650975, -0.015381602, 0.0043718936, 0.002159289, + 0.035908177, -0.008243952, -0.030443309, 0.027564257, 0.042625964, -0.0033688906, 0.01843393, 0.019087048, + 0.024578573, 0.03268257, -0.015608194, -0.014128681, -0.0033538956, -0.0028757197, -0.004121976, -0.032389335, + 0.0034322033, 0.058807302, 0.010943064, -0.030523283, 0.008903735, 0.017500903, 0.00871713, -0.0029406983, + 0.013995391, -0.03132302, -0.019660193, -0.00770413, -0.0038853872, 0.0015894766, -0.0015294964, -0.006251275, + -0.021099718, -0.010256623, -0.008863748, 0.028550599, 0.02020668, -0.0012962399, -0.003415542, -0.0022509254, + 0.0119360695, 0.027590916, -0.046971202, -0.0015194997, -0.022405956, 0.0016677842, -0.00018535563, -0.015421589, + -0.031802863, 0.03814744, 0.0065411795, 0.016567878, -0.015621523, 0.022899127, -0.011076353, 0.02841731, + -0.002679118, -0.002342562, 0.015341615, 0.01804739, -0.020566562, -0.012989056, -0.002990682, 0.01643459, + 0.00042527664, 0.008243952, -0.013715484, -0.004835075, -0.009803439, 0.03129636, -0.021432944, 0.0012087687, + -0.015741484, -0.0052016205, 0.00080890034, -0.01755422, 0.004811749, -0.017967418, -0.026684547, -0.014128681, + 0.0041386373, -0.013742141, -0.010056688, -0.013268964, -0.0110630235, -0.028337335, 0.015981404, -0.00997005, + -0.02424535, -0.013968734, -0.028310679, -0.027750863, -0.020699851, 0.02235264, 0.001057985, 0.00081639783, + -0.0099367285, 0.013522214, -0.012016043, -0.00086471526, 0.013568865, 0.0019376953, -0.019020405, 0.017460918, + -0.023045745, 0.008503866, 0.0064678704, -0.011509543, 0.018727167, -0.003372223, -0.0028690554, -0.0027024434, + -0.011902748, -0.012182655, -0.015714826, -0.0098634185, 0.00593138, 0.018753825, 0.0010146659, 0.013029044, + 0.0003521757, -0.017620865, 0.04102649, 0.00552818, 0.024485271, -0.009630162, -0.015608194, 0.0006718621, + -0.0008418062, 0.012395918, 0.0057980907, 0.016221326, 0.010616505, 0.004838407, -0.012402583, 0.019900113, + -0.0034521967, 0.000247002, -0.03153628, 0.0011038032, -0.020819811, 0.016234655, -0.00330058, -0.0032289368, + 0.00078973995, -0.021952773, -0.022459272, 0.03118973, 0.03673457, -0.021472929, 0.0072109587, -0.015075036, + 0.004855068, -0.0008151483, 0.0069643734, 0.010023367, -0.010276617, -0.023019087, 0.0068244194, -0.0012520878, + -0.0015086699, 0.022046074, -0.034148756, -0.0022192693, 0.002427534, -0.0027124402, 0.0060346797, 0.015461575, + 0.0137554705, 0.009230294, -0.009583511, 0.032629255, 0.015994733, -0.019167023, -0.009203636, 0.03393549, + -0.017274313, -0.012042701, -0.0009930064, 0.026777849, -0.013582194, -0.0027590916, -0.017594207, -0.026804507, + -0.0014236979, -0.022032745, 0.0091236625, -0.0042419364, -0.00858384, -0.0033905501, -0.020739838, 0.016821127, + 0.022539245, 0.015381602, 0.015141681, 0.028817179, -0.019726837, -0.0051283115, -0.011489551, -0.013208984, + -0.0047017853, -0.0072309524, 0.01767418, 0.0025658219, -0.010323267, 0.012609182, -0.028097415, 0.026871152, + -0.010276617, 0.021912785, 0.0022542577, 0.005124979, -0.0019710176, 0.004518512, -0.040360045, 0.010969722, + -0.0031539614, -0.020366628, -0.025778178, -0.0110030435, -0.016221326, 0.0036587953, 0.016207997, 0.003007343, + -0.0032555948, 0.0044052163, -0.022046074, -0.0008822095, -0.009363583, 0.028230704, -0.024538586, 0.0029840174, + 0.0016044717, -0.014181997, 0.031349678, -0.014381931, -0.027750863, 0.02613806, 0.0004136138, -0.005748107, + -0.01868718, -0.0010138329, 0.0054348772, 0.010703143, -0.003682121, 0.0030856507, -0.004275259, -0.010403241, + 0.021113047, -0.022685863, -0.023032416, 0.031429652, 0.001792743, -0.005644808, -0.011842767, -0.04078657, + -0.0026874484, 0.06915057, -0.00056939584, -0.013995391, 0.010703143, -0.013728813, -0.022939114, -0.015261642, + -0.022485929, 0.016807798, 0.007964044, 0.0144219175, 0.016821127, 0.0076241563, 0.005461535, -0.013248971, + 0.015301628, 0.0085171955, -0.004318578, 0.011136333, -0.0059047225, -0.010249958, -0.018207338, 0.024645219, + 0.021752838, 0.0007614159, -0.013648839, 0.01111634, -0.010503208, -0.0038487327, -0.008203966, -0.00397869, + 0.0029740208, 0.008530525, 0.005261601, 0.01642126, -0.0038753906, -0.013222313, 0.026537929, 0.024671877, + -0.043505676, 0.014195326, 0.024778508, 0.0056914594, -0.025951454, 0.017620865, -0.0021359634, 0.008643821, + 0.021299653, 0.0041686273, -0.009017031, 0.04044002, 0.024378639, -0.027777521, -0.014208655, 0.0028623908, + 0.042119466, 0.005801423, -0.028124074, -0.03129636, 0.022139376, -0.022179363, -0.04067994, 0.013688826, + 0.013328944, 0.0046184794, -0.02828402, -0.0063412455, -0.0046184794, -0.011756129, -0.010383247, -0.0018543894, + -0.0018593877, -0.00052024535, 0.004815081, 0.014781799, 0.018007403, 0.01306903, -0.020433271, 0.009043689, + 0.033189073, -0.006844413, -0.019766824, -0.018767154, 0.00533491, -0.0024575242, 0.018727167, 0.0058080875, + -0.013835444, 0.0040719924, 0.004881726, 0.012029372, 0.005664801, 0.03193615, 0.0058047553, 0.002695779, + 0.009290274, 0.02361889, 0.017834127, 0.0049017193, -0.0036388019, 0.010776452, -0.019793482, 0.0067777685, + -0.014208655, -0.024911797, 0.002385881, 0.0034988478, 0.020899786, -0.0025858153, -0.011849431, 0.033189073, + -0.021312982, 0.024965113, -0.014635181, 0.014048708, -0.0035921505, -0.003347231, 0.030869836, -0.0017161017, + -0.0061346465, 0.009203636, -0.025165047, 0.0068510775, 0.021499587, 0.013782129, -0.0024475274, -0.0051149824, + -0.024445284, 0.006167969, 0.0068844, -0.00076183246, 0.030150073, -0.0055948244, -0.011162991, -0.02057989, + -0.009703471, -0.020646535, 0.008004031, 0.0066378145, -0.019900113, -0.012169327, -0.01439526, 0.0044252095, + -0.004018677, 0.014621852, -0.025085073, -0.013715484, -0.017980747, 0.0071043274, 0.011456228, -0.01010334, + -0.0035321703, -0.03801415, -0.012036037, -0.0028990454, -0.05419549, -0.024058744, -0.024272008, 0.015221654, + 0.027964126, 0.03182952, -0.015354944, 0.004855068, 0.011522872, 0.004771762, 0.0027874154, 0.023405626, + 0.0004242353, -0.03132302, 0.007057676, 0.008763781, -0.0027057757, 0.023005757, -0.0071176565, -0.005238275, + 0.029110415, -0.010989714, 0.013728813, -0.009630162, -0.029137073, -0.0049317093, -0.0008630492, -0.015248313, + 0.0043219104, -0.0055681667, -0.013175662, 0.029723546, 0.025098402, 0.012849103, -0.0009996708, 0.03118973, + -0.0021709518, 0.0260181, -0.020526575, 0.028097415, -0.016141351, 0.010509873, -0.022965772, 0.002865723, + 0.0020493253, 0.0020509914, -0.0041419696, -0.00039695262, 0.017287642, 0.0038987163, 0.014795128, -0.014661839, + -0.008950386, 0.004431874, -0.009383577, 0.0012604183, -0.023019087, 0.0029273694, -0.033135757, 0.009176978, + -0.011023037, -0.002102641, 0.02663123, -0.03849399, -0.0044152127, 0.0004527676, -0.0026924468, 0.02828402, + 0.017727496, 0.035135098, 0.02728435, -0.005348239, -0.001467017, -0.019766824, 0.014715155, 0.011982721, + 0.0045651635, 0.023458943, -0.0010046692, -0.0031373003, -0.0006972704, 0.0019043729, -0.018967088, -0.024311995, + 0.0011546199, 0.007977373, -0.004755101, -0.010016702, -0.02780418, -0.004688456, 0.013022379, -0.005484861, + 0.0017227661, -0.015394931, -0.028763862, -0.026684547, 0.0030589928, -0.018513903, 0.028363993, 0.0044818576, + -0.009270281, 0.038920518, -0.016008062, 0.0093902415, 0.004815081, -0.021059733, 0.01451522, -0.0051583014, + 0.023765508, -0.017874114, -0.016821127, -0.012522544, -0.0028390652, 0.0040886537, 0.020259995, -0.031216389, + -0.014115352, -0.009176978, 0.010303274, 0.020313311, 0.0064112223, -0.02235264, -0.022872468, 0.0052449396, + 0.0005723116, 0.0037321046, 0.016807798, -0.018527232, -0.009303603, 0.0024858483, -0.0012662497, -0.007110992, + 0.011976057, -0.007790768, -0.042999174, -0.006727785, -0.011829439, 0.007024354, 0.005278262, -0.017740825, + -0.0041519664, 0.0085905045, 0.027750863, -0.038387362, 0.024391968, 0.00087721116, 0.010509873, -0.00038508154, + -0.006857742, 0.0183273, -0.0037054466, 0.015461575, 0.0017394272, -0.0017944091, 0.014181997, -0.0052682655, + 0.009023695, 0.00719763, -0.013522214, 0.0034422, 0.014941746, -0.0016711164, -0.025298337, -0.017634194, + 0.0058714002, -0.005321581, 0.017834127, 0.0110630235, -0.03369557, 0.029190388, -0.008943722, 0.009363583, + -0.0034222065, -0.026111402, -0.007037683, -0.006561173, 0.02473852, -0.007084334, -0.010110005, -0.008577175, + 0.0030439978, -0.022712521, 0.0054582027, -0.0012620845, -0.0011954397, -0.015741484, 0.0129557345, + -0.00042111133, 0.00846388, 0.008930393, 0.016487904, 0.010469886, -0.007917393, -0.011762793, -0.0214596, + 0.000917198, 0.021672864, 0.010269952, -0.007737452, -0.010243294, -0.0067244526, -0.015488233, -0.021552904, + 0.017127695, 0.011109675, 0.038067464, 0.00871713, -0.0025591573, 0.021312982, -0.006237946, 0.034628596, + -0.0045251767, 0.008357248, 0.020686522, 0.0010696478, 0.0076708077, 0.03772091, -0.018700508, -0.0020676525, + -0.008923728, -0.023298996, 0.018233996, -0.010256623, 0.0017860786, 0.009796774, -0.00897038, -0.01269582, + -0.018527232, 0.009190307, -0.02372552, -0.042119466, 0.008097334, -0.0066778013, -0.021046404, 0.0019593548, + 0.011083017, -0.0016028056, 0.012662497, -0.000059095124, 0.0071043274, -0.014675168, 0.024831824, -0.053582355, + 0.038387362, 0.0005698124, 0.015954746, 0.021552904, 0.031589597, -0.009230294, -0.0006147976, 0.002625802, + -0.011749465, -0.034362018, -0.0067844326, -0.018793812, 0.011442899, -0.008743787, 0.017474247, -0.021619547, + 0.01831397, -0.009037024, -0.0057247817, -0.02728435, 0.010363255, 0.034415334, -0.024032086, -0.0020126705, + -0.0045518344, -0.019353628, -0.018340627, -0.03129636, -0.0034038792, -0.006321252, -0.0016161345, 0.033642255, + -0.000056075285, -0.005005019, 0.004571828, -0.0024075406, -0.00010215386, 0.0098634185, 0.1980148, -0.003825407, + -0.025191706, 0.035161756, 0.005358236, 0.025111731, 0.023485601, 0.0023342315, -0.011882754, 0.018287312, + -0.0068910643, 0.003912045, 0.009243623, -0.001355387, -0.028603915, -0.012802451, -0.030150073, -0.014795128, + -0.028630573, -0.0013487226, 0.002667455, 0.00985009, -0.0033972147, -0.021486258, 0.009503538, -0.017847456, + 0.013062365, -0.014341944, 0.005078328, 0.025165047, -0.015594865, -0.025924796, -0.0018177348, 0.010996379, + -0.02993681, 0.007324255, 0.014475234, -0.028577257, 0.005494857, 0.00011725306, -0.013315615, 0.015941417, + 0.009376912, 0.0025158382, 0.008743787, 0.023832154, -0.008084005, -0.014195326, -0.008823762, 0.0033455652, + -0.032362677, -0.021552904, -0.0056081535, 0.023298996, -0.025444955, 0.0097301295, 0.009736794, 0.015274971, + -0.0012937407, -0.018087378, -0.0039387033, 0.008637156, -0.011189649, -0.00023846315, -0.011582852, 0.0066411467, + -0.018220667, 0.0060846633, 0.0376676, -0.002709108, 0.0072776037, 0.0034188742, -0.010249958, -0.0007747449, + -0.00795738, -0.022192692, 0.03910712, 0.032122757, 0.023898797, 0.0076241563, -0.007397564, -0.003655463, + 0.011442899, -0.014115352, -0.00505167, -0.031163072, 0.030336678, -0.006857742, -0.022259338, 0.004048667, + 0.02072651, 0.0030156737, -0.0042119464, 0.00041861215, -0.005731446, 0.011103011, 0.013822115, 0.021512916, + 0.009216965, -0.006537847, -0.027057758, -0.04054665, 0.010403241, -0.0056281467, -0.005701456, -0.002709108, + -0.00745088, -0.0024841821, 0.009356919, -0.022659205, 0.004061996, -0.013175662, 0.017074378, -0.006141311, + -0.014541878, 0.02993681, -0.00028448965, -0.025271678, 0.011689484, -0.014528549, 0.004398552, -0.017274313, + 0.0045751603, 0.012455898, 0.004121976, -0.025458284, -0.006744446, 0.011822774, -0.015035049, -0.03257594, + 0.014675168, -0.0039187097, 0.019726837, -0.0047251107, 0.0022825818, 0.011829439, 0.005391558, -0.016781142, + -0.0058747325, 0.010309938, -0.013049036, 0.01186276, -0.0011246296, 0.0062112883, 0.0028190718, -0.021739509, + 0.009883412, -0.0073175905, -0.012715813, -0.017181009, -0.016607866, -0.042492677, -0.0014478565, -0.01794076, + 0.012302616, -0.015194997, -0.04433207, -0.020606548, 0.009696807, 0.010303274, -0.01694109, -0.004018677, + 0.019353628, -0.001991011, 0.000058938927, 0.010536531, -0.17274313, 0.010143327, 0.014235313, -0.024152048, + 0.025684876, -0.0012504216, 0.036601283, -0.003698782, 0.0007310093, 0.004165295, -0.0029157067, 0.017101036, + -0.046891227, -0.017460918, 0.022965772, 0.020233337, -0.024072073, 0.017220996, 0.009370248, 0.0010363255, + 0.0194336, -0.019606877, 0.01818068, -0.020819811, 0.007410893, 0.0019326969, 0.017887443, 0.006651143, + 0.00067394477, -0.011889419, -0.025058415, -0.008543854, 0.021579562, 0.0047484366, 0.014062037, 0.0075508473, + -0.009510202, -0.009143656, 0.0046817916, 0.013982063, -0.0027990784, 0.011782787, 0.014541878, -0.015701497, + -0.029350337, 0.021979429, 0.01332228, -0.026244693, -0.0123492675, -0.003895384, 0.0071576433, -0.035454992, + -0.00046984528, 0.0033522295, 0.039347045, 0.0005119148, 0.00476843, -0.012995721, 0.0024042083, -0.006931051, + -0.014461905, -0.0127558, 0.0034555288, -0.0074842023, -0.030256703, -0.007057676, -0.00807734, 0.007804097, + -0.006957709, 0.017181009, -0.034575284, -0.008603834, -0.005008351, -0.015834786, 0.02943031, 0.016861115, + -0.0050849924, 0.014235313, 0.0051449724, 0.0025924798, -0.0025741523, 0.04289254, -0.002104307, 0.012969063, + -0.008310596, 0.00423194, 0.0074975314, 0.0018810473, -0.014248641, -0.024725191, 0.0151016945, -0.017527562, + 0.0018727167, 0.0002830318, 0.015168339, 0.0144219175, -0.004048667, -0.004358565, 0.011836103, -0.010343261, + -0.005911387, 0.0022825818, 0.0073175905, 0.00403867, 0.013188991, 0.03334902, 0.006111321, 0.008597169, + 0.030123414, -0.015474904, 0.0017877447, -0.024551915, 0.013155668, 0.023525586, -0.0255116, 0.017220996, + 0.004358565, -0.00934359, 0.0099967085, 0.011162991, 0.03092315, -0.021046404, -0.015514892, 0.0011946067, + -0.01816735, 0.010876419, -0.10124666, -0.03550831, 0.0056348112, 0.013942076, 0.005951374, 0.020419942, + -0.006857742, -0.020873128, -0.021259667, 0.0137554705, 0.0057880944, -0.029163731, -0.018767154, -0.021392956, + 0.030896494, -0.005494857, -0.0027307675, -0.006801094, -0.014821786, 0.021392956, -0.0018110704, -0.0018843795, + -0.012362596, -0.0072176233, -0.017194338, -0.018713837, -0.024272008, 0.03801415, 0.00015880188, 0.0044951867, + -0.028630573, -0.0014070367, -0.00916365, -0.026537929, -0.009576847, -0.013995391, -0.0077107945, 0.0050016865, + 0.00578143, -0.04467862, 0.008363913, 0.010136662, -0.0006268769, -0.006591163, 0.015341615, -0.027377652, + -0.00093136, 0.029243704, -0.020886457, -0.01041657, -0.02424535, 0.005291591, -0.02980352, -0.009190307, + 0.019460259, -0.0041286405, 0.004801752, 0.0011787785, -0.001257086, -0.011216307, -0.013395589, 0.00088137644, + -0.0051616337, 0.03876057, -0.0033455652, 0.00075850025, -0.006951045, -0.0062112883, 0.018140694, -0.006351242, + -0.008263946, 0.018154023, -0.012189319, 0.0075508473, -0.044358727, -0.0040153447, 0.0093302615, -0.010636497, + 0.032789204, -0.005264933, -0.014235313, -0.018393943, 0.007297597, -0.016114693, 0.015021721, 0.020033404, + 0.0137688, 0.0011046362, 0.010616505, -0.0039453674, 0.012109346, 0.021099718, -0.0072842683, -0.019153694, + -0.003768759, 0.039320387, -0.006747778, -0.0016852784, 0.018154023, 0.0010963057, -0.015035049, -0.021033075, + -0.04345236, 0.017287642, 0.016341286, -0.008610498, 0.00236922, 0.009290274, 0.028950468, -0.014475234, + -0.0035654926, 0.015434918, -0.03372223, 0.004501851, -0.012929076, -0.008483873, -0.0044685286, -0.0102233, + 0.01615468, 0.0022792495, 0.010876419, -0.0059647025, 0.01895376, -0.0069976957, -0.0042952523, 0.017207667, + -0.00036133936, 0.0085905045, 0.008084005, 0.03129636, -0.016994404, -0.014915089, 0.020100048, -0.012009379, + -0.006684466, 0.01306903, 0.00015765642, -0.00530492, 0.0005277429, 0.015421589, 0.015528221, 0.032202728, + -0.003485519, -0.0014286962, 0.033908837, 0.001367883, 0.010509873, 0.025271678, -0.020993087, 0.019846799, + 0.006897729, -0.010216636, -0.00725761, 0.01818068, -0.028443968, -0.011242964, -0.014435247, -0.013688826, + 0.006101324, -0.0022509254, 0.013848773, -0.0019077052, 0.017181009, 0.03422873, 0.005324913, -0.0035188415, + 0.014128681, -0.004898387, 0.005038341, 0.0012320944, -0.005561502, -0.017847456, 0.0008538855, -0.0047884234, + 0.011849431, 0.015421589, -0.013942076, 0.0029790192, -0.013702155, 0.0001199605, -0.024431955, 0.019926772, + 0.022179363, -0.016487904, -0.03964028, 0.0050849924, 0.017487574, 0.022792496, 0.0012504216, 0.004048667, + -0.00997005, 0.0076041627, -0.014328616, -0.020259995, 0.0005598157, -0.010469886, 0.0016852784, 0.01716768, + -0.008990373, -0.001987679, 0.026417969, 0.023792166, 0.0046917885, -0.0071909656, -0.00032051947, -0.023259008, + -0.009170313, 0.02071318, -0.03156294, -0.030869836, -0.006324584, 0.013795458, -0.00047151142, 0.016874444, + 0.00947688, 0.00985009, -0.029883493, 0.024205362, -0.013522214, -0.015075036, -0.030603256, 0.029270362, + 0.010503208, 0.021539574, 0.01743426, -0.023898797, 0.022019416, -0.0068777353, 0.027857494, -0.021259667, + 0.0025758184, 0.006197959, 0.006447877, -0.00025200035, -0.004941706, -0.021246338, -0.005504854, -0.008390571, + -0.0097301295, 0.027244363, -0.04446536, 0.05216949, 0.010243294, -0.016008062, 0.0122493, -0.0199401, + 0.009077012, 0.019753495, 0.006431216, -0.037960835, -0.027377652, 0.016381273, -0.0038620618, 0.022512587, + -0.010996379, -0.0015211658, -0.0102233, 0.007071005, 0.008230623, -0.009490209, -0.010083347, 0.024431955, + 0.002427534, 0.02828402, 0.0035721571, -0.022192692, -0.011882754, 0.010056688, 0.0011904413, -0.01426197, + -0.017500903, -0.00010985966, 0.005591492, -0.0077707744, -0.012049366, 0.011869425, 0.00858384, -0.024698535, + -0.030283362, 0.020140035, 0.011949399, -0.013968734, 0.042732596, -0.011649498, -0.011982721, -0.016967745, + -0.0060913274, -0.007130985, -0.013109017, -0.009710136 }; +} diff --git a/mongodb/fragment-spi/sample/src/test/java/com/example/data/mongodb/Movies.java b/mongodb/fragment-spi/sample/src/test/java/com/example/data/mongodb/Movies.java new file mode 100644 index 000000000..f0ac5f150 --- /dev/null +++ b/mongodb/fragment-spi/sample/src/test/java/com/example/data/mongodb/Movies.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 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 com.example.data.mongodb; + +import lombok.SneakyThrows; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import org.bson.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; + +import org.springframework.core.io.Resource; + +import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.model.InsertOneModel; + +/** + * @author Christoph Strobl + */ +class Movies { + + private static final Logger log = LoggerFactory.getLogger(Movies.class); + + private final MongoClient client; + static final String DATABASE = "test"; + static final String COLLECTION = "movies"; + + public Movies(MongoClient client) { + this.client = client; + } + + boolean alreadyInitialized() { + return client.getDatabase(DATABASE).getCollection(COLLECTION).estimatedDocumentCount() > 0; + } + + @SneakyThrows + void initialize(Resource resource) { + + loadSampleData(resource); + createVectorIndex(); + } + + @SneakyThrows + private void createVectorIndex() { + + log.atLevel(Level.INFO).log("creating vector index"); + client.getDatabase(DATABASE).runCommand(createSearchIndexDefinition()); + Thread.sleep(5000); // this takes time + log.atLevel(Level.INFO).log("index 'plot_vector_index' created"); + } + + private void loadSampleData(Resource resource) throws IOException, InterruptedException { + + log.atLevel(Level.INFO).log("Loading movies from {}", resource); + + InputStream stream = new GZIPInputStream(resource.getInputStream()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readerFor(JsonNode.class).readTree(stream); + + if (node.isArray()) { + + Iterator elements = node.elements(); + List> bulk = new ArrayList<>(node.size()); + + while (elements.hasNext()) { + bulk.add(new InsertOneModel<>(Document.parse(elements.next().toString()))); + } + + client.getDatabase(DATABASE).getCollection(COLLECTION).bulkWrite(bulk); + log.atLevel(Level.INFO).log("Created {} movies in {}.{}", node.size(), DATABASE, COLLECTION); + } + + Thread.sleep(2000); // give writes a little time to complete' + } + + private org.bson.Document createSearchIndexDefinition() { + + List vectorFields = List.of(new org.bson.Document().append("type", "vector") + .append("path", "plot_embedding").append("numDimensions", 1536).append("similarity", "cosine")); + + return new org.bson.Document().append("createSearchIndexes", COLLECTION).append("indexes", + List.of(new org.bson.Document().append("name", "plot_vector_index").append("type", "vectorSearch") + .append("definition", new org.bson.Document("fields", vectorFields)))); + } +} diff --git a/mongodb/fragment-spi/sample/src/test/resources/mflix.embedded_movies.json.gz b/mongodb/fragment-spi/sample/src/test/resources/mflix.embedded_movies.json.gz new file mode 100644 index 000000000..7c52d2b1d Binary files /dev/null and b/mongodb/fragment-spi/sample/src/test/resources/mflix.embedded_movies.json.gz differ diff --git a/mongodb/pom.xml b/mongodb/pom.xml index e74e61223..04d2c282c 100644 --- a/mongodb/pom.xml +++ b/mongodb/pom.xml @@ -17,7 +17,8 @@ 2011 - aggregation + aot-optimization + aggregation example fluent-api @@ -35,7 +36,8 @@ querydsl linking util - + fragment-spi + diff --git a/mongodb/querydsl/src/test/java/example/springdata/mongodb/imperative/CustomerRepositoryTests.java b/mongodb/querydsl/src/test/java/example/springdata/mongodb/imperative/CustomerRepositoryTests.java index aad71680f..04ffd7017 100644 --- a/mongodb/querydsl/src/test/java/example/springdata/mongodb/imperative/CustomerRepositoryTests.java +++ b/mongodb/querydsl/src/test/java/example/springdata/mongodb/imperative/CustomerRepositoryTests.java @@ -19,10 +19,9 @@ import example.springdata.mongodb.Customer; import example.springdata.mongodb.QCustomer; - +import example.springdata.mongodb.util.MongoContainers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; import org.springframework.data.mongodb.core.MongoOperations; @@ -31,7 +30,6 @@ import org.testcontainers.containers.MongoDBContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; /** * @author Christoph Strobl @@ -41,8 +39,7 @@ class CustomerRepositoryTests { @Container // - private static MongoDBContainer mongoDBContainer = new MongoDBContainer( - DockerImageName.parse("mongo:5.0")); + private static MongoDBContainer mongoDBContainer = MongoContainers.getDefaultContainer(); @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { diff --git a/mongodb/reactive/src/main/java/example/springdata/mongodb/people/ReactivePersonRepository.java b/mongodb/reactive/src/main/java/example/springdata/mongodb/people/ReactivePersonRepository.java index adc63a317..1004a90e0 100644 --- a/mongodb/reactive/src/main/java/example/springdata/mongodb/people/ReactivePersonRepository.java +++ b/mongodb/reactive/src/main/java/example/springdata/mongodb/people/ReactivePersonRepository.java @@ -15,6 +15,9 @@ */ package example.springdata.mongodb.people; +import java.util.List; + +import org.springframework.data.domain.Limit; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -37,6 +40,17 @@ public interface ReactivePersonRepository extends ReactiveCrudRepository findByLastname(String lastname); + /** + * Find at most the number of users defined via maxResults with the given lastname. + * This method will be translated into a query by constructing it directly from the method name as there is no other + * query declared. + * + * @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/mongodb/reactive/src/test/java/example/springdata/mongodb/people/ReactiveMongoTemplateIntegrationTest.java b/mongodb/reactive/src/test/java/example/springdata/mongodb/people/ReactiveMongoTemplateIntegrationTest.java index a42748612..bda03aa17 100644 --- a/mongodb/reactive/src/test/java/example/springdata/mongodb/people/ReactiveMongoTemplateIntegrationTest.java +++ b/mongodb/reactive/src/test/java/example/springdata/mongodb/people/ReactiveMongoTemplateIntegrationTest.java @@ -60,7 +60,8 @@ static void setProperties(DynamicPropertyRegistry registry) { @BeforeEach void setUp() { - StepVerifier.create(template.dropCollection(Person.class)).verifyComplete(); + template.dropCollection(Person.class).as(StepVerifier::create) // + .verifyComplete(); var insertAll = template .insertAll(Flux.just(new Person("Walter", "White", 50), // diff --git a/mongodb/reactive/src/test/java/example/springdata/mongodb/people/ReactivePersonRepositoryIntegrationTest.java b/mongodb/reactive/src/test/java/example/springdata/mongodb/people/ReactivePersonRepositoryIntegrationTest.java index 7224a1f1d..194659b47 100644 --- a/mongodb/reactive/src/test/java/example/springdata/mongodb/people/ReactivePersonRepositoryIntegrationTest.java +++ b/mongodb/reactive/src/test/java/example/springdata/mongodb/people/ReactivePersonRepositoryIntegrationTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import example.springdata.mongodb.util.MongoContainers; +import org.springframework.data.domain.Limit; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -157,6 +158,14 @@ void shouldQueryDataWithQueryDerivation() { repository.findByLastname("White").as(StepVerifier::create).expectNextCount(2).verifyComplete(); } + /** + * Limit result size. + */ + @Test + void shouldLimitResultSize() { + repository.findByLastname("White", Limit.of(1)).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + } + /** * Fetch data using a string query. */ diff --git a/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainer.java b/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainer.java new file mode 100644 index 000000000..f74158fc7 --- /dev/null +++ b/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 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.mongodb.util; + +import java.util.List; + +import org.springframework.util.StringUtils; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * @author Christoph Strobl + */ +public class AtlasContainer extends GenericContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongodb/mongodb-atlas-local"); + private static final String DEFAULT_TAG = "latest"; + private static final String MONGODB_DATABASE_NAME_DEFAULT = "test"; + + public AtlasContainer() { + this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); + } + + public AtlasContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public AtlasContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + setExposedPorts(List.of(27017)); + } + + public String getConnectionString() { + return getConnectionString(MONGODB_DATABASE_NAME_DEFAULT); + } + + /** + * Gets a connection string url. + * + * @return a connection url pointing to a mongodb instance + */ + public String getConnectionString(String database) { + return String.format("mongodb://%s:%d/%s?directConnection=true", getHost(), getMappedPort(27017), StringUtils.hasText(database) ? database : MONGODB_DATABASE_NAME_DEFAULT); + } +} diff --git a/mongodb/util/src/main/java/example/springdata/mongodb/util/MongoContainers.java b/mongodb/util/src/main/java/example/springdata/mongodb/util/MongoContainers.java index d45922037..4019c3e0e 100644 --- a/mongodb/util/src/main/java/example/springdata/mongodb/util/MongoContainers.java +++ b/mongodb/util/src/main/java/example/springdata/mongodb/util/MongoContainers.java @@ -22,14 +22,22 @@ * Utility methods to create a {@link MongoDBContainer}. * * @author Mark Paluch + * @author Christoph Strobl */ public class MongoContainers { - private static final String IMAGE_NAME = "mongo:5.0"; - private static final String IMAGE_NAME_PROPERTY = "mongo.default.image.name"; + private static final String IMAGE_NAME = "mongo:8.0"; + private static final String IMAGE_NAME_PROPERTY = "mongo.default.image.name"; - public static MongoDBContainer getDefaultContainer() { - return new MongoDBContainer(DockerImageName.parse(System.getProperty(IMAGE_NAME_PROPERTY, IMAGE_NAME))) - .withReuse(true); - } + private static final String ATLAS_IMAGE_NAME = "mongodb/mongodb-atlas-local:latest"; + private static final String ATLAS_IMAGE_NAME_PROPERTY = "mongo.atlas.image.name"; + + public static MongoDBContainer getDefaultContainer() { + return new MongoDBContainer(DockerImageName.parse(System.getProperty(IMAGE_NAME_PROPERTY, IMAGE_NAME))) + .withReuse(true); + } + + public static AtlasContainer getAtlasContainer() { + return new AtlasContainer(System.getProperty(ATLAS_IMAGE_NAME_PROPERTY, ATLAS_IMAGE_NAME)).withReuse(true); + } } diff --git a/multi-store/pom.xml b/multi-store/pom.xml index 5c53decb3..a91dd33b0 100644 --- a/multi-store/pom.xml +++ b/multi-store/pom.xml @@ -30,6 +30,20 @@ test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + mongodb + test + + \ No newline at end of file diff --git a/multi-store/src/test/java/example/springdata/multistore/ApplicationConfigurationTest.java b/multi-store/src/test/java/example/springdata/multistore/ApplicationConfigurationTest.java index 7eac97ffe..e05a1afd3 100644 --- a/multi-store/src/test/java/example/springdata/multistore/ApplicationConfigurationTest.java +++ b/multi-store/src/test/java/example/springdata/multistore/ApplicationConfigurationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2024 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,34 +15,46 @@ */ package example.springdata.multistore; +import static org.assertj.core.api.Assertions.*; + import example.springdata.multistore.customer.Customer; import example.springdata.multistore.shop.Order; -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.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.repository.support.Repositories; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.utility.DockerImageName; /** * Integration test to check repository interfaces are assigned to the correct store modules. * * @author Oliver Gierke */ -@RunWith(SpringRunner.class) @SpringBootTest -public class ApplicationConfigurationTest { +class ApplicationConfigurationTest { @Autowired ApplicationContext context; + @TestConfiguration + static class Infrastructure { + + @Bean + @ServiceConnection + MongoDBContainer mongoDBContainer() { + return new MongoDBContainer(DockerImageName.parse("mongodb/mongodb-community-server")); + } + } + @Test - public void repositoriesAreAssignedToAppropriateStores() { + void repositoriesAreAssignedToAppropriateStores() { var repositories = new Repositories(context); diff --git a/multi-store/src/test/resources/logback.xml b/multi-store/src/test/resources/logback.xml index 0478c08d7..5a76a8fe3 100644 --- a/multi-store/src/test/resources/logback.xml +++ b/multi-store/src/test/resources/logback.xml @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/pom.xml b/pom.xml index c69fc17ec..9924b6c58 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.0-RC1 + 3.5.0-RC1 @@ -37,22 +37,23 @@ 1.1.3 + 21 + 21 UTF-8 - 2023.1.0-SNAPSHOT spring-data-next-releasetrain - 2024.0.0-SNAPSHOT + 2025.1.0-SNAPSHOT spring-data-next - 2023.1.0-SNAPSHOT + 2025.0.0-SNAPSHOT @@ -160,17 +161,23 @@ central Maven Central https://repo1.maven.org/maven2/ + + false + spring-milestone https://repo.spring.io/milestone + + false + spring-snapshot https://repo.spring.io/snapshot - - true - + + false + @@ -179,11 +186,55 @@ central Maven Central https://repo1.maven.org/maven2/ + + false + spring-milestone https://repo.spring.io/milestone/ + + false + + + + + + com.gradle + develocity-maven-extension + + + + + maven-surefire-plugin + + these tests showcase Spring Data features and should always rerun + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + diff --git a/r2dbc/example/src/main/java/example/springdata/r2dbc/basics/CustomerRepository.java b/r2dbc/example/src/main/java/example/springdata/r2dbc/basics/CustomerRepository.java index bbf002b6b..5358fb499 100644 --- a/r2dbc/example/src/main/java/example/springdata/r2dbc/basics/CustomerRepository.java +++ b/r2dbc/example/src/main/java/example/springdata/r2dbc/basics/CustomerRepository.java @@ -15,6 +15,7 @@ */ package example.springdata.r2dbc.basics; +import org.springframework.data.domain.Limit; import reactor.core.publisher.Flux; import org.springframework.data.r2dbc.repository.Query; @@ -28,4 +29,6 @@ interface CustomerRepository extends ReactiveCrudRepository { @Query("select id, firstname, lastname from customer c where c.lastname = :lastname") Flux findByLastname(String lastname); + + Flux findByLastname(String lastname, Limit limit); } diff --git a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/TransactionalServiceIntegrationTests.java b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/TransactionalServiceIntegrationTests.java index f3fe9869a..6b788a337 100644 --- a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/TransactionalServiceIntegrationTests.java +++ b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/TransactionalServiceIntegrationTests.java @@ -15,6 +15,7 @@ */ package example.springdata.r2dbc.basics; +import org.springframework.data.domain.Limit; import reactor.core.publisher.Hooks; import reactor.test.StepVerifier; @@ -72,6 +73,25 @@ void exceptionTriggersRollback() { .verifyComplete(); } + @Test + void limitResultSize() { + + service.save(new Customer(null, "Carter", "Matthews")) // + .as(StepVerifier::create) // + .expectNextMatches(Customer::hasId) // + .verifyComplete(); + + service.save(new Customer(null, "Evad", "Matthews")) // + .as(StepVerifier::create) // + .expectNextMatches(Customer::hasId) // + .verifyComplete(); + + repository.findByLastname("Matthews", Limit.of(1)) // + .as(StepVerifier::create) // + .expectNextCount(1) + .verifyComplete(); + } + @Test // #500 void insertsDataTransactionally() { diff --git a/redis/example/pom.xml b/redis/example/pom.xml index 2dc044e7a..6437e84e2 100644 --- a/redis/example/pom.xml +++ b/redis/example/pom.xml @@ -15,9 +15,13 @@ - ${project.groupId} - spring-data-redis-example-utils - ${project.version} + org.springframework.boot + spring-boot-testcontainers + test + + + org.springframework + spring-test test diff --git a/redis/example/src/test/java/example/springdata/redis/RedisTestConfiguration.java b/redis/example/src/test/java/example/springdata/redis/RedisTestConfiguration.java index 94cd96c31..c7eff4316 100644 --- a/redis/example/src/test/java/example/springdata/redis/RedisTestConfiguration.java +++ b/redis/example/src/test/java/example/springdata/redis/RedisTestConfiguration.java @@ -15,11 +15,11 @@ */ package example.springdata.redis; -import jakarta.annotation.PreDestroy; - -import org.springframework.beans.factory.annotation.Autowired; +import com.redis.testcontainers.RedisContainer; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.utility.DockerImageName; /** * @author Christoph Strobl @@ -27,12 +27,9 @@ @SpringBootApplication public class RedisTestConfiguration { - @Autowired RedisConnectionFactory factory; - - /** - * Clear database before shut down. - */ - public @PreDestroy void flushTestDb() { - factory.getConnection().flushDb(); + @Bean + @ServiceConnection(name = "redis") + RedisContainer redisContainer() { + return new RedisContainer(DockerImageName.parse("redis:7")); } } diff --git a/redis/example/src/test/java/example/springdata/redis/commands/GeoOperationsTests.java b/redis/example/src/test/java/example/springdata/redis/commands/GeoOperationsTests.java index d70f46100..613ff6f79 100644 --- a/redis/example/src/test/java/example/springdata/redis/commands/GeoOperationsTests.java +++ b/redis/example/src/test/java/example/springdata/redis/commands/GeoOperationsTests.java @@ -15,13 +15,10 @@ */ package example.springdata.redis.commands; -import static org.assertj.core.api.Assertions.*; - -import example.springdata.redis.test.condition.EnabledOnCommand; +import static org.assertj.core.api.Assertions.assertThat; 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.geo.Circle; @@ -37,7 +34,6 @@ * @author Mark Paluch */ @SpringBootTest -@EnabledOnCommand("GEOADD") class GeoOperationsTests { @Autowired RedisOperations operations; diff --git a/redis/example/src/test/java/example/springdata/redis/commands/KeyOperationsTests.java b/redis/example/src/test/java/example/springdata/redis/commands/KeyOperationsTests.java index 16ee33b3d..6fc637c17 100644 --- a/redis/example/src/test/java/example/springdata/redis/commands/KeyOperationsTests.java +++ b/redis/example/src/test/java/example/springdata/redis/commands/KeyOperationsTests.java @@ -15,13 +15,10 @@ */ package example.springdata.redis.commands; -import example.springdata.redis.test.condition.EnabledOnRedisAvailable; - import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest; import org.springframework.data.redis.connection.RedisConnection; @@ -36,7 +33,6 @@ * @author Christoph Strobl */ @DataRedisTest -@EnabledOnRedisAvailable class KeyOperationsTests { private static final String PREFIX = KeyOperationsTests.class.getSimpleName(); diff --git a/redis/pom.xml b/redis/pom.xml index 8d99e58e7..4a8501731 100644 --- a/redis/pom.xml +++ b/redis/pom.xml @@ -33,19 +33,19 @@ spring-boot-starter-data-redis - - - - - + + org.testcontainers + junit-jupiter + test + - - com.github.kstyrc - embedded-redis - 0.6 - - + + com.redis + testcontainers-redis + 2.2.2 + test + - + diff --git a/redis/pubsub/pom.xml b/redis/pubsub/pom.xml index 9ba42bdf3..761b002ed 100644 --- a/redis/pubsub/pom.xml +++ b/redis/pubsub/pom.xml @@ -16,9 +16,13 @@ - ${project.groupId} - spring-data-redis-example-utils - ${project.version} + org.springframework.boot + spring-boot-testcontainers + test + + + org.springframework + spring-test test diff --git a/redis/pubsub/src/test/java/example/springdata/redis/PubSubVirtualThreadsTests.java b/redis/pubsub/src/test/java/example/springdata/redis/PubSubVirtualThreadsTests.java index 8c4b7d603..5782868c4 100644 --- a/redis/pubsub/src/test/java/example/springdata/redis/PubSubVirtualThreadsTests.java +++ b/redis/pubsub/src/test/java/example/springdata/redis/PubSubVirtualThreadsTests.java @@ -22,9 +22,8 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.task.AsyncTaskExecutor; @@ -39,7 +38,7 @@ * @author Mark Paluch */ @SpringBootTest(properties = "spring.threads.virtual.enabled=true") -@EnabledOnJre(JRE.JAVA_21) +@EnabledForJreRange(min = JRE.JAVA_21) public class PubSubVirtualThreadsTests { @Autowired RedisConnectionFactory connectionFactory; diff --git a/redis/pubsub/src/test/java/example/springdata/redis/RedisTestConfiguration.java b/redis/pubsub/src/test/java/example/springdata/redis/RedisTestConfiguration.java index cec2d756c..1c38139ce 100644 --- a/redis/pubsub/src/test/java/example/springdata/redis/RedisTestConfiguration.java +++ b/redis/pubsub/src/test/java/example/springdata/redis/RedisTestConfiguration.java @@ -15,10 +15,22 @@ */ package example.springdata.redis; +import com.redis.testcontainers.RedisContainer; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.utility.DockerImageName; /** * @author Mark Paluch + * @author Christoph Strobl */ @SpringBootApplication -public class RedisTestConfiguration {} +public class RedisTestConfiguration { + + @Bean + @ServiceConnection(name = "redis") + RedisContainer redisContainer() { + return new RedisContainer(DockerImageName.parse("redis:7")); + } +} diff --git a/redis/reactive/pom.xml b/redis/reactive/pom.xml index b730243d4..241bec0f3 100644 --- a/redis/reactive/pom.xml +++ b/redis/reactive/pom.xml @@ -41,9 +41,13 @@ - ${project.groupId} - spring-data-redis-example-utils - ${project.version} + org.springframework.boot + spring-boot-testcontainers + test + + + org.springframework + spring-test test diff --git a/redis/reactive/src/test/java/example/springdata/redis/RedisTestConfiguration.java b/redis/reactive/src/test/java/example/springdata/redis/RedisTestConfiguration.java index 2191dcb8d..b790f72c4 100644 --- a/redis/reactive/src/test/java/example/springdata/redis/RedisTestConfiguration.java +++ b/redis/reactive/src/test/java/example/springdata/redis/RedisTestConfiguration.java @@ -15,28 +15,30 @@ */ package example.springdata.redis; -import jakarta.annotation.PreDestroy; - +import com.redis.testcontainers.RedisContainer; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializationContext.RedisSerializationContextBuilder; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.testcontainers.utility.DockerImageName; /** * @author Mark Paluch + * @author Christoph Strobl */ @SpringBootApplication public class RedisTestConfiguration { @Bean - public LettuceConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(); + @ServiceConnection(name = "redis") + RedisContainer redisContainer() { + return new RedisContainer(DockerImageName.parse("redis:7")); } /** @@ -72,10 +74,4 @@ public ReactiveRedisTemplate reactiveJsonObjectRedisTemplate( return new ReactiveRedisTemplate<>(connectionFactory, serializationContext); } - /** - * Clear database before shut down. - */ - public @PreDestroy void flushTestDb() { - redisConnectionFactory().getConnection().flushDb(); - } } diff --git a/redis/reactive/src/test/java/example/springdata/redis/commands/KeyCommandsTests.java b/redis/reactive/src/test/java/example/springdata/redis/commands/KeyCommandsTests.java index 1ff35d61f..1dc5598e5 100644 --- a/redis/reactive/src/test/java/example/springdata/redis/commands/KeyCommandsTests.java +++ b/redis/reactive/src/test/java/example/springdata/redis/commands/KeyCommandsTests.java @@ -15,19 +15,14 @@ */ package example.springdata.redis.commands; -import example.springdata.redis.RedisTestConfiguration; -import example.springdata.redis.test.condition.EnabledOnRedisAvailable; -import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; - import java.nio.ByteBuffer; import java.time.Duration; import java.util.Collections; import java.util.UUID; +import example.springdata.redis.RedisTestConfiguration; 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.redis.connection.ReactiveRedisConnection; @@ -36,6 +31,8 @@ import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.util.ByteUtils; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; /** * Show usage of reactive operations on Redis keys using low level API provided by @@ -44,7 +41,6 @@ * @author Mark Paluch */ @SpringBootTest(classes = RedisTestConfiguration.class) -@EnabledOnRedisAvailable class KeyCommandsTests { private static final String PREFIX = KeyCommandsTests.class.getSimpleName(); @@ -77,7 +73,7 @@ void iterateOverKeysMatchingPrefixUsingKeysCommand() { .count() // .doOnSuccess(count -> System.out.println(String.format("Total No. found: %s", count))); - StepVerifier.create(keyCount).expectNext(50L).verifyComplete(); + keyCount.as(StepVerifier::create).expectNext(50L).verifyComplete(); } /** @@ -98,7 +94,7 @@ void storeToListAndPop() { .flatMap(result -> llen) // .doOnNext(count -> System.out.println(String.format("Total items in list left: %s", count)));// - StepVerifier.create(popAndLlen).expectNext(0L).verifyComplete(); + popAndLlen.as(StepVerifier::create).expectNext(0L).verifyComplete(); } private void generateRandomKeys(int nrKeys) { @@ -109,7 +105,9 @@ private void generateRandomKeys(int nrKeys) { .map(key -> SetCommand.set(key) // .value(ByteBuffer.wrap(UUID.randomUUID().toString().getBytes()))); - StepVerifier.create(connection.stringCommands().set(generator)).expectNextCount(nrKeys).verifyComplete(); + connection.stringCommands().set(generator).as(StepVerifier::create) // + .expectNextCount(nrKeys) // + .verifyComplete(); } diff --git a/redis/reactive/src/test/java/example/springdata/redis/operations/JacksonJsonTests.java b/redis/reactive/src/test/java/example/springdata/redis/operations/JacksonJsonTests.java index c361dc955..716d775d3 100644 --- a/redis/reactive/src/test/java/example/springdata/redis/operations/JacksonJsonTests.java +++ b/redis/reactive/src/test/java/example/springdata/redis/operations/JacksonJsonTests.java @@ -18,7 +18,6 @@ import example.springdata.redis.EmailAddress; import example.springdata.redis.Person; import example.springdata.redis.RedisTestConfiguration; -import example.springdata.redis.test.condition.EnabledOnRedisAvailable; import lombok.extern.slf4j.Slf4j; import reactor.test.StepVerifier; @@ -39,7 +38,6 @@ */ @Slf4j @SpringBootTest(classes = RedisTestConfiguration.class) -@EnabledOnRedisAvailable class JacksonJsonTests { @Autowired ReactiveRedisOperations typedOperations; @@ -56,7 +54,7 @@ class JacksonJsonTests { @Test void shouldWriteAndReadPerson() { - StepVerifier.create(typedOperations.opsForValue().set("homer", new Person("Homer", "Simpson"))) // + typedOperations.opsForValue().set("homer", new Person("Homer", "Simpson")).as(StepVerifier::create) // .expectNext(true) // .verifyComplete(); diff --git a/redis/reactive/src/test/java/example/springdata/redis/operations/ListOperationsTests.java b/redis/reactive/src/test/java/example/springdata/redis/operations/ListOperationsTests.java index 553d7534a..46457dc11 100644 --- a/redis/reactive/src/test/java/example/springdata/redis/operations/ListOperationsTests.java +++ b/redis/reactive/src/test/java/example/springdata/redis/operations/ListOperationsTests.java @@ -16,7 +16,6 @@ package example.springdata.redis.operations; import example.springdata.redis.RedisTestConfiguration; -import example.springdata.redis.test.condition.EnabledOnRedisAvailable; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -38,14 +37,16 @@ */ @Slf4j @SpringBootTest(classes = RedisTestConfiguration.class) -@EnabledOnRedisAvailable class ListOperationsTests { @Autowired ReactiveRedisOperations operations; @BeforeEach void before() { - StepVerifier.create(operations.execute(it -> it.serverCommands().flushDb())).expectNext("OK").verifyComplete(); + + operations.execute(it -> it.serverCommands().flushDb()).as(StepVerifier::create) // + .expectNext("OK") // + .verifyComplete(); } /** @@ -63,7 +64,7 @@ void shouldPollAndPopulateQueue() { .log("example.springdata.redis", Level.INFO); log.info("Blocking pop...waiting for message"); - StepVerifier.create(blpop) // + blpop.as(StepVerifier::create) // .then(() -> { Mono.delay(Duration.ofSeconds(10)).doOnSuccess(it -> { diff --git a/redis/reactive/src/test/java/example/springdata/redis/operations/ValueOperationsTests.java b/redis/reactive/src/test/java/example/springdata/redis/operations/ValueOperationsTests.java index d036285e2..f27cdb823 100644 --- a/redis/reactive/src/test/java/example/springdata/redis/operations/ValueOperationsTests.java +++ b/redis/reactive/src/test/java/example/springdata/redis/operations/ValueOperationsTests.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.*; import example.springdata.redis.RedisTestConfiguration; -import example.springdata.redis.test.condition.EnabledOnRedisAvailable; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -39,14 +38,16 @@ */ @Slf4j @SpringBootTest(classes = RedisTestConfiguration.class) -@EnabledOnRedisAvailable class ValueOperationsTests { @Autowired ReactiveRedisOperations operations; @BeforeEach void before() { - StepVerifier.create(operations.execute(it -> it.serverCommands().flushDb())).expectNext("OK").verifyComplete(); + + operations.execute(it -> it.serverCommands().flushDb()).as(StepVerifier::create) // + .expectNext("OK") // + .verifyComplete(); } /** @@ -67,14 +68,14 @@ void shouldCacheValue() { log.info("Initial access (takes a while...)"); - StepVerifier.create(cachedMono).expectSubscription() // + cachedMono.as(StepVerifier::create).expectSubscription() // .expectNoEvent(Duration.ofSeconds(9)) // .expectNext("Hello, World!") // .verifyComplete(); log.info("Subsequent access (use cached value)"); - var duration = StepVerifier.create(cachedMono) // + var duration = cachedMono.as(StepVerifier::create) // .expectNext("Hello, World!") // .verifyComplete(); diff --git a/redis/repositories/pom.xml b/redis/repositories/pom.xml index fd8a5e133..e8fdc3e12 100644 --- a/redis/repositories/pom.xml +++ b/redis/repositories/pom.xml @@ -15,15 +15,13 @@ - ${project.groupId} - spring-data-redis-example-utils - ${project.version} + org.springframework.boot + spring-boot-testcontainers test - - com.github.kstyrc - embedded-redis + org.springframework + spring-test test diff --git a/redis/repositories/src/test/java/example/springdata/redis/repositories/PersonRepositoryTests.java b/redis/repositories/src/test/java/example/springdata/redis/repositories/PersonRepositoryTests.java index d169460e7..e325046fa 100644 --- a/redis/repositories/src/test/java/example/springdata/redis/repositories/PersonRepositoryTests.java +++ b/redis/repositories/src/test/java/example/springdata/redis/repositories/PersonRepositoryTests.java @@ -17,7 +17,7 @@ import static org.assertj.core.api.Assertions.*; -import example.springdata.redis.test.condition.EnabledOnRedisAvailable; +import com.redis.testcontainers.RedisContainer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.data.domain.Example; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -40,16 +41,23 @@ import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.index.GeoIndexed; import org.springframework.data.redis.core.index.Indexed; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; /** * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch */ +@Testcontainers @DataRedisTest -@EnabledOnRedisAvailable class PersonRepositoryTests { + @Container + @ServiceConnection + static RedisContainer redis = new RedisContainer(DockerImageName.parse("redis:7")); + /** {@link Charset} for String conversion **/ private static final Charset CHARSET = StandardCharsets.UTF_8; diff --git a/redis/streams/pom.xml b/redis/streams/pom.xml index 73d473363..8cdcce5f2 100644 --- a/redis/streams/pom.xml +++ b/redis/streams/pom.xml @@ -22,9 +22,13 @@ - ${project.groupId} - spring-data-redis-example-utils - ${project.version} + org.springframework.boot + spring-boot-testcontainers + test + + + org.springframework + spring-test test diff --git a/redis/streams/src/test/java/example/springdata/redis/reactive/ReactiveStreamApiTests.java b/redis/streams/src/test/java/example/springdata/redis/reactive/ReactiveStreamApiTests.java index 2ed3a06c3..ed061f717 100644 --- a/redis/streams/src/test/java/example/springdata/redis/reactive/ReactiveStreamApiTests.java +++ b/redis/streams/src/test/java/example/springdata/redis/reactive/ReactiveStreamApiTests.java @@ -18,8 +18,12 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.redis.connection.stream.StreamOffset.*; +import com.redis.testcontainers.RedisContainer; import example.springdata.redis.SensorData; -import example.springdata.redis.test.condition.EnabledOnCommand; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; import reactor.test.StepVerifier; import java.time.Duration; @@ -43,11 +47,15 @@ /** * @author Christoph Strobl */ +@Testcontainers @DataRedisTest -@EnabledOnCommand("XADD") @ImportAutoConfiguration(RedisReactiveAutoConfiguration.class) class ReactiveStreamApiTests { + @Container + @ServiceConnection + static RedisContainer redis = new RedisContainer(DockerImageName.parse("redis:7")); + @Autowired ReactiveStringRedisTemplate template; @Autowired StreamReceiver> streamReceiver; diff --git a/redis/streams/src/test/java/example/springdata/redis/sync/SyncStreamApiTests.java b/redis/streams/src/test/java/example/springdata/redis/sync/SyncStreamApiTests.java index 0cf27940f..b4d7d08a8 100644 --- a/redis/streams/src/test/java/example/springdata/redis/sync/SyncStreamApiTests.java +++ b/redis/streams/src/test/java/example/springdata/redis/sync/SyncStreamApiTests.java @@ -18,8 +18,8 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.redis.connection.stream.StreamOffset.*; +import com.redis.testcontainers.RedisContainer; import example.springdata.redis.SensorData; -import example.springdata.redis.test.condition.EnabledOnCommand; import java.util.concurrent.TimeUnit; @@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.stream.MapRecord; import org.springframework.data.redis.connection.stream.ReadOffset; @@ -36,15 +37,22 @@ import org.springframework.data.redis.core.StreamOperations; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.stream.StreamMessageListenerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; /** * @author Christoph Strobl * @author Mark Paluch */ +@Testcontainers @DataRedisTest -@EnabledOnCommand("XADD") class SyncStreamApiTests { + @Container + @ServiceConnection + static RedisContainer redis = new RedisContainer(DockerImageName.parse("redis:7")); + @Autowired StringRedisTemplate template; @Autowired StreamMessageListenerContainer> messageListenerContainer; diff --git a/redis/util/pom.xml b/redis/util/pom.xml index 09db062aa..8d345c0da 100644 --- a/redis/util/pom.xml +++ b/redis/util/pom.xml @@ -25,12 +25,6 @@ lettuce-core - - com.github.kstyrc - embedded-redis - true - - diff --git a/rest/multi-store/pom.xml b/rest/multi-store/pom.xml index 85f1dbc69..8163f5671 100644 --- a/rest/multi-store/pom.xml +++ b/rest/multi-store/pom.xml @@ -33,10 +33,18 @@ spring-boot-starter-data-mongodb + + + + org.springframework.boot + spring-boot-testcontainers + test + + - de.flapdoodle.embed - de.flapdoodle.embed.mongo - runtime + org.testcontainers + mongodb + test diff --git a/rest/multi-store/src/test/java/example/springdata/multistore/ApplicationIntegrationTests.java b/rest/multi-store/src/test/java/example/springdata/multistore/ApplicationIntegrationTests.java index 2c3e442e6..3c7363a67 100644 --- a/rest/multi-store/src/test/java/example/springdata/multistore/ApplicationIntegrationTests.java +++ b/rest/multi-store/src/test/java/example/springdata/multistore/ApplicationIntegrationTests.java @@ -24,6 +24,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.utility.DockerImageName; /** * Integration test to show the usage of repositories backed by different stores. @@ -37,6 +42,16 @@ public class ApplicationIntegrationTests { @Autowired PersonRepository personRepository; @Autowired TreasureRepository treasureRepository; + @TestConfiguration + static class Infrastructure { + + @Bean + @ServiceConnection + MongoDBContainer mongoDBContainer() { + return new MongoDBContainer(DockerImageName.parse("mongodb/mongodb-community-server")); + } + } + @Test public void useMultipleRepositories() { diff --git a/rest/multi-store/src/test/resources/application.properties b/rest/multi-store/src/test/resources/application.properties deleted file mode 100644 index 5239185aa..000000000 --- a/rest/multi-store/src/test/resources/application.properties +++ /dev/null @@ -1,3 +0,0 @@ -# Random port for embedded MongoDB -spring.data.mongodb.port=0 -spring.mongodb.embedded.version=3.6.0 \ No newline at end of file diff --git a/rest/pom.xml b/rest/pom.xml index d361a147d..d8aeaedf2 100644 --- a/rest/pom.xml +++ b/rest/pom.xml @@ -15,8 +15,8 @@ Sample projects for Spring Data REST - - + starbucks + multi-store projections security headers diff --git a/rest/starbucks/pom.xml b/rest/starbucks/pom.xml index 122cbb8c6..aa0793a15 100644 --- a/rest/starbucks/pom.xml +++ b/rest/starbucks/pom.xml @@ -22,6 +22,7 @@ de.flapdoodle.embed de.flapdoodle.embed.mongo + 4.16.2 runtime @@ -69,14 +70,14 @@ org.webjars jquery - 2.1.3 + 3.7.1 runtime org.webjars bootstrap - 3.3.4 + 5.3.3 runtime @@ -93,6 +94,20 @@ runtime + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + mongodb + test + + diff --git a/rest/starbucks/src/main/java/example/springdata/rest/stores/StoreInitializer.java b/rest/starbucks/src/main/java/example/springdata/rest/stores/StoreInitializer.java index 2f27b74ee..85f34688a 100644 --- a/rest/starbucks/src/main/java/example/springdata/rest/stores/StoreInitializer.java +++ b/rest/starbucks/src/main/java/example/springdata/rest/stores/StoreInitializer.java @@ -28,7 +28,6 @@ import org.springframework.batch.item.file.mapping.DefaultLineMapper; import org.springframework.batch.item.file.separator.DefaultRecordSeparatorPolicy; import org.springframework.batch.item.file.transform.DelimitedLineTokenizer; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.MongoOperations; @@ -45,7 +44,6 @@ @Component public class StoreInitializer { - @Autowired public StoreInitializer(StoreRepository repository, MongoOperations operations) throws Exception { if (repository.count() != 0) { @@ -64,8 +62,8 @@ public StoreInitializer(StoreRepository repository, MongoOperations operations) } /** - * Reads a file {@code starbucks.csv} from the class path and parses it into {@link Store} instances about to - * be persisted. + * Reads a file {@code starbucks.csv} from the class path and parses it into {@link Store} instances about to be + * persisted. * * @return * @throws Exception diff --git a/rest/starbucks/src/main/java/example/springdata/rest/stores/StoreRepository.java b/rest/starbucks/src/main/java/example/springdata/rest/stores/StoreRepository.java index 366bc363d..1a67b3f92 100644 --- a/rest/starbucks/src/main/java/example/springdata/rest/stores/StoreRepository.java +++ b/rest/starbucks/src/main/java/example/springdata/rest/stores/StoreRepository.java @@ -24,6 +24,7 @@ import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.binding.QuerydslBindings; import org.springframework.data.querydsl.binding.SingleValueBinding; +import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.rest.core.annotation.RestResource; @@ -36,7 +37,8 @@ * * @author Oliver Gierke */ -public interface StoreRepository extends PagingAndSortingRepository, QuerydslPredicateExecutor { +public interface StoreRepository + extends CrudRepository, PagingAndSortingRepository, QuerydslPredicateExecutor { @RestResource(rel = "by-location") Page findByAddressLocationNear(Point location, Distance distance, Pageable pageable); diff --git a/rest/starbucks/src/main/java/example/springdata/rest/stores/web/StoresController.java b/rest/starbucks/src/main/java/example/springdata/rest/stores/web/StoresController.java index febca76a5..a1dee0fbb 100644 --- a/rest/starbucks/src/main/java/example/springdata/rest/stores/web/StoresController.java +++ b/rest/starbucks/src/main/java/example/springdata/rest/stores/web/StoresController.java @@ -34,8 +34,7 @@ import org.springframework.hateoas.LinkRelation; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; /** @@ -70,7 +69,7 @@ class StoresController { * @param pageable the pagination information * @return */ - @RequestMapping(value = "/", method = RequestMethod.GET) + @GetMapping("/") String index(Model model, @RequestParam Optional location, @RequestParam Optional distance, Pageable pageable) { diff --git a/rest/starbucks/src/main/resources/application.properties b/rest/starbucks/src/main/resources/application.properties index f644711ab..1e5a96cc0 100644 --- a/rest/starbucks/src/main/resources/application.properties +++ b/rest/starbucks/src/main/resources/application.properties @@ -1,5 +1,2 @@ -# Random port for embedded MongoDB -spring.data.mongodb.port=0 -spring.mongodb.embedded.version=3.6.0 # Spring Data REST spring.data.rest.base-path=/api diff --git a/rest/starbucks/src/test/java/example/springdata/rest/stores/StarbucksClient.java b/rest/starbucks/src/test/java/example/springdata/rest/stores/StarbucksClient.java index 3e6d5ff10..a314c91a4 100644 --- a/rest/starbucks/src/test/java/example/springdata/rest/stores/StarbucksClient.java +++ b/rest/starbucks/src/test/java/example/springdata/rest/stores/StarbucksClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2024 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. @@ -26,20 +26,17 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.client.Traverson; import org.springframework.hateoas.server.core.TypeReferences.CollectionModelType; import org.springframework.hateoas.server.core.TypeReferences.EntityModelType; import org.springframework.hateoas.server.core.TypeReferences.PagedModelType; -import org.springframework.http.RequestEntity; -import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.RestClient; /** * A test case to discover the search resource and execute a predefined search with it. @@ -47,22 +44,12 @@ * @author Oliver Gierke * @author Divya Srivastava */ -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @Slf4j +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class StarbucksClient { - @SpringBootApplication - static class Config { - - @Bean - public RestTemplate restTemplate() { - return new RestTemplate(); - } - } - @LocalServerPort int port; - - @Autowired RestOperations restOperations; + @Autowired TestRestTemplate template; private static final String SERVICE_URI = "http://localhost:%s/api"; @@ -102,17 +89,23 @@ void accessServiceUsingRestTemplate() { // Access root resource - var uri = URI.create(String.format(SERVICE_URI, port)); - var request = RequestEntity.get(uri).accept(HAL_JSON).build(); - var rootLinks = restOperations.exchange(request, new EntityModelType<>() {}).getBody(); - var links = rootLinks.getLinks(); + var client = RestClient.create(template.getRestTemplate()); + + var links = client.get() + .uri(URI.create(String.format(SERVICE_URI, port))) + .accept(HAL_JSON) + .retrieve() + .body(new EntityModelType<>() {}) + .getLinks(); // Follow stores link var storesLink = links.getRequiredLink("stores").expand(); - request = RequestEntity.get(URI.create(storesLink.getHref())).accept(HAL_JSON).build(); - var stores = restOperations.exchange(request, new CollectionModelType() {}).getBody(); + var stores = client.get().uri(storesLink.toUri()) + .accept(HAL_JSON) + .retrieve() + .body(new CollectionModelType() {}); stores.getContent().forEach(store -> log.info("{} - {}", store.name, store.address)); } @@ -126,8 +119,7 @@ public String toString() { return String.format("%s, %s %s - lat: %s, long: %s", street, zip, city, location.y, location.x); } - record Location(double x, double y) { - } + record Location(double x, double y) {} } } } diff --git a/rest/starbucks/src/test/java/example/springdata/rest/stores/StoreRepositoryIntegrationTests.java b/rest/starbucks/src/test/java/example/springdata/rest/stores/StoreRepositoryIntegrationTests.java index f5de082ee..e3ab2fc4d 100644 --- a/rest/starbucks/src/test/java/example/springdata/rest/stores/StoreRepositoryIntegrationTests.java +++ b/rest/starbucks/src/test/java/example/springdata/rest/stores/StoreRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2024 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,7 +24,6 @@ import org.junit.jupiter.api.AfterEach; 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.PageRequest; @@ -38,8 +37,8 @@ * @author Oliver Gierke * @author Mark Paluch */ -@SpringBootTest(classes = Application.class) -public class StoreRepositoryIntegrationTests { +@SpringBootTest(classes = { Application.class, TestApplication.class }) +class StoreRepositoryIntegrationTests { @Autowired StoreRepository repository; @@ -50,7 +49,7 @@ public void clearDb() { } @Test - public void findsStoresByLocation() { + void findsStoresByLocation() { var location = new Point(-73.995146, 40.740337); var store = new Store(UUID.randomUUID(), "Foo", new Address("street", "city", "zip", location)); diff --git a/rest/starbucks/src/test/java/example/springdata/rest/stores/TestApplication.java b/rest/starbucks/src/test/java/example/springdata/rest/stores/TestApplication.java new file mode 100644 index 000000000..cb1bfcf18 --- /dev/null +++ b/rest/starbucks/src/test/java/example/springdata/rest/stores/TestApplication.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 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.rest.stores; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * @author Oliver Drotbohm + */ +@Configuration +public class TestApplication { + + @Bean + @ServiceConnection + MongoDBContainer mongoDBContainer() { + return new MongoDBContainer(DockerImageName.parse("mongodb/mongodb-community-server")); + } + + public static void main(String[] args) { + + SpringApplication.from(Application::main) + .with(TestApplication.class) + .run(args); + } +} diff --git a/web/pom.xml b/web/pom.xml index f9899badc..30c94ab95 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -16,7 +16,7 @@ 2015 - example + example querydsl projection diff --git a/web/querydsl/README.md b/web/querydsl/README.md index b8aab84c5..91ae105c5 100644 --- a/web/querydsl/README.md +++ b/web/querydsl/README.md @@ -4,9 +4,8 @@ This example shows some of the Spring Data Querydsl integration features with Sp ## Quickstart -1. Install MongoDB (http://www.mongodb.org/downloads, unzip, run `mkdir data`, run `bin/mongod --dbpath=data`) -2. Build and run the app (`mvn spring-boot:run`) -4. Access app directly via its UI (`http://localhost:8080/`). +1. Build and run the app (`mvn spring-boot:test-run`) +2. Access app directly via its UI (`http://localhost:8080/`). ## Interesting bits diff --git a/web/querydsl/pom.xml b/web/querydsl/pom.xml index 36ab136b2..2f66cf7e2 100644 --- a/web/querydsl/pom.xml +++ b/web/querydsl/pom.xml @@ -58,20 +58,20 @@ io.github.jpenren thymeleaf-spring-data-dialect - 3.3.0 + 3.6.0 org.webjars jquery - 2.1.3 + 3.7.1 runtime org.webjars bootstrap - 3.3.4 + 5.3.3 runtime @@ -94,6 +94,12 @@ test + + org.springframework.boot + spring-boot-testcontainers + test + + diff --git a/web/querydsl/src/main/resources/templates/index.html b/web/querydsl/src/main/resources/templates/index.html index 6e59b3e99..92abc3d0c 100644 --- a/web/querydsl/src/main/resources/templates/index.html +++ b/web/querydsl/src/main/resources/templates/index.html @@ -23,23 +23,27 @@

Search

- +
- +
- +
- +
- +
@@ -69,13 +73,13 @@

Search

City Street Email - + - 1. + 1. - + Firstname Lastname diff --git a/web/querydsl/src/test/java/example/users/ApplicationTests.java b/web/querydsl/src/test/java/example/users/ApplicationTests.java index 594ea2331..0f83c0706 100644 --- a/web/querydsl/src/test/java/example/users/ApplicationTests.java +++ b/web/querydsl/src/test/java/example/users/ApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2024 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,44 +15,26 @@ */ package example.users; - import org.junit.jupiter.api.Test; - import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.testcontainers.containers.MongoDBContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; /** * @author Oliver Gierke * @author Divya Srivastava + * @author Tim Sparg */ @Testcontainers @SpringBootTest class ApplicationTests { - @Container // - private static MongoDBContainer mongoDBContainer = MongoContainers.getDefaultContainer(); - - @DynamicPropertySource - static void setProperties(DynamicPropertyRegistry registry) { - registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); - } + @Container + @ServiceConnection static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:7"); @Test void contextBootstraps() {} - static class MongoContainers { - - private static final String IMAGE_NAME = "mongo:5.0"; - private static final String IMAGE_NAME_PROPERTY = "mongo.default.image.name"; - - public static MongoDBContainer getDefaultContainer() { - return new MongoDBContainer(DockerImageName.parse(System.getProperty(IMAGE_NAME_PROPERTY, IMAGE_NAME))) - .withReuse(true); - } - } } diff --git a/web/querydsl/src/test/java/example/users/TestApplication.java b/web/querydsl/src/test/java/example/users/TestApplication.java new file mode 100644 index 000000000..054772e49 --- /dev/null +++ b/web/querydsl/src/test/java/example/users/TestApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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.users; + +import org.springframework.boot.SpringApplication; + +/** + * @author Tim Sparg + * @author Oliver Drotbohm + */ +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.from(Application::main) + .with(TestcontainersConfiguration.class) + .run(args); + } +} diff --git a/web/querydsl/src/test/java/example/users/TestcontainersConfiguration.java b/web/querydsl/src/test/java/example/users/TestcontainersConfiguration.java new file mode 100644 index 000000000..077a7d4f3 --- /dev/null +++ b/web/querydsl/src/test/java/example/users/TestcontainersConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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.users; + +import org.springframework.boot.devtools.restart.RestartScope; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * @author Tim Sparg + * @author Oliver Drotbohm + */ +@TestConfiguration(proxyBeanMethods = false) +public class TestcontainersConfiguration { + + @Bean + @ServiceConnection + @RestartScope + MongoDBContainer mongoDbContainer() { + return new MongoDBContainer(DockerImageName.parse("mongo:latest")); + } +}