diff --git a/r2dbc/pom.xml b/r2dbc/pom.xml index 5c6e2732..fbb69575 100644 --- a/r2dbc/pom.xml +++ b/r2dbc/pom.xml @@ -18,6 +18,7 @@ example + query-by-example diff --git a/r2dbc/query-by-example/README.adoc b/r2dbc/query-by-example/README.adoc new file mode 100644 index 00000000..37015d3a --- /dev/null +++ b/r2dbc/query-by-example/README.adoc @@ -0,0 +1,35 @@ +== Spring Data R2DBC - Query-by-Example (QBE) example + +This project contains samples of Query-by-Example of Spring Data R2DBC. + +=== Support for Query-by-Example + +Query by Example (QBE) is a user-friendly querying technique with a simple interface. +It allows dynamic query creation and does not require to write queries containing field names. +In fact, Query by Example does not require to write queries using SQL at all. + +An `Example` takes a data object (usually the entity object or a subtype of it) and a specification how to match properties. +You can use Query by Example with Repositories. + +[source,java] +---- +interface PersonRepository extends ReactiveCrudRepository, ReactiveQueryByExampleExecutor { +} +---- + +[source,java] +---- +Example example = Example.of(new Person("Jon", "Snow")); +repo.findAll(example); + + +ExampleMatcher matcher = ExampleMatcher.matching(). + .withMatcher("firstname", endsWith()) + .withMatcher("lastname", startsWith().ignoreCase()); + +Example example = Example.of(new Person("Jon", "Snow"), matcher); +repo.count(example); +---- + +This example contains shows the usage with `PersonRepositoryIntegrationTests`. + diff --git a/r2dbc/query-by-example/pom.xml b/r2dbc/query-by-example/pom.xml new file mode 100644 index 00000000..ec3fc86c --- /dev/null +++ b/r2dbc/query-by-example/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + + org.springframework.data.examples + spring-data-r2dbc-examples + 2.0.0.BUILD-SNAPSHOT + + + spring-data-r2dbc-query-by-example + + Spring Data R2DBC - Query by Example + + diff --git a/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/Person.java b/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/Person.java new file mode 100644 index 00000000..775afbd5 --- /dev/null +++ b/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/Person.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.r2dbc.basics; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import org.springframework.data.annotation.Id; + +/** + * Sample person class. + * + * @author Mark Paluch + */ +@Data +@AllArgsConstructor +class Person { + + @Id Integer id; + String firstname, lastname; + Integer age; + + boolean hasId() { + return id != null; + } +} diff --git a/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/PersonRepository.java b/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/PersonRepository.java new file mode 100644 index 00000000..fe267454 --- /dev/null +++ b/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/PersonRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.r2dbc.basics; + +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +/** + * Simple repository interface for {@link Person} instances. The interface implements + * {@link ReactiveQueryByExampleExecutor} and allows execution of methods accepting + * {@link org.springframework.data.domain.Example}. + * + * @author Mark Paluch + */ +interface PersonRepository extends ReactiveCrudRepository, ReactiveQueryByExampleExecutor {} diff --git a/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java b/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java new file mode 100644 index 00000000..f1ecf3a5 --- /dev/null +++ b/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.r2dbc.basics; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Mark Paluch + */ +@SpringBootApplication +@EnableTransactionManagement +class InfrastructureConfiguration {} diff --git a/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/PersonRepositoryIntegrationTests.java b/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/PersonRepositoryIntegrationTests.java new file mode 100644 index 00000000..63007c2d --- /dev/null +++ b/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/PersonRepositoryIntegrationTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.r2dbc.basics; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.ExampleMatcher.*; +import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.*; + +import reactor.core.publisher.Hooks; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Example; +import org.springframework.r2dbc.core.DatabaseClient; + +/** + * Integration test showing the usage of R2DBC Query-by-Example support through Spring Data repositories. + * + * @author Mark Paluch + */ +@SpringBootTest(classes = InfrastructureConfiguration.class) +class PersonRepositoryIntegrationTests { + + @Autowired PersonRepository people; + @Autowired DatabaseClient client; + + private Person skyler, walter, flynn, marie, hank; + + @BeforeEach + void setUp() { + + Hooks.onOperatorDebug(); + + List statements = Arrays.asList(// + "DROP TABLE IF EXISTS person;", + "CREATE TABLE person (id SERIAL PRIMARY KEY, firstname VARCHAR(100) NOT NULL, lastname VARCHAR(100) NOT NULL, age INTEGER NOT NULL);"); + + this.skyler = new Person(null, "Skyler", "White", 45); + this.walter = new Person(null, "Walter", "White", 50); + this.flynn = new Person(null, "Walter Jr. (Flynn)", "White", 17); + this.marie = new Person(null, "Marie", "Schrader", 38); + this.hank = new Person(null, "Hank", "Schrader", 43); + + statements.stream().map(client::sql) // + .map(DatabaseClient.GenericExecuteSpec::then) // + .forEach(it -> it.as(StepVerifier::create).verifyComplete()); + + people.saveAll(Arrays.asList(skyler, walter, flynn, marie, hank)) // + .as(StepVerifier::create) // + .expectNextCount(5) // + .verifyComplete(); + } + + @Test + void countBySimpleExample() { + + Example example = Example.of(new Person(null, null, "White", null)); + + people.count(example).as(StepVerifier::create) // + .expectNext(3L) // + .verifyComplete(); + } + + @Test + void ignorePropertiesAndMatchByAge() { + + Example example = Example.of(flynn, matching(). // + withIgnorePaths("firstname", "lastname")); + + people.findOne(example) // + .as(StepVerifier::create) // + .expectNext(flynn) // + .verifyComplete(); + } + + @Test + void substringMatching() { + + Example example = Example.of(new Person(null, "er", null, null), matching(). // + withStringMatcher(StringMatcher.ENDING)); + + people.findAll(example).collectList() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual).containsExactlyInAnyOrder(skyler, walter); + }) // + .verifyComplete(); + } + + @Test + void matchStartingStringsIgnoreCase() { + + Example example = Example.of(new Person(null, "Walter", "WHITE", null), matching(). // + withIgnorePaths("age"). // + withMatcher("firstname", startsWith()). // + withMatcher("lastname", ignoreCase())); + + people.findAll(example).collectList() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual).containsExactlyInAnyOrder(flynn, walter); + }) // + .verifyComplete(); + } + + @Test + void configuringMatchersUsingLambdas() { + + Example example = Example.of(new Person(null, "Walter", "WHITE", null), matching(). // + withIgnorePaths("age"). // + withMatcher("firstname", GenericPropertyMatcher::startsWith). // + withMatcher("lastname", GenericPropertyMatcher::ignoreCase)); + + people.findAll(example).collectList() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual).containsExactlyInAnyOrder(flynn, walter); + }) // + .verifyComplete(); + } + + @Test + void valueTransformer() { + + Example example = Example.of(new Person(null, null, "White", 99), matching(). // + withMatcher("age", matcher -> matcher.transform(value -> Optional.of(50)))); + + people.findAll(example).collectList() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual).containsExactlyInAnyOrder(walter); + }) // + .verifyComplete(); + } +}