Skip to content

Commit 30c5e2f

Browse files
committed
#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.
1 parent bac5c34 commit 30c5e2f

File tree

6 files changed

+229
-68
lines changed

6 files changed

+229
-68
lines changed

src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,12 @@ default S orderBy(Sort.Order... orders) {
480480
interface GenericInsertSpec<T> extends InsertSpec<T> {
481481

482482
/**
483-
* Specify a field and non-{@literal null} value to insert.
483+
* Specify a field and non-{@literal null} value to insert. {@code value} can be either a scalar value or
484+
* {@link SettableValue}.
484485
*
485486
* @param field must not be {@literal null} or empty.
486-
* @param value must not be {@literal null}
487+
* @param value the field value to set, must not be {@literal null}. Can be either a scalar value or
488+
* {@link SettableValue}.
487489
*/
488490
GenericInsertSpec<T> value(String field, Object value);
489491

@@ -492,19 +494,10 @@ interface GenericInsertSpec<T> extends InsertSpec<T> {
492494
*
493495
* @param field must not be {@literal null} or empty.
494496
* @param type must not be {@literal null}.
495-
* @deprecated will be removed soon. Use {@link #nullValue(String)}.
496497
*/
497-
@Deprecated
498498
default GenericInsertSpec<T> nullValue(String field, Class<?> type) {
499499
return value(field, SettableValue.empty(type));
500500
}
501-
502-
/**
503-
* Specify a {@literal null} value to insert.
504-
*
505-
* @param field must not be {@literal null} or empty.
506-
*/
507-
GenericInsertSpec<T> nullValue(String field);
508501
}
509502

510503
/**
@@ -704,10 +697,11 @@ interface DeleteSpec {
704697
interface BindSpec<S extends BindSpec<S>> {
705698

706699
/**
707-
* Bind a non-{@literal null} value to a parameter identified by its {@code index}.
700+
* Bind a non-{@literal null} value to a parameter identified by its {@code index}. {@code value} can be either a
701+
* scalar value or {@link SettableValue}.
708702
*
709703
* @param index zero based index to bind the parameter to.
710-
* @param value to bind. Must not be {@literal null}.
704+
* @param value must not be {@literal null}. Can be either a scalar value or {@link SettableValue}.
711705
*/
712706
S bind(int index, Object value);
713707

@@ -720,10 +714,11 @@ interface BindSpec<S extends BindSpec<S>> {
720714
S bindNull(int index, Class<?> type);
721715

722716
/**
723-
* Bind a non-{@literal null} value to a parameter identified by its {@code name}.
717+
* Bind a non-{@literal null} value to a parameter identified by its {@code name}. {@code value} can be either a
718+
* scalar value or {@link SettableValue}.
724719
*
725720
* @param name must not be {@literal null} or empty.
726-
* @param value must not be {@literal null}.
721+
* @param value must not be {@literal null}. Can be either a scalar value or {@link SettableValue}.
727722
*/
728723
S bind(String name, Object value);
729724

@@ -734,12 +729,5 @@ interface BindSpec<S extends BindSpec<S>> {
734729
* @param type must not be {@literal null}.
735730
*/
736731
S bindNull(String name, Class<?> type);
737-
738-
/**
739-
* Bind a bean according to Java {@link java.beans.BeanInfo Beans} using property names.
740-
*
741-
* @param bean must not be {@literal null}.
742-
*/
743-
S bind(Object bean);
744732
}
745733
}

src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515
*/
1616
package org.springframework.data.r2dbc.core;
1717

18+
import io.r2dbc.spi.Connection;
19+
import io.r2dbc.spi.ConnectionFactory;
20+
import io.r2dbc.spi.R2dbcException;
21+
import io.r2dbc.spi.Result;
22+
import io.r2dbc.spi.Row;
23+
import io.r2dbc.spi.RowMetadata;
24+
import io.r2dbc.spi.Statement;
25+
import reactor.core.publisher.Flux;
26+
import reactor.core.publisher.Mono;
27+
1828
import java.lang.reflect.InvocationHandler;
1929
import java.lang.reflect.InvocationTargetException;
2030
import java.lang.reflect.Method;
@@ -31,18 +41,9 @@
3141
import java.util.function.Supplier;
3242
import java.util.stream.Collectors;
3343

34-
import io.r2dbc.spi.Connection;
35-
import io.r2dbc.spi.ConnectionFactory;
36-
import io.r2dbc.spi.R2dbcException;
37-
import io.r2dbc.spi.Result;
38-
import io.r2dbc.spi.Row;
39-
import io.r2dbc.spi.RowMetadata;
40-
import io.r2dbc.spi.Statement;
4144
import org.apache.commons.logging.Log;
4245
import org.apache.commons.logging.LogFactory;
4346
import org.reactivestreams.Publisher;
44-
import reactor.core.publisher.Flux;
45-
import reactor.core.publisher.Mono;
4647

4748
import org.springframework.dao.DataAccessException;
4849
import org.springframework.dao.InvalidDataAccessApiUsageException;
@@ -383,7 +384,12 @@ public ExecuteSpecSupport bind(int index, Object value) {
383384
Assert.notNull(value, () -> String.format("Value at index %d must not be null. Use bindNull(…) instead.", index));
384385

385386
Map<Integer, SettableValue> byIndex = new LinkedHashMap<>(this.byIndex);
386-
byIndex.put(index, SettableValue.fromOrEmpty(value, value.getClass()));
387+
388+
if (value instanceof SettableValue) {
389+
byIndex.put(index, (SettableValue) value);
390+
} else {
391+
byIndex.put(index, SettableValue.fromOrEmpty(value, value.getClass()));
392+
}
387393

388394
return createInstance(byIndex, this.byName, this.sqlSupplier);
389395
}
@@ -407,7 +413,12 @@ public ExecuteSpecSupport bind(String name, Object value) {
407413
() -> String.format("Value for parameter %s must not be null. Use bindNull(…) instead.", name));
408414

409415
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
410-
byName.put(name, SettableValue.fromOrEmpty(value, value.getClass()));
416+
417+
if (value instanceof SettableValue) {
418+
byName.put(name, (SettableValue) value);
419+
} else {
420+
byName.put(name, SettableValue.fromOrEmpty(value, value.getClass()));
421+
}
411422

412423
return createInstance(this.byIndex, byName, this.sqlSupplier);
413424
}
@@ -433,13 +444,6 @@ protected ExecuteSpecSupport createInstance(Map<Integer, SettableValue> byIndex,
433444
Supplier<String> sqlSupplier) {
434445
return new ExecuteSpecSupport(byIndex, byName, sqlSupplier);
435446
}
436-
437-
public ExecuteSpecSupport bind(Object bean) {
438-
439-
Assert.notNull(bean, "Bean must not be null!");
440-
441-
throw new UnsupportedOperationException("Implement me!");
442-
}
443447
}
444448

445449
/**
@@ -510,11 +514,6 @@ public DefaultGenericExecuteSpec bindNull(String name, Class<?> type) {
510514
return (DefaultGenericExecuteSpec) super.bindNull(name, type);
511515
}
512516

513-
@Override
514-
public DefaultGenericExecuteSpec bind(Object bean) {
515-
return (DefaultGenericExecuteSpec) super.bind(bean);
516-
}
517-
518517
@Override
519518
protected ExecuteSpecSupport createInstance(Map<Integer, SettableValue> byIndex, Map<String, SettableValue> byName,
520519
Supplier<String> sqlSupplier) {
@@ -603,11 +602,6 @@ public DefaultTypedExecuteSpec<T> bindNull(String name, Class<?> type) {
603602
return (DefaultTypedExecuteSpec<T>) super.bindNull(name, type);
604603
}
605604

606-
@Override
607-
public DefaultTypedExecuteSpec<T> bind(Object bean) {
608-
return (DefaultTypedExecuteSpec<T>) super.bind(bean);
609-
}
610-
611605
@Override
612606
protected DefaultTypedExecuteSpec<T> createInstance(Map<Integer, SettableValue> byIndex,
613607
Map<String, SettableValue> byName, Supplier<String> sqlSupplier) {
@@ -933,8 +927,6 @@ class DefaultGenericInsertSpec<T> implements GenericInsertSpec<T> {
933927
public GenericInsertSpec<T> value(String field, Object value) {
934928

935929
Assert.notNull(field, "Field must not be null!");
936-
Assert.notNull(value,
937-
() -> String.format("Value for field %s must not be null. Use nullValue(…) instead.", field));
938930

939931
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
940932

@@ -947,17 +939,6 @@ public GenericInsertSpec<T> value(String field, Object value) {
947939
return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
948940
}
949941

950-
@Override
951-
public GenericInsertSpec<T> nullValue(String field) {
952-
953-
Assert.notNull(field, "Field must not be null!");
954-
955-
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
956-
byName.put(field, null);
957-
958-
return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
959-
}
960-
961942
@Override
962943
public <R> FetchSpec<R> map(Function<Row, R> mappingFunction) {
963944

src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.r2dbc.core
1717

1818
import kotlinx.coroutines.reactive.awaitFirstOrNull
19+
import org.springframework.data.r2dbc.mapping.SettableValue
1920

2021
/**
2122
* Coroutines variant of [DatabaseClient.GenericExecuteSpec.then].
@@ -27,7 +28,23 @@ suspend fun DatabaseClient.GenericExecuteSpec.await() {
2728
}
2829

2930
/**
30-
* Extension for [DatabaseClient.GenericExecuteSpec.as] providing a
31+
* Extension for [DatabaseClient.BindSpec.bind] providing a variant leveraging reified type parameters
32+
*
33+
* @author Mark Paluch
34+
*/
35+
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
36+
inline fun <reified T : Any> DatabaseClient.BindSpec<*>.bind(index: Int, value: T?) = bind(index, SettableValue.fromOrEmpty(value, T::class.java))
37+
38+
/**
39+
* Extension for [DatabaseClient.BindSpec.bind] providing a variant leveraging reified type parameters
40+
*
41+
* @author Mark Paluch
42+
*/
43+
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
44+
inline fun <reified T : Any> DatabaseClient.BindSpec<*>.bind(name: String, value: T?) = bind(name, SettableValue.fromOrEmpty(value, T::class.java))
45+
46+
/**
47+
* Extension for [DatabaseClient.GenericExecuteSpec. as] providing a
3148
* `asType<Foo>()` variant.
3249
*
3350
* @author Sebastien Deleuze
@@ -80,6 +97,15 @@ suspend fun <T> DatabaseClient.InsertSpec<T>.await() {
8097
inline fun <reified T : Any> DatabaseClient.InsertIntoSpec.into(): DatabaseClient.TypedInsertSpec<T> =
8198
into(T::class.java)
8299

100+
/**
101+
* Extension for [DatabaseClient.GenericInsertSpec.value] providing a variant leveraging reified type parameters
102+
*
103+
* @author Mark Paluch
104+
*/
105+
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
106+
inline fun <reified T : Any> DatabaseClient.GenericInsertSpec<*>.value(name: String, value: T?) = value(name, SettableValue.fromOrEmpty(value, T::class.java))
107+
108+
83109
/**
84110
* Extension for [DatabaseClient.SelectFromSpec.from] providing a
85111
* `from<Foo>()` variant.

src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public void insert() {
172172
databaseClient.insert().into("legoset")//
173173
.value("id", 42055) //
174174
.value("name", "SCHAUFELRADBAGGER") //
175-
.nullValue("manual") //
175+
.nullValue("manual", Integer.class) //
176176
.fetch() //
177177
.rowsUpdated() //
178178
.as(StepVerifier::create) //
@@ -190,7 +190,7 @@ public void insertWithoutResult() {
190190
databaseClient.insert().into("legoset")//
191191
.value("id", 42055) //
192192
.value("name", "SCHAUFELRADBAGGER") //
193-
.nullValue("manual") //
193+
.nullValue("manual", Integer.class) //
194194
.then() //
195195
.as(StepVerifier::create) //
196196
.verifyComplete();

src/test/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientUnitTests.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.reactivestreams.Subscription;
3535

3636
import org.springframework.data.r2dbc.dialect.PostgresDialect;
37+
import org.springframework.data.r2dbc.mapping.SettableValue;
3738
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
3839

3940
/**
@@ -118,6 +119,34 @@ public void executeShouldBindNullValues() {
118119
verify(statement).bindNull("$1", String.class);
119120
}
120121

122+
@Test // gh-162
123+
public void executeShouldBindSettableValues() {
124+
125+
Statement statement = mock(Statement.class);
126+
when(connection.createStatement("SELECT * FROM table WHERE key = $1")).thenReturn(statement);
127+
when(statement.execute()).thenReturn(Mono.empty());
128+
129+
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) DatabaseClient.builder()
130+
.connectionFactory(connectionFactory)
131+
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)).build();
132+
133+
databaseClient.execute("SELECT * FROM table WHERE key = $1") //
134+
.bind(0, SettableValue.empty(String.class)) //
135+
.then() //
136+
.as(StepVerifier::create) //
137+
.verifyComplete();
138+
139+
verify(statement).bindNull(0, String.class);
140+
141+
databaseClient.execute("SELECT * FROM table WHERE key = $1") //
142+
.bind("$1", SettableValue.empty(String.class)) //
143+
.then() //
144+
.as(StepVerifier::create) //
145+
.verifyComplete();
146+
147+
verify(statement).bindNull("$1", String.class);
148+
}
149+
121150
@Test // gh-128
122151
public void executeShouldBindNamedNullValues() {
123152

@@ -138,7 +167,7 @@ public void executeShouldBindNamedNullValues() {
138167
verify(statement).bindNull(0, String.class);
139168
}
140169

141-
@Test // gh-128
170+
@Test // gh-128, gh-162
142171
public void executeShouldBindValues() {
143172

144173
Statement statement = mock(Statement.class);
@@ -150,7 +179,7 @@ public void executeShouldBindValues() {
150179
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)).build();
151180

152181
databaseClient.execute("SELECT * FROM table WHERE key = $1") //
153-
.bind(0, "foo") //
182+
.bind(0, SettableValue.from("foo")) //
154183
.then() //
155184
.as(StepVerifier::create) //
156185
.verifyComplete();
@@ -166,6 +195,52 @@ public void executeShouldBindValues() {
166195
verify(statement).bind("$1", "foo");
167196
}
168197

198+
@Test // gh-162
199+
public void insertShouldAcceptNullValues() {
200+
201+
Statement statement = mock(Statement.class);
202+
when(connection.createStatement("INSERT INTO foo (first, second) VALUES ($1, $2)")).thenReturn(statement);
203+
when(statement.returnGeneratedValues()).thenReturn(statement);
204+
when(statement.execute()).thenReturn(Mono.empty());
205+
206+
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) DatabaseClient.builder()
207+
.connectionFactory(connectionFactory)
208+
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)).build();
209+
210+
databaseClient.insert().into("foo") //
211+
.value("first", "foo") //
212+
.nullValue("second", Integer.class) //
213+
.then() //
214+
.as(StepVerifier::create) //
215+
.verifyComplete();
216+
217+
verify(statement).bind(0, "foo");
218+
verify(statement).bindNull(1, Integer.class);
219+
}
220+
221+
@Test // gh-162
222+
public void insertShouldAcceptSettableValue() {
223+
224+
Statement statement = mock(Statement.class);
225+
when(connection.createStatement("INSERT INTO foo (first, second) VALUES ($1, $2)")).thenReturn(statement);
226+
when(statement.returnGeneratedValues()).thenReturn(statement);
227+
when(statement.execute()).thenReturn(Mono.empty());
228+
229+
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) DatabaseClient.builder()
230+
.connectionFactory(connectionFactory)
231+
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE)).build();
232+
233+
databaseClient.insert().into("foo") //
234+
.value("first", SettableValue.from("foo")) //
235+
.value("second", SettableValue.empty(Integer.class)) //
236+
.then() //
237+
.as(StepVerifier::create) //
238+
.verifyComplete();
239+
240+
verify(statement).bind(0, "foo");
241+
verify(statement).bindNull(1, Integer.class);
242+
}
243+
169244
@Test // gh-128
170245
public void executeShouldBindNamedValuesByIndex() {
171246

0 commit comments

Comments
 (0)