diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java index cf8172b..e94f36e 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java @@ -201,8 +201,20 @@ public interface DatabaseClient { */ interface SelectFromSpec { + /** + * Specify the source {@literal table} to select from. + * + * @param table must not be {@literal null} or empty. + * @return + */ GenericSelectSpec from(String table); + /** + * Specify the source table to select from to using the {@link Class entity class}. + * + * @param table must not be {@literal null}. + * @return + */ TypedSelectSpec from(Class table); } @@ -220,7 +232,7 @@ public interface DatabaseClient { GenericInsertSpec into(String table); /** - * Specify the target {@link Class} table to insert to using the {@link Class entity class}. + * Specify the target table to insert to using the {@link Class entity class}. * * @param table must not be {@literal null}. * @return @@ -296,12 +308,26 @@ public interface DatabaseClient { */ interface SelectSpec> { + /** + * Configure projected fields. + * + * @param selectedFields must not be {@literal null}. + */ S project(String... selectedFields); - S where(Object criteriaDefinition); + /** + * Configure {@link Sort}. + * + * @param sort must not be {@literal null}. + */ S orderBy(Sort sort); + /** + * Configure pagination. Overrides {@link Sort} if the {@link Pageable} contains a {@link Sort} object. + * + * @param page must not be {@literal null}. + */ S page(Pageable page); } diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java index 320684b..5066cb2 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java @@ -30,6 +30,8 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -47,12 +49,17 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.springframework.dao.DataAccessException; import org.springframework.dao.UncategorizedDataAccessException; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.NullHandling; +import org.springframework.data.domain.Sort.Order; import org.springframework.data.jdbc.core.function.connectionfactory.ConnectionProxy; import org.springframework.data.util.Pair; import org.springframework.jdbc.core.SqlProvider; import org.springframework.jdbc.support.SQLExceptionTranslator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Default implementation of {@link DatabaseClient}. @@ -93,7 +100,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override public SelectFromSpec select() { - throw new UnsupportedOperationException("Implement me"); + return new DefaultSelectFromSpec(); } @Override @@ -254,7 +261,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation. + * Base class for {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} + * implementations. */ @RequiredArgsConstructor private class GenericExecuteSpecSupport { @@ -481,6 +489,295 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } } + /** + * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SelectFromSpec} implementation. + */ + class DefaultSelectFromSpec implements SelectFromSpec { + + @Override + public GenericSelectSpec from(String table) { + return new DefaultGenericSelectSpec(table); + } + + @Override + public TypedSelectSpec from(Class table) { + return new DefaultTypedSelectSpec<>(table); + } + } + + /** + * Base class for {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} + * implementations. + */ + @RequiredArgsConstructor + private abstract class DefaultSelectSpecSupport { + + final String table; + final List projectedFields; + final Sort sort; + final Pageable page; + + DefaultSelectSpecSupport(String table) { + + Assert.hasText(table, "Table name must not be null!"); + + this.table = table; + this.projectedFields = Collections.emptyList(); + this.sort = Sort.unsorted(); + this.page = Pageable.unpaged(); + } + + public DefaultSelectSpecSupport project(String... selectedFields) { + Assert.notNull(selectedFields, "Projection fields must not be null!"); + + List projectedFields = new ArrayList<>(this.projectedFields.size() + selectedFields.length); + projectedFields.addAll(this.projectedFields); + projectedFields.addAll(Arrays.asList(selectedFields)); + + return createInstance(table, projectedFields, sort, page); + } + + public DefaultSelectSpecSupport orderBy(Sort sort) { + + Assert.notNull(sort, "Sort must not be null!"); + + return createInstance(table, projectedFields, sort, page); + } + + public DefaultSelectSpecSupport page(Pageable page) { + + Assert.notNull(page, "Pageable must not be null!"); + + return createInstance(table, projectedFields, sort, page); + } + + StringBuilder getLimitOffset(Pageable pageable) { + return new StringBuilder().append("LIMIT").append(' ').append(page.getPageSize()) // + .append(' ').append("OFFSET").append(' ').append(page.getOffset()); + } + + StringBuilder getSortClause(Sort sort) { + + StringBuilder sortClause = new StringBuilder(); + + for (Order order : sort) { + + if (sortClause.length() != 0) { + sortClause.append(',').append(' '); + } + + sortClause.append(order.getProperty()).append(' ').append(order.getDirection().isAscending() ? "ASC" : "DESC"); + + if (order.getNullHandling() == NullHandling.NULLS_FIRST) { + sortClause.append(' ').append("NULLS FIRST"); + } + + if (order.getNullHandling() == NullHandling.NULLS_LAST) { + sortClause.append(' ').append("NULLS LAST"); + } + } + return sortClause; + } + + SqlResult execute(String sql, BiFunction mappingFunction) { + + Function selectFunction = it -> { + + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL statement [" + sql + "]"); + } + + return it.createStatement(sql); + }; + + Function> resultFunction = it -> Flux.from(selectFunction.apply(it).execute()); + + return new DefaultSqlResult<>(DefaultDatabaseClient.this, // + sql, // + resultFunction, // + it -> Mono.error(new UnsupportedOperationException("Not available for SELECT")), // + mappingFunction); + } + + protected abstract DefaultSelectSpecSupport createInstance(String table, List projectedFields, Sort sort, + Pageable page); + } + + private class DefaultGenericSelectSpec extends DefaultSelectSpecSupport implements GenericSelectSpec { + + public DefaultGenericSelectSpec(String table, List projectedFields, Sort sort, Pageable page) { + super(table, projectedFields, sort, page); + } + + DefaultGenericSelectSpec(String table) { + super(table); + } + + @Override + public TypedSelectSpec as(Class resultType) { + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, resultType, + dataAccessStrategy.getRowMapper(resultType)); + } + + @Override + public DefaultGenericSelectSpec project(String... selectedFields) { + return (DefaultGenericSelectSpec) super.project(selectedFields); + } + + @Override + public DefaultGenericSelectSpec orderBy(Sort sort) { + return (DefaultGenericSelectSpec) super.orderBy(sort); + } + + @Override + public DefaultGenericSelectSpec page(Pageable page) { + return (DefaultGenericSelectSpec) super.page(page); + } + + @Override + public FetchSpec> fetch() { + return exchange(ColumnMapRowMapper.INSTANCE); + } + + @Override + public Mono>> exchange() { + return Mono.just(exchange(ColumnMapRowMapper.INSTANCE)); + } + + private SqlResult exchange(BiFunction mappingFunction) { + + List projectedFields; + + if (this.projectedFields.isEmpty()) { + projectedFields = Collections.singletonList("*"); + } else { + projectedFields = this.projectedFields; + } + + StringBuilder selectBuilder = new StringBuilder(); + selectBuilder.append("SELECT").append(' ') // + .append(StringUtils.collectionToDelimitedString(projectedFields, ", ")).append(' ') // + .append("FROM").append(' ').append(table); + + if (sort.isSorted()) { + selectBuilder.append(' ').append("ORDER BY").append(' ').append(getSortClause(sort)); + } + + if (page.isPaged()) { + selectBuilder.append(' ').append(getLimitOffset(page)); + } + + return execute(selectBuilder.toString(), mappingFunction); + } + + @Override + protected DefaultGenericSelectSpec createInstance(String table, List projectedFields, Sort sort, + Pageable page) { + return new DefaultGenericSelectSpec(table, projectedFields, sort, page); + } + } + + /** + * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.TypedInsertSpec}. + */ + @SuppressWarnings("unchecked") + private class DefaultTypedSelectSpec extends DefaultSelectSpecSupport implements TypedSelectSpec { + + private final Class typeToRead; + private final BiFunction mappingFunction; + + DefaultTypedSelectSpec(Class typeToRead) { + + super(dataAccessStrategy.getTableName(typeToRead)); + + this.typeToRead = typeToRead; + this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead); + } + + DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, Class typeToRead, + BiFunction mappingFunction) { + super(table, projectedFields, sort, page); + this.typeToRead = typeToRead; + this.mappingFunction = mappingFunction; + } + + @Override + public TypedSelectSpec as(Class resultType) { + + Assert.notNull(resultType, "Result type must not be null!"); + + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, + dataAccessStrategy.getRowMapper(resultType)); + } + + @Override + public TypedSelectSpec extract(BiFunction mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null!"); + + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, mappingFunction); + } + + @Override + public DefaultTypedSelectSpec project(String... selectedFields) { + return (DefaultTypedSelectSpec) super.project(selectedFields); + } + + @Override + public DefaultTypedSelectSpec orderBy(Sort sort) { + return (DefaultTypedSelectSpec) super.orderBy(sort); + } + + @Override + public DefaultTypedSelectSpec page(Pageable page) { + return (DefaultTypedSelectSpec) super.page(page); + } + + @Override + public FetchSpec fetch() { + return exchange(mappingFunction); + } + + @Override + public Mono> exchange() { + return Mono.just(exchange(mappingFunction)); + } + + private SqlResult exchange(BiFunction mappingFunction) { + + List projectedFields; + + if (this.projectedFields.isEmpty()) { + projectedFields = dataAccessStrategy.getAllFields(typeToRead); + } else { + projectedFields = this.projectedFields; + } + + StringBuilder selectBuilder = new StringBuilder(); + selectBuilder.append("SELECT").append(' ') // + .append(StringUtils.collectionToDelimitedString(projectedFields, ", ")).append(' ') // + .append("FROM").append(' ').append(table); + + if (sort.isSorted()) { + + Sort mappedSort = dataAccessStrategy.getMappedSort(typeToRead, sort); + selectBuilder.append(' ').append("ORDER BY").append(' ').append(getSortClause(mappedSort)); + } + + if (page.isPaged()) { + selectBuilder.append(' ').append(getLimitOffset(page)); + } + + return execute(selectBuilder.toString(), mappingFunction); + } + + @Override + protected DefaultTypedSelectSpec createInstance(String table, List projectedFields, Sort sort, + Pageable page) { + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, mappingFunction); + } + } + /** * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.InsertIntoSpec} implementation. */ diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java index 38d101e..e30f042 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java @@ -15,21 +15,26 @@ */ package org.springframework.data.jdbc.core.function; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.BiFunction; +import java.util.stream.Collectors; import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.util.Pair; +import org.springframework.data.util.StreamUtils; import org.springframework.util.ClassUtils; -import io.r2dbc.spi.Row; -import io.r2dbc.spi.RowMetadata; - /** * @author Mark Paluch */ @@ -38,6 +43,20 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private final EntityInstantiators instantiators = new EntityInstantiators(); private final JdbcMappingContext mappingContext = new JdbcMappingContext(); + @Override + public List getAllFields(Class typeToRead) { + + JdbcPersistentEntity persistentEntity = mappingContext.getPersistentEntity(typeToRead); + + if (persistentEntity == null) { + return Collections.singletonList("*"); + } + + return StreamUtils.createStreamFromIterator(persistentEntity.iterator()) // + .map(JdbcPersistentProperty::getColumnName) // + .collect(Collectors.toList()); + } + @Override public List> getInsert(Object object) { @@ -62,6 +81,30 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return values; } + @Override + public Sort getMappedSort(Class typeToRead, Sort sort) { + + JdbcPersistentEntity entity = mappingContext.getPersistentEntity(typeToRead); + if (entity == null) { + return sort; + } + + List mappedOrder = new ArrayList<>(); + + for (Order order : sort) { + + JdbcPersistentProperty persistentProperty = entity.getPersistentProperty(order.getProperty()); + if (persistentProperty == null) { + mappedOrder.add(order); + } else { + mappedOrder + .add(Order.by(persistentProperty.getColumnName()).with(order.getNullHandling()).with(order.getDirection())); + } + } + + return Sort.by(mappedOrder); + } + @Override public BiFunction getRowMapper(Class typeToRead) { return new EntityRowMapper((JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(typeToRead), diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java index 26c9f65..d2c996c 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java @@ -15,21 +15,26 @@ */ package org.springframework.data.jdbc.core.function; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + import java.util.List; import java.util.function.BiFunction; +import org.springframework.data.domain.Sort; import org.springframework.data.util.Pair; -import io.r2dbc.spi.Row; -import io.r2dbc.spi.RowMetadata; - /** * @author Mark Paluch */ public interface ReactiveDataAccessStrategy { + List getAllFields(Class typeToRead); + List> getInsert(Object object); + Sort getMappedSort(Class typeToRead, Sort sort); + // TODO: Broaden T to Mono/Flux for reactive relational data access? BiFunction getRowMapper(Class typeToRead); diff --git a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java index ab8a49d..9acba55 100644 --- a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core.function; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Sort.Order.*; import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; import io.r2dbc.postgresql.PostgresqlConnectionFactory; @@ -28,6 +29,8 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.function.ExternalDatabase.ProvidedDatabase; import org.springframework.data.jdbc.core.mapping.Table; import org.springframework.jdbc.core.JdbcTemplate; @@ -162,6 +165,80 @@ public class DatabaseClientIntegrationTests { assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } + @Test + public void select() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from(LegoSet.class) // + .project("id", "name", "manual") // + .orderBy(Sort.by("id")) // + .fetch().all() // + .as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.getId()).isEqualTo(42055); + assertThat(actual.getName()).isEqualTo("SCHAUFELRADBAGGER"); + assertThat(actual.getManual()).isEqualTo(12); + }).verifyComplete(); + } + + @Test + public void selectOrderByIdDesc() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42068, 'FLUGHAFEN-LÖSCHFAHRZEUG', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from(LegoSet.class) // + .orderBy(Sort.by(desc("id"))) // + .fetch().all() // + .map(LegoSet::getId) // + .as(StepVerifier::create) // + .expectNext(42068, 42064, 42055) // + .verifyComplete(); + } + + @Test + public void selectOrderPaged() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42068, 'FLUGHAFEN-LÖSCHFAHRZEUG', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from(LegoSet.class) // + .orderBy(Sort.by(desc("id"))) // + .page(PageRequest.of(1, 1)).fetch().all() // + .map(LegoSet::getId) // + .as(StepVerifier::create) // + .expectNext(42064) // + .verifyComplete(); + } + + @Test + public void selectTypedLater() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42068, 'FLUGHAFEN-LÖSCHFAHRZEUG', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from("legoset") // + .orderBy(Sort.by(desc("id"))) // + .as(LegoSet.class) // + .fetch().all() // + .map(LegoSet::getId) // + .as(StepVerifier::create) // + .expectNext(42068, 42064, 42055) // + .verifyComplete(); + } + @Data @Table("legoset") static class LegoSet {