Add example using R2DBC Query by Example.

Closes #613.
This commit is contained in:
Mark Paluch
2021-04-12 15:58:34 +02:00
parent e5238de37c
commit 162ad7af2f
7 changed files with 302 additions and 0 deletions

View File

@@ -18,6 +18,7 @@
<modules>
<module>example</module>
<module>query-by-example</module>
</modules>
<dependencies>

View File

@@ -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<Person, Long>, ReactiveQueryByExampleExecutor<Person> {
}
----
[source,java]
----
Example<Person> example = Example.of(new Person("Jon", "Snow"));
repo.findAll(example);
ExampleMatcher matcher = ExampleMatcher.matching().
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
Example<Person> example = Example.of(new Person("Jon", "Snow"), matcher);
repo.count(example);
----
This example contains shows the usage with `PersonRepositoryIntegrationTests`.

View File

@@ -0,0 +1,17 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-r2dbc-examples</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-data-r2dbc-query-by-example</artifactId>
<name>Spring Data R2DBC - Query by Example</name>
</project>

View File

@@ -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;
}
}

View File

@@ -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<Person, String>, ReactiveQueryByExampleExecutor<Person> {}

View File

@@ -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 {}

View File

@@ -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<String> 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<Person> example = Example.of(new Person(null, null, "White", null));
people.count(example).as(StepVerifier::create) //
.expectNext(3L) //
.verifyComplete();
}
@Test
void ignorePropertiesAndMatchByAge() {
Example<Person> example = Example.of(flynn, matching(). //
withIgnorePaths("firstname", "lastname"));
people.findOne(example) //
.as(StepVerifier::create) //
.expectNext(flynn) //
.verifyComplete();
}
@Test
void substringMatching() {
Example<Person> 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<Person> 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<Person> 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<Person> 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();
}
}