Skip to content

Commit bcd35a0

Browse files
author
lub2code
committed
Implement Query by Example.
Implement Spring Data's Query by Example feature. See #532 and spring-projects/spring-data-relational#929.
1 parent 65872fb commit bcd35a0

12 files changed

+507
-26
lines changed

Diff for: pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<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">
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
34

45
<modelVersion>4.0.0</modelVersion>
56

Diff for: src/main/asciidoc/new-features.adoc

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
[[new-features]]
22
= New & Noteworthy
33

4+
[[new-features.1-3-0]]
5+
== What's New in Spring Data R2DBC 1.3.0
6+
7+
* Introduce <<r2dbc.repositories.queries.query-by-example,Query by Example support>>.
8+
49
[[new-features.1-2-0]]
510
== What's New in Spring Data R2DBC 1.2.0
611

Diff for: src/main/asciidoc/reference/r2dbc-repositories.adoc

+41
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,47 @@ Extensions are retrieved from the application context at the time of SpEL evalua
279279

280280
TIP: When using SpEL expressions in combination with plain parameters, use named parameter notation instead of native bind markers to ensure a proper binding order.
281281

282+
[[r2dbc.repositories.queries.query-by-example]]
283+
=== Query By Example
284+
285+
Spring Data R2DBC also lets you use Query By Example to fashion queries.
286+
This technique allows you to use a "probe" object.
287+
Essentially, any field that isn't empty or `null` will be used to match.
288+
289+
Here's an example:
290+
291+
====
292+
[source,java,indent=0]
293+
----
294+
include::../{example-root}/QueryByExampleTests.java[tag=example]
295+
----
296+
<1> Create a domain object with the criteria (`null` fields will be ignored).
297+
<2> Using the domain object, create an `Example`.
298+
<3> Through the `R2dbcRepository`, execute query (use `findOne` for a `Mono`).
299+
====
300+
301+
This illustrates how to craft a simple probe using a domain object.
302+
In this case, it will query based on the `Employee` object's `name` field being equal to `Frodo`.
303+
`null` fields are ignored.
304+
305+
====
306+
[source,java,indent=0]
307+
----
308+
include::../{example-root}/QueryByExampleTests.java[tag=example-2]
309+
----
310+
<1> Create a custom `ExampleMatcher` that matches on ALL fields (use `matchingAny()` to match on *ANY* fields)
311+
<2> For the `name` field, use a wildcard that matches against the end of the field
312+
<3> Match columns against `null` (don't forget that `NULL` doesn't equal `NULL` in relational databases).
313+
<4> Ignore the `role` field when forming the query.
314+
<5> Plug the custom `ExampleMatcher` into the probe.
315+
====
316+
317+
It's also possible to apply a `withTransform()` against any property, allowing you to transform a property before forming the query.
318+
For example, you can apply a `toUpperCase()` to a `String` -based property before the query is created.
319+
320+
Query By Example really shines when you you don't know all the fields needed in a query in advance.
321+
If you were building a filter on a web page where the user can pick the fields, Query By Example is a great way to flexibly capture that into an efficient query.
322+
282323
[[r2dbc.entity-persistence.state-detection-strategies]]
283324
=== Entity State Detection Strategies
284325

Diff for: src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
package org.springframework.data.r2dbc.repository;
1717

1818
import org.springframework.data.repository.NoRepositoryBean;
19+
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
1920
import org.springframework.data.repository.reactive.ReactiveSortingRepository;
2021

2122
/**
2223
* R2DBC specific {@link org.springframework.data.repository.Repository} interface with reactive support.
2324
*
2425
* @author Mark Paluch
2526
* @author Stephen Cohen
27+
* @author Greg Turnquist
2628
*/
2729
@NoRepositoryBean
28-
public interface R2dbcRepository<T, ID> extends ReactiveSortingRepository<T, ID> {}
30+
public interface R2dbcRepository<T, ID> extends ReactiveSortingRepository<T, ID>, ReactiveQueryByExampleExecutor<T> {}

Diff for: src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java

+62-2
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@
2121
import java.util.List;
2222

2323
import org.reactivestreams.Publisher;
24-
24+
import org.springframework.data.domain.Example;
2525
import org.springframework.data.domain.Sort;
2626
import org.springframework.data.r2dbc.convert.R2dbcConverter;
2727
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
2828
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
2929
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
30+
import org.springframework.data.r2dbc.repository.R2dbcRepository;
3031
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
3132
import org.springframework.data.relational.core.query.Criteria;
3233
import org.springframework.data.relational.core.query.Query;
3334
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
35+
import org.springframework.data.relational.repository.query.RelationalExampleMapper;
3436
import org.springframework.data.repository.reactive.ReactiveSortingRepository;
3537
import org.springframework.data.util.Lazy;
3638
import org.springframework.data.util.Streamable;
@@ -45,13 +47,15 @@
4547
* @author Jens Schauder
4648
* @author Mingyuan Wu
4749
* @author Stephen Cohen
50+
* @author Greg Turnquist
4851
*/
4952
@Transactional(readOnly = true)
50-
public class SimpleR2dbcRepository<T, ID> implements ReactiveSortingRepository<T, ID> {
53+
public class SimpleR2dbcRepository<T, ID> implements R2dbcRepository<T, ID> {
5154

5255
private final RelationalEntityInformation<T, ID> entity;
5356
private final R2dbcEntityOperations entityOperations;
5457
private final Lazy<RelationalPersistentProperty> idProperty;
58+
private final RelationalExampleMapper exampleMapper;
5559

5660
/**
5761
* Create a new {@link SimpleR2dbcRepository}.
@@ -70,6 +74,7 @@ public SimpleR2dbcRepository(RelationalEntityInformation<T, ID> entity, R2dbcEnt
7074
.getMappingContext() //
7175
.getRequiredPersistentEntity(this.entity.getJavaType()) //
7276
.getRequiredIdProperty());
77+
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
7378
}
7479

7580
/**
@@ -90,6 +95,7 @@ public SimpleR2dbcRepository(RelationalEntityInformation<T, ID> entity, Database
9095
.getMappingContext() //
9196
.getRequiredPersistentEntity(this.entity.getJavaType()) //
9297
.getRequiredIdProperty());
98+
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
9399
}
94100

95101
/**
@@ -112,6 +118,7 @@ public SimpleR2dbcRepository(RelationalEntityInformation<T, ID> entity,
112118
.getMappingContext() //
113119
.getRequiredPersistentEntity(this.entity.getJavaType()) //
114120
.getRequiredIdProperty());
121+
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
115122
}
116123

117124
// -------------------------------------------------------------------------
@@ -372,6 +379,59 @@ public Flux<T> findAll(Sort sort) {
372379
return this.entityOperations.select(Query.empty().sort(sort), this.entity.getJavaType());
373380
}
374381

382+
// -------------------------------------------------------------------------
383+
// Methods from ReactiveQueryByExampleExecutor
384+
// -------------------------------------------------------------------------
385+
386+
@Override
387+
public <S extends T> Mono<S> findOne(Example<S> example) {
388+
389+
Assert.notNull(example, "Example must not be null!");
390+
391+
Query query = this.exampleMapper.getMappedExample(example);
392+
393+
return this.entityOperations.selectOne(query, example.getProbeType());
394+
}
395+
396+
@Override
397+
public <S extends T> Flux<S> findAll(Example<S> example) {
398+
399+
Assert.notNull(example, "Example must not be null!");
400+
401+
return findAll(example, Sort.unsorted());
402+
}
403+
404+
@Override
405+
public <S extends T> Flux<S> findAll(Example<S> example, Sort sort) {
406+
407+
Assert.notNull(example, "Example must not be null!");
408+
Assert.notNull(sort, "Sort must not be null!");
409+
410+
Query query = this.exampleMapper.getMappedExample(example).sort(sort);
411+
412+
return this.entityOperations.select(query, example.getProbeType());
413+
}
414+
415+
@Override
416+
public <S extends T> Mono<Long> count(Example<S> example) {
417+
418+
Assert.notNull(example, "Example must not be null!");
419+
420+
Query query = this.exampleMapper.getMappedExample(example);
421+
422+
return this.entityOperations.count(query, example.getProbeType());
423+
}
424+
425+
@Override
426+
public <S extends T> Mono<Boolean> exists(Example<S> example) {
427+
428+
Assert.notNull(example, "Example must not be null!");
429+
430+
Query query = this.exampleMapper.getMappedExample(example);
431+
432+
return this.entityOperations.exists(query, example.getProbeType());
433+
}
434+
375435
private RelationalPersistentProperty getIdProperty() {
376436
return this.idProperty.get();
377437
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.springframework.data.r2dbc.documentation;
2+
3+
import static org.springframework.data.domain.ExampleMatcher.*;
4+
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.*;
5+
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
import reactor.core.publisher.Flux;
9+
10+
import org.junit.jupiter.api.Test;
11+
import org.springframework.data.annotation.Id;
12+
import org.springframework.data.domain.Example;
13+
import org.springframework.data.domain.ExampleMatcher;
14+
import org.springframework.data.r2dbc.repository.R2dbcRepository;
15+
16+
public class QueryByExampleTests {
17+
18+
private EmployeeRepository repository;
19+
20+
@Test
21+
void queryByExampleSimple() {
22+
23+
// tag::example[]
24+
Employee employee = new Employee(); // <1>
25+
employee.setName("Frodo");
26+
27+
Example<Employee> example = Example.of(employee); // <2>
28+
29+
Flux<Employee> employees = repository.findAll(example); // <3>
30+
31+
// do whatever with the flux
32+
// end::example[]
33+
}
34+
35+
@Test
36+
void queryByExampleCustomMatcher() {
37+
38+
// tag::example-2[]
39+
Employee employee = new Employee();
40+
employee.setName("Baggins");
41+
employee.setRole("ring bearer");
42+
43+
ExampleMatcher matcher = matching() // <1>
44+
.withMatcher("name", endsWith()) // <2>
45+
.withIncludeNullValues() // <3>
46+
.withIgnorePaths("role"); // <4>
47+
Example<Employee> example = Example.of(employee, matcher); // <5>
48+
49+
Flux<Employee> employees = repository.findAll(example);
50+
51+
// do whatever with the flux
52+
// end::example-2[]
53+
}
54+
55+
@Data
56+
@NoArgsConstructor
57+
public class Employee {
58+
59+
private @Id Integer id;
60+
private String name;
61+
private String role;
62+
}
63+
64+
public interface EmployeeRepository extends R2dbcRepository<Employee, Integer> {}
65+
}

Diff for: src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java

-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.junit.jupiter.api.BeforeEach;
3333
import org.junit.jupiter.api.Test;
3434
import org.junit.jupiter.api.extension.ExtendWith;
35-
3635
import org.springframework.beans.factory.annotation.Autowired;
3736
import org.springframework.context.annotation.ComponentScan;
3837
import org.springframework.context.annotation.Configuration;

0 commit comments

Comments
 (0)