#162 - Refine declaration of nullable values in DatabaseClient.

DatabaseClient.BindSpec.bind(…) (execute) and DatabaseClient.GenericInsertSpec.value(…) (insert) now consistently accept SettableValue for scalar and absent values.

This change allows us to provide a Kotlin extension leveraging reified generics to provide the type of a value even if it is null to construct an appropriate SettableValue for fluent API usage.
This commit is contained in:
Mark Paluch
2019-09-03 16:23:48 +02:00
parent bac5c343b7
commit 30c5e2f5e0
6 changed files with 229 additions and 68 deletions

View File

@@ -480,10 +480,12 @@ public interface DatabaseClient {
interface GenericInsertSpec<T> extends InsertSpec<T> {
/**
* Specify a field and non-{@literal null} value to insert.
* Specify a field and non-{@literal null} value to insert. {@code value} can be either a scalar value or
* {@link SettableValue}.
*
* @param field must not be {@literal null} or empty.
* @param value must not be {@literal null}
* @param value the field value to set, must not be {@literal null}. Can be either a scalar value or
* {@link SettableValue}.
*/
GenericInsertSpec<T> value(String field, Object value);
@@ -492,19 +494,10 @@ public interface DatabaseClient {
*
* @param field must not be {@literal null} or empty.
* @param type must not be {@literal null}.
* @deprecated will be removed soon. Use {@link #nullValue(String)}.
*/
@Deprecated
default GenericInsertSpec<T> nullValue(String field, Class<?> type) {
return value(field, SettableValue.empty(type));
}
/**
* Specify a {@literal null} value to insert.
*
* @param field must not be {@literal null} or empty.
*/
GenericInsertSpec<T> nullValue(String field);
}
/**
@@ -704,10 +697,11 @@ public interface DatabaseClient {
interface BindSpec<S extends BindSpec<S>> {
/**
* Bind a non-{@literal null} value to a parameter identified by its {@code index}.
* Bind a non-{@literal null} value to a parameter identified by its {@code index}. {@code value} can be either a
* scalar value or {@link SettableValue}.
*
* @param index zero based index to bind the parameter to.
* @param value to bind. Must not be {@literal null}.
* @param value must not be {@literal null}. Can be either a scalar value or {@link SettableValue}.
*/
S bind(int index, Object value);
@@ -720,10 +714,11 @@ public interface DatabaseClient {
S bindNull(int index, Class<?> type);
/**
* Bind a non-{@literal null} value to a parameter identified by its {@code name}.
* Bind a non-{@literal null} value to a parameter identified by its {@code name}. {@code value} can be either a
* scalar value or {@link SettableValue}.
*
* @param name must not be {@literal null} or empty.
* @param value must not be {@literal null}.
* @param value must not be {@literal null}. Can be either a scalar value or {@link SettableValue}.
*/
S bind(String name, Object value);
@@ -734,12 +729,5 @@ public interface DatabaseClient {
* @param type must not be {@literal null}.
*/
S bindNull(String name, Class<?> type);
/**
* Bind a bean according to Java {@link java.beans.BeanInfo Beans} using property names.
*
* @param bean must not be {@literal null}.
*/
S bind(Object bean);
}
}

View File

@@ -15,6 +15,16 @@
*/
package org.springframework.data.r2dbc.core;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.R2dbcException;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
import io.r2dbc.spi.Statement;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -31,18 +41,9 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.R2dbcException;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
import io.r2dbc.spi.Statement;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
@@ -383,7 +384,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Assert.notNull(value, () -> String.format("Value at index %d must not be null. Use bindNull(…) instead.", index));
Map<Integer, SettableValue> byIndex = new LinkedHashMap<>(this.byIndex);
byIndex.put(index, SettableValue.fromOrEmpty(value, value.getClass()));
if (value instanceof SettableValue) {
byIndex.put(index, (SettableValue) value);
} else {
byIndex.put(index, SettableValue.fromOrEmpty(value, value.getClass()));
}
return createInstance(byIndex, this.byName, this.sqlSupplier);
}
@@ -407,7 +413,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
() -> String.format("Value for parameter %s must not be null. Use bindNull(…) instead.", name));
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
byName.put(name, SettableValue.fromOrEmpty(value, value.getClass()));
if (value instanceof SettableValue) {
byName.put(name, (SettableValue) value);
} else {
byName.put(name, SettableValue.fromOrEmpty(value, value.getClass()));
}
return createInstance(this.byIndex, byName, this.sqlSupplier);
}
@@ -433,13 +444,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Supplier<String> sqlSupplier) {
return new ExecuteSpecSupport(byIndex, byName, sqlSupplier);
}
public ExecuteSpecSupport bind(Object bean) {
Assert.notNull(bean, "Bean must not be null!");
throw new UnsupportedOperationException("Implement me!");
}
}
/**
@@ -510,11 +514,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return (DefaultGenericExecuteSpec) super.bindNull(name, type);
}
@Override
public DefaultGenericExecuteSpec bind(Object bean) {
return (DefaultGenericExecuteSpec) super.bind(bean);
}
@Override
protected ExecuteSpecSupport createInstance(Map<Integer, SettableValue> byIndex, Map<String, SettableValue> byName,
Supplier<String> sqlSupplier) {
@@ -603,11 +602,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return (DefaultTypedExecuteSpec<T>) super.bindNull(name, type);
}
@Override
public DefaultTypedExecuteSpec<T> bind(Object bean) {
return (DefaultTypedExecuteSpec<T>) super.bind(bean);
}
@Override
protected DefaultTypedExecuteSpec<T> createInstance(Map<Integer, SettableValue> byIndex,
Map<String, SettableValue> byName, Supplier<String> sqlSupplier) {
@@ -933,8 +927,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
public GenericInsertSpec<T> value(String field, Object value) {
Assert.notNull(field, "Field must not be null!");
Assert.notNull(value,
() -> String.format("Value for field %s must not be null. Use nullValue(…) instead.", field));
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
@@ -947,17 +939,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
}
@Override
public GenericInsertSpec<T> nullValue(String field) {
Assert.notNull(field, "Field must not be null!");
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
byName.put(field, null);
return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
}
@Override
public <R> FetchSpec<R> map(Function<Row, R> mappingFunction) {

View File

@@ -16,6 +16,7 @@
package org.springframework.data.r2dbc.core
import kotlinx.coroutines.reactive.awaitFirstOrNull
import org.springframework.data.r2dbc.mapping.SettableValue
/**
* Coroutines variant of [DatabaseClient.GenericExecuteSpec.then].
@@ -27,7 +28,23 @@ suspend fun DatabaseClient.GenericExecuteSpec.await() {
}
/**
* Extension for [DatabaseClient.GenericExecuteSpec.as] providing a
* Extension for [DatabaseClient.BindSpec.bind] providing a variant leveraging reified type parameters
*
* @author Mark Paluch
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
inline fun <reified T : Any> DatabaseClient.BindSpec<*>.bind(index: Int, value: T?) = bind(index, SettableValue.fromOrEmpty(value, T::class.java))
/**
* Extension for [DatabaseClient.BindSpec.bind] providing a variant leveraging reified type parameters
*
* @author Mark Paluch
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
inline fun <reified T : Any> DatabaseClient.BindSpec<*>.bind(name: String, value: T?) = bind(name, SettableValue.fromOrEmpty(value, T::class.java))
/**
* Extension for [DatabaseClient.GenericExecuteSpec. as] providing a
* `asType<Foo>()` variant.
*
* @author Sebastien Deleuze
@@ -80,6 +97,15 @@ suspend fun <T> DatabaseClient.InsertSpec<T>.await() {
inline fun <reified T : Any> DatabaseClient.InsertIntoSpec.into(): DatabaseClient.TypedInsertSpec<T> =
into(T::class.java)
/**
* Extension for [DatabaseClient.GenericInsertSpec.value] providing a variant leveraging reified type parameters
*
* @author Mark Paluch
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
inline fun <reified T : Any> DatabaseClient.GenericInsertSpec<*>.value(name: String, value: T?) = value(name, SettableValue.fromOrEmpty(value, T::class.java))
/**
* Extension for [DatabaseClient.SelectFromSpec.from] providing a
* `from<Foo>()` variant.

View File

@@ -172,7 +172,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr
databaseClient.insert().into("legoset")//
.value("id", 42055) //
.value("name", "SCHAUFELRADBAGGER") //
.nullValue("manual") //
.nullValue("manual", Integer.class) //
.fetch() //
.rowsUpdated() //
.as(StepVerifier::create) //
@@ -190,7 +190,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr
databaseClient.insert().into("legoset")//
.value("id", 42055) //
.value("name", "SCHAUFELRADBAGGER") //
.nullValue("manual") //
.nullValue("manual", Integer.class) //
.then() //
.as(StepVerifier::create) //
.verifyComplete();

View File

@@ -34,6 +34,7 @@ import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.data.r2dbc.mapping.SettableValue;
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
/**
@@ -118,6 +119,34 @@ public class DefaultDatabaseClientUnitTests {
verify(statement).bindNull("$1", String.class);
}
@Test // gh-162
public void executeShouldBindSettableValues() {
Statement statement = mock(Statement.class);
when(connection.createStatement("SELECT * FROM table WHERE key = $1")).thenReturn(statement);
when(statement.execute()).thenReturn(Mono.empty());
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) DatabaseClient.builder()
.connectionFactory(connectionFactory)
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)).build();
databaseClient.execute("SELECT * FROM table WHERE key = $1") //
.bind(0, SettableValue.empty(String.class)) //
.then() //
.as(StepVerifier::create) //
.verifyComplete();
verify(statement).bindNull(0, String.class);
databaseClient.execute("SELECT * FROM table WHERE key = $1") //
.bind("$1", SettableValue.empty(String.class)) //
.then() //
.as(StepVerifier::create) //
.verifyComplete();
verify(statement).bindNull("$1", String.class);
}
@Test // gh-128
public void executeShouldBindNamedNullValues() {
@@ -138,7 +167,7 @@ public class DefaultDatabaseClientUnitTests {
verify(statement).bindNull(0, String.class);
}
@Test // gh-128
@Test // gh-128, gh-162
public void executeShouldBindValues() {
Statement statement = mock(Statement.class);
@@ -150,7 +179,7 @@ public class DefaultDatabaseClientUnitTests {
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)).build();
databaseClient.execute("SELECT * FROM table WHERE key = $1") //
.bind(0, "foo") //
.bind(0, SettableValue.from("foo")) //
.then() //
.as(StepVerifier::create) //
.verifyComplete();
@@ -166,6 +195,52 @@ public class DefaultDatabaseClientUnitTests {
verify(statement).bind("$1", "foo");
}
@Test // gh-162
public void insertShouldAcceptNullValues() {
Statement statement = mock(Statement.class);
when(connection.createStatement("INSERT INTO foo (first, second) VALUES ($1, $2)")).thenReturn(statement);
when(statement.returnGeneratedValues()).thenReturn(statement);
when(statement.execute()).thenReturn(Mono.empty());
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) DatabaseClient.builder()
.connectionFactory(connectionFactory)
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)).build();
databaseClient.insert().into("foo") //
.value("first", "foo") //
.nullValue("second", Integer.class) //
.then() //
.as(StepVerifier::create) //
.verifyComplete();
verify(statement).bind(0, "foo");
verify(statement).bindNull(1, Integer.class);
}
@Test // gh-162
public void insertShouldAcceptSettableValue() {
Statement statement = mock(Statement.class);
when(connection.createStatement("INSERT INTO foo (first, second) VALUES ($1, $2)")).thenReturn(statement);
when(statement.returnGeneratedValues()).thenReturn(statement);
when(statement.execute()).thenReturn(Mono.empty());
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) DatabaseClient.builder()
.connectionFactory(connectionFactory)
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)).build();
databaseClient.insert().into("foo") //
.value("first", SettableValue.from("foo")) //
.value("second", SettableValue.empty(Integer.class)) //
.then() //
.as(StepVerifier::create) //
.verifyComplete();
verify(statement).bind(0, "foo");
verify(statement).bindNull(1, Integer.class);
}
@Test // gh-128
public void executeShouldBindNamedValuesByIndex() {

View File

@@ -21,6 +21,7 @@ import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.springframework.data.r2dbc.mapping.SettableValue
import reactor.core.publisher.Mono
/**
@@ -32,6 +33,66 @@ import reactor.core.publisher.Mono
*/
class DatabaseClientExtensionsTests {
@Test // gh-162
fun bindByIndexShouldBindValue() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.bind(eq(0), any()) } returns spec
runBlocking {
spec.bind<String>(0, "foo")
}
verify {
spec.bind(0, SettableValue.fromOrEmpty("foo", String::class.java))
}
}
@Test // gh-162
fun bindByIndexShouldBindNull() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.bind(eq(0), any()) } returns spec
runBlocking {
spec.bind<String>(0, null)
}
verify {
spec.bind(0, SettableValue.empty(String::class.java))
}
}
@Test // gh-162
fun bindByNameShouldBindValue() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.bind(eq("field"), any()) } returns spec
runBlocking {
spec.bind<String>("field", "foo")
}
verify {
spec.bind("field", SettableValue.fromOrEmpty("foo", String::class.java))
}
}
@Test // gh-162
fun bindByNameShouldBindNull() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.bind(eq("field"), any()) } returns spec
runBlocking {
spec.bind<String>("field", null)
}
verify {
spec.bind("field", SettableValue.empty(String::class.java))
}
}
@Test // gh-63
fun genericExecuteSpecAwait() {
@@ -140,6 +201,36 @@ class DatabaseClientExtensionsTests {
}
}
@Test // gh-162
fun insertValueShouldBindValue() {
val spec = mockk<DatabaseClient.GenericInsertSpec<Any>>()
every { spec.value(eq("field"), any()) } returns spec
runBlocking {
spec.value<String>("field", "foo")
}
verify {
spec.value("field", SettableValue.fromOrEmpty("foo", String::class.java))
}
}
@Test // gh-162
fun insertValueShouldBindNull() {
val spec = mockk<DatabaseClient.GenericInsertSpec<Any>>()
every { spec.value(eq("field"), any()) } returns spec
runBlocking {
spec.value<String>("field", null)
}
verify {
spec.value("field", SettableValue.empty(String::class.java))
}
}
@Test // gh-122
fun selectFromSpecFrom() {