Skip to content

Commit

Permalink
Implement Query by Example.
Browse files Browse the repository at this point in the history
Implement Spring Data's Query by Example feature.

See #532 and spring-projects/spring-data-relational#929.
  • Loading branch information
lub2code committed Mar 4, 2021
1 parent 65872fb commit bcd35a0
Show file tree
Hide file tree
Showing 12 changed files with 507 additions and 26 deletions.
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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">
<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>

Expand Down
5 changes: 5 additions & 0 deletions src/main/asciidoc/new-features.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
[[new-features]]
= New & Noteworthy

[[new-features.1-3-0]]
== What's New in Spring Data R2DBC 1.3.0

* Introduce <<r2dbc.repositories.queries.query-by-example,Query by Example support>>.

[[new-features.1-2-0]]
== What's New in Spring Data R2DBC 1.2.0

Expand Down
41 changes: 41 additions & 0 deletions src/main/asciidoc/reference/r2dbc-repositories.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,47 @@ Extensions are retrieved from the application context at the time of SpEL evalua

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.

[[r2dbc.repositories.queries.query-by-example]]
=== Query By Example

Spring Data R2DBC also lets you use Query By Example to fashion queries.
This technique allows you to use a "probe" object.
Essentially, any field that isn't empty or `null` will be used to match.

Here's an example:

====
[source,java,indent=0]
----
include::../{example-root}/QueryByExampleTests.java[tag=example]
----
<1> Create a domain object with the criteria (`null` fields will be ignored).
<2> Using the domain object, create an `Example`.
<3> Through the `R2dbcRepository`, execute query (use `findOne` for a `Mono`).
====

This illustrates how to craft a simple probe using a domain object.
In this case, it will query based on the `Employee` object's `name` field being equal to `Frodo`.
`null` fields are ignored.

====
[source,java,indent=0]
----
include::../{example-root}/QueryByExampleTests.java[tag=example-2]
----
<1> Create a custom `ExampleMatcher` that matches on ALL fields (use `matchingAny()` to match on *ANY* fields)
<2> For the `name` field, use a wildcard that matches against the end of the field
<3> Match columns against `null` (don't forget that `NULL` doesn't equal `NULL` in relational databases).
<4> Ignore the `role` field when forming the query.
<5> Plug the custom `ExampleMatcher` into the probe.
====

It's also possible to apply a `withTransform()` against any property, allowing you to transform a property before forming the query.
For example, you can apply a `toUpperCase()` to a `String` -based property before the query is created.

Query By Example really shines when you you don't know all the fields needed in a query in advance.
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.

[[r2dbc.entity-persistence.state-detection-strategies]]
=== Entity State Detection Strategies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
package org.springframework.data.r2dbc.repository;

import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.reactive.ReactiveSortingRepository;

/**
* R2DBC specific {@link org.springframework.data.repository.Repository} interface with reactive support.
*
* @author Mark Paluch
* @author Stephen Cohen
* @author Greg Turnquist
*/
@NoRepositoryBean
public interface R2dbcRepository<T, ID> extends ReactiveSortingRepository<T, ID> {}
public interface R2dbcRepository<T, ID> extends ReactiveSortingRepository<T, ID>, ReactiveQueryByExampleExecutor<T> {}
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@
import java.util.List;

import org.reactivestreams.Publisher;

import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
import org.springframework.data.relational.repository.query.RelationalExampleMapper;
import org.springframework.data.repository.reactive.ReactiveSortingRepository;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Streamable;
Expand All @@ -45,13 +47,15 @@
* @author Jens Schauder
* @author Mingyuan Wu
* @author Stephen Cohen
* @author Greg Turnquist
*/
@Transactional(readOnly = true)
public class SimpleR2dbcRepository<T, ID> implements ReactiveSortingRepository<T, ID> {
public class SimpleR2dbcRepository<T, ID> implements R2dbcRepository<T, ID> {

private final RelationalEntityInformation<T, ID> entity;
private final R2dbcEntityOperations entityOperations;
private final Lazy<RelationalPersistentProperty> idProperty;
private final RelationalExampleMapper exampleMapper;

/**
* Create a new {@link SimpleR2dbcRepository}.
Expand All @@ -70,6 +74,7 @@ public SimpleR2dbcRepository(RelationalEntityInformation<T, ID> entity, R2dbcEnt
.getMappingContext() //
.getRequiredPersistentEntity(this.entity.getJavaType()) //
.getRequiredIdProperty());
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
}

/**
Expand All @@ -90,6 +95,7 @@ public SimpleR2dbcRepository(RelationalEntityInformation<T, ID> entity, Database
.getMappingContext() //
.getRequiredPersistentEntity(this.entity.getJavaType()) //
.getRequiredIdProperty());
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
}

/**
Expand All @@ -112,6 +118,7 @@ public SimpleR2dbcRepository(RelationalEntityInformation<T, ID> entity,
.getMappingContext() //
.getRequiredPersistentEntity(this.entity.getJavaType()) //
.getRequiredIdProperty());
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
}

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

// -------------------------------------------------------------------------
// Methods from ReactiveQueryByExampleExecutor
// -------------------------------------------------------------------------

@Override
public <S extends T> Mono<S> findOne(Example<S> example) {

Assert.notNull(example, "Example must not be null!");

Query query = this.exampleMapper.getMappedExample(example);

return this.entityOperations.selectOne(query, example.getProbeType());
}

@Override
public <S extends T> Flux<S> findAll(Example<S> example) {

Assert.notNull(example, "Example must not be null!");

return findAll(example, Sort.unsorted());
}

@Override
public <S extends T> Flux<S> findAll(Example<S> example, Sort sort) {

Assert.notNull(example, "Example must not be null!");
Assert.notNull(sort, "Sort must not be null!");

Query query = this.exampleMapper.getMappedExample(example).sort(sort);

return this.entityOperations.select(query, example.getProbeType());
}

@Override
public <S extends T> Mono<Long> count(Example<S> example) {

Assert.notNull(example, "Example must not be null!");

Query query = this.exampleMapper.getMappedExample(example);

return this.entityOperations.count(query, example.getProbeType());
}

@Override
public <S extends T> Mono<Boolean> exists(Example<S> example) {

Assert.notNull(example, "Example must not be null!");

Query query = this.exampleMapper.getMappedExample(example);

return this.entityOperations.exists(query, example.getProbeType());
}

private RelationalPersistentProperty getIdProperty() {
return this.idProperty.get();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.springframework.data.r2dbc.documentation;

import static org.springframework.data.domain.ExampleMatcher.*;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.*;

import lombok.Data;
import lombok.NoArgsConstructor;
import reactor.core.publisher.Flux;

import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.r2dbc.repository.R2dbcRepository;

public class QueryByExampleTests {

private EmployeeRepository repository;

@Test
void queryByExampleSimple() {

// tag::example[]
Employee employee = new Employee(); // <1>
employee.setName("Frodo");

Example<Employee> example = Example.of(employee); // <2>

Flux<Employee> employees = repository.findAll(example); // <3>

// do whatever with the flux
// end::example[]
}

@Test
void queryByExampleCustomMatcher() {

// tag::example-2[]
Employee employee = new Employee();
employee.setName("Baggins");
employee.setRole("ring bearer");

ExampleMatcher matcher = matching() // <1>
.withMatcher("name", endsWith()) // <2>
.withIncludeNullValues() // <3>
.withIgnorePaths("role"); // <4>
Example<Employee> example = Example.of(employee, matcher); // <5>

Flux<Employee> employees = repository.findAll(example);

// do whatever with the flux
// end::example-2[]
}

@Data
@NoArgsConstructor
public class Employee {

private @Id Integer id;
private String name;
private String role;
}

public interface EmployeeRepository extends R2dbcRepository<Employee, Integer> {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
Expand Down
Loading

0 comments on commit bcd35a0

Please sign in to comment.