diff --git a/pom.xml b/pom.xml index 80fa6093..2d8394ad 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 2b12f309..35a73281 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -1,6 +1,11 @@ [[new-features]] = New & Noteworthy +[[new-features.1-3-0]] +== What's New in Spring Data R2DBC 1.3.0 + +* Introduce <>. + [[new-features.1-2-0]] == What's New in Spring Data R2DBC 1.2.0 diff --git a/src/main/asciidoc/reference/r2dbc-repositories.adoc b/src/main/asciidoc/reference/r2dbc-repositories.adoc index 451d78e5..4365f7f0 100644 --- a/src/main/asciidoc/reference/r2dbc-repositories.adoc +++ b/src/main/asciidoc/reference/r2dbc-repositories.adoc @@ -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 diff --git a/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java index bece5141..f3a13f00 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java @@ -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 extends ReactiveSortingRepository {} +public interface R2dbcRepository extends ReactiveSortingRepository, ReactiveQueryByExampleExecutor {} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index d48313f7..21551626 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -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 implements ReactiveSortingRepository { +public class SimpleR2dbcRepository implements R2dbcRepository { private final RelationalEntityInformation entity; private final R2dbcEntityOperations entityOperations; private final Lazy idProperty; + private final RelationalExampleMapper exampleMapper; /** * Create a new {@link SimpleR2dbcRepository}. @@ -70,6 +74,7 @@ public class SimpleR2dbcRepository implements ReactiveSortingRepository implements ReactiveSortingRepository implements ReactiveSortingRepository implements ReactiveSortingRepository Mono findOne(Example example) { + + Assert.notNull(example, "Example must not be null!"); + + Query query = this.exampleMapper.getMappedExample(example); + + return this.entityOperations.selectOne(query, example.getProbeType()); + } + + @Override + public Flux findAll(Example example) { + + Assert.notNull(example, "Example must not be null!"); + + return findAll(example, Sort.unsorted()); + } + + @Override + public Flux findAll(Example 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 Mono count(Example example) { + + Assert.notNull(example, "Example must not be null!"); + + Query query = this.exampleMapper.getMappedExample(example); + + return this.entityOperations.count(query, example.getProbeType()); + } + + @Override + public Mono exists(Example 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(); } diff --git a/src/test/java/org/springframework/data/r2dbc/documentation/QueryByExampleTests.java b/src/test/java/org/springframework/data/r2dbc/documentation/QueryByExampleTests.java new file mode 100644 index 00000000..680be03c --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/documentation/QueryByExampleTests.java @@ -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 example = Example.of(employee); // <2> + + Flux 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 example = Example.of(employee, matcher); // <5> + + Flux 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 {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java index e8d821ba..897f47d8 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java @@ -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; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java index 552b6f48..511e1b30 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java @@ -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 repository; + SimpleR2dbcRepository repositoryWithNonScalarId; JdbcTemplate jdbc; @BeforeEach void before() { + MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext); + RelationalEntityInformation entityInformation = new MappingRelationalEntityInformation<>( (RelationalPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); - this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, - new MappingR2dbcConverter(mappingContext), strategy); + this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, converter, strategy); + + RelationalEntityInformation boxedEntityInformation = new MappingRelationalEntityInformation<>( + (RelationalPersistentEntity) 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 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 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 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 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 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 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 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 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 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 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 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 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 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 exampleWithRegExGlobal = Example.of(legoSet, matching().withStringMatcher(REGEX)); + + assertThatIllegalStateException().isThrownBy(() -> { + + repositoryWithNonScalarId.findAll(exampleWithRegExGlobal) // + .map(LegoSetWithNonScalarId::getName) // + .as(StepVerifier::create) // + .expectNext("Moon space base") // + .verifyComplete(); + }); + + Example 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 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 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 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 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) { diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java index 33c850dc..32a22cde 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java @@ -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 entityInformation = new MappingRelationalEntityInformation<>( diff --git a/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java index 49986837..c6085409 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java @@ -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" // + ");"; diff --git a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java index 26cd2d62..2d48c412 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java @@ -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" // + ");"; diff --git a/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java index 5a49d2fc..b3c9bad8 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java @@ -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" // + ");";