Implement Query by Example.

Implement Spring Data's Query by Example feature.

See #532 and https://github.com/spring-projects/spring-data-jdbc/issues/929.
This commit is contained in:
Greg L. Turnquist
2021-02-16 16:57:08 -06:00
parent 9b256ed149
commit 9336d09fe9
12 changed files with 507 additions and 26 deletions

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@
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;
/**
@@ -23,6 +24,7 @@ import org.springframework.data.repository.reactive.ReactiveSortingRepository;
*
* @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> {}

View File

@@ -21,16 +21,18 @@ import reactor.core.publisher.Mono;
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;
@@ -45,13 +47,15 @@ import org.springframework.util.Assert;
* @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}.
@@ -70,6 +74,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveSortingRepository<T
.getMappingContext() //
.getRequiredPersistentEntity(this.entity.getJavaType()) //
.getRequiredIdProperty());
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
}
/**
@@ -90,6 +95,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveSortingRepository<T
.getMappingContext() //
.getRequiredPersistentEntity(this.entity.getJavaType()) //
.getRequiredIdProperty());
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
}
/**
@@ -112,6 +118,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveSortingRepository<T
.getMappingContext() //
.getRequiredPersistentEntity(this.entity.getJavaType()) //
.getRequiredIdProperty());
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
}
// -------------------------------------------------------------------------
@@ -372,6 +379,59 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveSortingRepository<T
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();
}

View File

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

View File

@@ -32,7 +32,6 @@ import javax.sql.DataSource;
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;

View File

@@ -16,6 +16,9 @@
package org.springframework.data.r2dbc.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.domain.ExampleMatcher.*;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.*;
import static org.springframework.data.domain.ExampleMatcher.StringMatcher.*;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -33,12 +36,12 @@ import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
@@ -58,6 +61,7 @@ import org.springframework.r2dbc.core.DatabaseClient;
* @author Bogdan Ilchyshyn
* @author Stephen Cohen
* @author Jens Schauder
* @author Greg Turnquist
*/
public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport {
@@ -68,16 +72,25 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
@Autowired private ReactiveDataAccessStrategy strategy;
SimpleR2dbcRepository<LegoSet, Integer> repository;
SimpleR2dbcRepository<LegoSetWithNonScalarId, Integer> repositoryWithNonScalarId;
JdbcTemplate jdbc;
@BeforeEach
void before() {
MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext);
RelationalEntityInformation<LegoSet, Integer> entityInformation = new MappingRelationalEntityInformation<>(
(RelationalPersistentEntity<LegoSet>) mappingContext.getRequiredPersistentEntity(LegoSet.class));
this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient,
new MappingR2dbcConverter(mappingContext), strategy);
this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, converter, strategy);
RelationalEntityInformation<LegoSetWithNonScalarId, Integer> boxedEntityInformation = new MappingRelationalEntityInformation<>(
(RelationalPersistentEntity<LegoSetWithNonScalarId>) mappingContext
.getRequiredPersistentEntity(LegoSetWithNonScalarId.class));
this.repositoryWithNonScalarId = new SimpleR2dbcRepository<>(boxedEntityInformation, databaseClient, converter,
strategy);
this.jdbc = createJdbcTemplate(createDataSource());
try {
@@ -117,10 +130,8 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
repository.save(new LegoSet(0, "SCHAUFELRADBAGGER", 12)) //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual.getId()).isGreaterThan(0);
}).verifyComplete();
.consumeNextWith(actual -> assertThat(actual.getId()).isGreaterThan(0)) //
.verifyComplete();
}
@Test // gh-93
@@ -174,7 +185,10 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
.verifyComplete();
Map<String, Object> map = jdbc.queryForMap("SELECT * FROM legoset");
assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsEntry("manual", 14).containsKey("id");
assertThat(map) //
.containsEntry("name", "SCHAUFELRADBAGGER") //
.containsEntry("manual", 14) //
.containsKey("id");
}
@Test // gh-93
@@ -194,7 +208,7 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
assertThat(legoSet.getVersion()).isEqualTo(43);
Map<String, Object> map = jdbc.queryForMap("SELECT * FROM legoset");
assertThat(map)
assertThat(map) //
.containsEntry("name", "SCHAUFELRADBAGGER") //
.containsEntry("manual", 14) //
.containsEntry("version", 43) //
@@ -311,10 +325,8 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
.map(LegoSet::getName) //
.collectList() //
.as(StepVerifier::create) //
.assertNext(actual -> {
assertThat(actual).hasSize(2).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF");
}).verifyComplete();
.assertNext(actual -> assertThat(actual).containsExactly("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF"))
.verifyComplete();
}
@Test // gh-407
@@ -329,12 +341,12 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
.map(LegoSet::getName) //
.collectList() //
.as(StepVerifier::create) //
.assertNext(actual -> assertThat(actual).containsExactly(
"SCHAUFELRADBAGGER",
"FORSCHUNGSSCHIFF",
"RALLYEAUTO",
"VOLTRON"
)).verifyComplete();
.assertNext(actual -> assertThat(actual).containsExactly( //
"SCHAUFELRADBAGGER", //
"FORSCHUNGSSCHIFF", //
"RALLYEAUTO", //
"VOLTRON"))
.verifyComplete();
}
@Test
@@ -480,21 +492,313 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
assertThat(count).isEqualTo(0);
}
@Test // gh-538
void shouldSelectByExampleUsingId() {
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)");
Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class);
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setId(id);
Example<LegoSetWithNonScalarId> example = Example.of(legoSet);
repositoryWithNonScalarId.findOne(example) //
.as(StepVerifier::create) //
.expectNext(new LegoSetWithNonScalarId(id, "SCHAUFELRADBAGGER", 12, null)) //
.verifyComplete();
}
@Test // gh-538
void shouldSelectByExampleUsingName() {
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(0, 'SCHAUFELRADBAGGER', 12)");
Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class);
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setName("SCHAUFELRADBAGGER");
Example<LegoSetWithNonScalarId> example = Example.of(legoSet);
repositoryWithNonScalarId.findOne(example) //
.as(StepVerifier::create) //
.expectNext(new LegoSetWithNonScalarId(id, "SCHAUFELRADBAGGER", 12, null)) //
.verifyComplete();
}
@Test // gh-538
void shouldSelectByExampleUsingManual() {
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)");
Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class);
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setManual(12);
Example<LegoSetWithNonScalarId> example = Example.of(legoSet);
repositoryWithNonScalarId.findOne(example) //
.as(StepVerifier::create) //
.expectNext(new LegoSetWithNonScalarId(id, "SCHAUFELRADBAGGER", 12, null)) //
.verifyComplete();
}
@Test // gh-538
void shouldSelectByExampleUsingGlobalStringMatcher() {
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(1, 'Moon space base', 12)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(2, 'Mars space base', 13)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(3, 'Moon construction kit', 14)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(4, 'Mars construction kit', 15)");
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setName("Moon");
Example<LegoSetWithNonScalarId> exampleByStarting = Example.of(legoSet, matching().withStringMatcher(STARTING));
repositoryWithNonScalarId.findAll(exampleByStarting) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base") //
.expectNext("Moon construction kit") //
.verifyComplete();
legoSet.setName("base");
Example<LegoSetWithNonScalarId> exampleByEnding = Example.of(legoSet, matching().withStringMatcher(ENDING));
repositoryWithNonScalarId.findAll(exampleByEnding) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base") //
.expectNext("Mars space base") //
.verifyComplete();
legoSet.setName("construction");
Example<LegoSetWithNonScalarId> exampleByContaining = Example.of(legoSet, matching().withStringMatcher(CONTAINING));
repositoryWithNonScalarId.findAll(exampleByContaining) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon construction kit") //
.expectNext("Mars construction kit") //
.verifyComplete();
}
@Test // gh-538
void shouldSelectByExampleUsingFieldLevelStringMatcher() {
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('Moon space base', 12)");
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('Mars space base', 13)");
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('Moon construction kit', 14)");
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('Mars construction kit', 15)");
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setName("Moon");
Example<LegoSetWithNonScalarId> exampleByFieldBasedStartsWith = Example.of(legoSet,
matching().withMatcher("name", startsWith()));
repositoryWithNonScalarId.findAll(exampleByFieldBasedStartsWith) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base") //
.expectNext("Moon construction kit") //
.verifyComplete();
legoSet.setName("base");
Example<LegoSetWithNonScalarId> exampleByFieldBasedEndsWith = Example.of(legoSet,
matching().withMatcher("name", endsWith()));
repositoryWithNonScalarId.findAll(exampleByFieldBasedEndsWith) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base") //
.expectNext("Mars space base") //
.verifyComplete();
legoSet.setName("construction");
Example<LegoSetWithNonScalarId> exampleByFieldBasedConstruction = Example.of(legoSet,
matching().withMatcher("name", contains()));
repositoryWithNonScalarId.findAll(exampleByFieldBasedConstruction) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon construction kit") //
.expectNext("Mars construction kit") //
.verifyComplete();
}
@Test // gh-538
void shouldSelectByExampleIgnoringCase() {
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(1, 'Moon space base', 12)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(2, 'Mars space base', 13)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(3, 'Moon construction kit', 14)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(4, 'Mars construction kit', 15)");
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setName("moon SPACE bAsE");
Example<LegoSetWithNonScalarId> exampleIgnoreCase = Example.of(legoSet, matching().withIgnoreCase());
repositoryWithNonScalarId.findAll(exampleIgnoreCase) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base") //
.verifyComplete();
legoSet.setName("moon SPACE bAsE");
Example<LegoSetWithNonScalarId> exampleByFieldBasedStartsWith = Example.of(legoSet,
matching().withMatcher("name", ignoreCase()));
repositoryWithNonScalarId.findAll(exampleByFieldBasedStartsWith) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base") //
.verifyComplete();
}
@Test // gh-538
void shouldFailSelectByExampleWhenUsingRegEx() {
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(1, 'Moon space base', 12)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(2, 'Mars space base', 13)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(3, 'Moon construction kit', 14)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(4, 'Mars construction kit', 15)");
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setName("moon");
Example<LegoSetWithNonScalarId> exampleWithRegExGlobal = Example.of(legoSet, matching().withStringMatcher(REGEX));
assertThatIllegalStateException().isThrownBy(() -> {
repositoryWithNonScalarId.findAll(exampleWithRegExGlobal) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base") //
.verifyComplete();
});
Example<LegoSetWithNonScalarId> exampleWithFieldRegEx = Example.of(legoSet,
matching().withMatcher("name", regex()));
assertThatIllegalStateException().isThrownBy(() -> {
repositoryWithNonScalarId.findAll(exampleWithFieldRegEx) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base") //
.verifyComplete();
});
}
@Test // gh-538
void shouldSelectByExampleIncludingNull() {
jdbc.execute("INSERT INTO legoset (id, name, extra, manual) VALUES(1, 'Moon space base', 'base', 12)");
jdbc.execute("INSERT INTO legoset (id, name, extra, manual) VALUES(2, 'Mars space base', 'base', 13)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(3, 'Moon construction kit', 14)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(4, 'Mars construction kit', 15)");
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setExtra("base");
Example<LegoSetWithNonScalarId> exampleIncludingNull = Example.of(legoSet, matching().withIncludeNullValues());
repositoryWithNonScalarId.findAll(exampleIncludingNull) //
.map(LegoSetWithNonScalarId::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base", "Mars space base", "Moon construction kit", "Mars construction kit") //
.verifyComplete();
}
@Test // gh-538
void shouldSelectByExampleWithAnyMatching() {
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(1, 'Moon space base', 12)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(2, 'Mars space base', 13)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(3, 'Moon construction kit', 14)");
jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(4, 'Mars construction kit', 15)");
LegoSet legoSet = new LegoSet();
legoSet.setName("Moon space base");
legoSet.setManual(15);
Example<LegoSet> exampleIncludingNull = Example.of(legoSet, matchingAny());
repository.findAll(exampleIncludingNull) //
.map(LegoSet::getName) //
.as(StepVerifier::create) //
.expectNext("Moon space base", "Mars construction kit") //
.verifyComplete();
}
@Test // gh-538
void shouldCountByExampleUsingId() {
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)");
Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class);
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setId(id);
Example<LegoSetWithNonScalarId> example = Example.of(legoSet);
repositoryWithNonScalarId.count(example) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
}
@Test // gh-538
void shouldCheckExistenceByExampleUsingId() {
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)");
Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class);
LegoSetWithNonScalarId legoSet = new LegoSetWithNonScalarId();
legoSet.setId(id);
Example<LegoSetWithNonScalarId> example = Example.of(legoSet);
repositoryWithNonScalarId.exists(example) //
.as(StepVerifier::create) //
.expectNext(true) //
.verifyComplete();
}
@Data
@Table("legoset")
@AllArgsConstructor
@NoArgsConstructor
static class LegoSet {
@Id int id;
String name;
Integer manual;
}
@Data
@Table("legoset")
@AllArgsConstructor
@NoArgsConstructor
static class LegoSetWithNonScalarId {
@Id Integer id;
String name;
Integer manual;
String extra;
}
@Data
@Table("legoset")
@NoArgsConstructor
static class LegoSetVersionable extends LegoSet {
@Version Integer version;
LegoSetVersionable(int id, String name, Integer manual, Integer version) {
@@ -507,6 +811,7 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
@Table("legoset")
@NoArgsConstructor
static class LegoSetPrimitiveVersionable extends LegoSet {
@Version int version;
LegoSetPrimitiveVersionable(int id, String name, Integer manual, int version) {

View File

@@ -28,7 +28,6 @@ import javax.sql.DataSource;
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.Configuration;
import org.springframework.dao.DataAccessException;
@@ -49,6 +48,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
* Integration tests for {@link SimpleR2dbcRepository} against H2.
*
* @author Mark Paluch
* @author Greg Turnquist
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@@ -86,7 +86,7 @@ public class H2SimpleR2dbcRepositoryIntegrationTests extends AbstractSimpleR2dbc
this.jdbc.execute("CREATE TABLE always_new (\n" //
+ " id integer PRIMARY KEY,\n" //
+ " name varchar(255) NOT NULL" //
+ " name varchar(255) NOT NULL\n" //
+ ");");
RelationalEntityInformation<AlwaysNew, Long> entityInformation = new MappingRelationalEntityInformation<>(

View File

@@ -43,6 +43,7 @@ public class H2TestSupport {
+ " id serial CONSTRAINT id PRIMARY KEY,\n" //
+ " version integer NULL,\n" //
+ " name varchar(255) NOT NULL,\n" //
+ " extra varchar(255),\n" //
+ " manual integer NULL\n" //
+ ");";

View File

@@ -36,6 +36,7 @@ public class PostgresTestSupport {
+ " id serial CONSTRAINT id PRIMARY KEY,\n" //
+ " version integer NULL,\n" //
+ " name varchar(255) NOT NULL,\n" //
+ " extra varchar(255),\n" //
+ " manual integer NULL\n" //
+ ");";

View File

@@ -28,6 +28,7 @@ public class SqlServerTestSupport {
+ " id integer IDENTITY(1,1) PRIMARY KEY,\n" //
+ " version integer NULL,\n" //
+ " name varchar(255) NOT NULL,\n" //
+ " extra varchar(255),\n" //
+ " manual integer NULL\n" //
+ ");";