Skip to content

Commit d6e00a8

Browse files
committed
Fix BLOB reading and writing.
We now correctly consider ByteBuffer, Clob, and Blob types as additional types to read and write blob data. Closes #1408
1 parent 1ff403e commit d6e00a8

File tree

3 files changed

+103
-7
lines changed

3 files changed

+103
-7
lines changed

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.r2dbc.convert;
1717

18+
import io.r2dbc.spi.Blob;
19+
import io.r2dbc.spi.Clob;
1820
import io.r2dbc.spi.ColumnMetadata;
1921
import io.r2dbc.spi.Row;
2022
import io.r2dbc.spi.RowMetadata;
@@ -158,7 +160,14 @@ private Object readFrom(Row row, @Nullable RowMetadata metadata, RelationalPersi
158160

159161
Object value = null;
160162
if (metadata == null || RowMetadataUtils.containsColumn(metadata, identifier)) {
161-
value = row.get(identifier);
163+
164+
if (property.getType().equals(Clob.class)) {
165+
value = row.get(identifier, Clob.class);
166+
} else if (property.getType().equals(Blob.class)) {
167+
value = row.get(identifier, Blob.class);
168+
} else {
169+
value = row.get(identifier);
170+
}
162171
}
163172

164173
if (value == null) {

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/mapping/R2dbcSimpleTypeHolder.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
*/
1616
package org.springframework.data.r2dbc.mapping;
1717

18+
import io.r2dbc.spi.Blob;
19+
import io.r2dbc.spi.Clob;
1820
import io.r2dbc.spi.Row;
1921

2022
import java.math.BigDecimal;
2123
import java.math.BigInteger;
24+
import java.nio.ByteBuffer;
2225
import java.util.Arrays;
2326
import java.util.Collections;
2427
import java.util.HashSet;
@@ -37,8 +40,9 @@ public class R2dbcSimpleTypeHolder extends SimpleTypeHolder {
3740
/**
3841
* Set of R2DBC simple types.
3942
*/
40-
public static final Set<Class<?>> R2DBC_SIMPLE_TYPES = Collections.unmodifiableSet(
41-
new HashSet<>(Arrays.asList(OutboundRow.class, Row.class, BigInteger.class, BigDecimal.class, UUID.class)));
43+
public static final Set<Class<?>> R2DBC_SIMPLE_TYPES = Collections
44+
.unmodifiableSet(new HashSet<>(Arrays.asList(OutboundRow.class, Row.class, BigInteger.class, BigDecimal.class,
45+
UUID.class, Blob.class, Clob.class, ByteBuffer.class)));
4246

4347
public static final SimpleTypeHolder HOLDER = new R2dbcSimpleTypeHolder();
4448

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
*/
1616
package org.springframework.data.r2dbc.core;
1717

18+
import static io.netty.buffer.ByteBufUtil.*;
1819
import static org.assertj.core.api.Assertions.*;
1920
import static org.springframework.data.relational.core.query.Criteria.*;
2021

22+
import io.netty.buffer.ByteBufUtil;
23+
import io.netty.buffer.Unpooled;
2124
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
2225
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
2326
import io.r2dbc.postgresql.codec.Box;
@@ -30,21 +33,26 @@
3033
import io.r2dbc.postgresql.codec.Point;
3134
import io.r2dbc.postgresql.codec.Polygon;
3235
import io.r2dbc.postgresql.extension.CodecRegistrar;
36+
import io.r2dbc.spi.Blob;
3337
import io.r2dbc.spi.ConnectionFactory;
3438
import lombok.AllArgsConstructor;
3539
import lombok.Data;
40+
import reactor.core.publisher.Flux;
41+
import reactor.core.publisher.Mono;
3642
import reactor.test.StepVerifier;
3743

44+
import java.nio.ByteBuffer;
45+
import java.nio.charset.StandardCharsets;
3846
import java.time.Duration;
3947
import java.util.Collections;
4048
import java.util.List;
49+
import java.util.concurrent.CompletableFuture;
4150

4251
import javax.sql.DataSource;
4352

4453
import org.junit.jupiter.api.BeforeEach;
4554
import org.junit.jupiter.api.Test;
4655
import org.junit.jupiter.api.extension.RegisterExtension;
47-
4856
import org.springframework.dao.DataAccessException;
4957
import org.springframework.data.annotation.Id;
5058
import org.springframework.data.r2dbc.convert.EnumWriteSupport;
@@ -81,6 +89,13 @@ void before() {
8189
+ "primitive_array INT[]," //
8290
+ "multidimensional_array INT[]," //
8391
+ "collection_array INT[][])");
92+
93+
template.execute("DROP TABLE IF EXISTS with_blobs");
94+
template.execute("CREATE TABLE with_blobs (" //
95+
+ "id serial PRIMARY KEY," //
96+
+ "byte_array bytea," //
97+
+ "byte_buffer bytea," //
98+
+ "byte_blob bytea)");
8499
}
85100

86101
@Test // gh-411
@@ -198,9 +213,9 @@ void shouldReadAndWriteInterval() {
198213

199214
template.execute("DROP TABLE IF EXISTS with_interval");
200215
template.execute("CREATE TABLE with_interval (" //
201-
+ "id serial PRIMARY KEY," //
202-
+ "interval INTERVAL" //
203-
+ ")");
216+
+ "id serial PRIMARY KEY," //
217+
+ "interval INTERVAL" //
218+
+ ")");
204219

205220
R2dbcEntityTemplate template = new R2dbcEntityTemplate(client,
206221
new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE));
@@ -213,6 +228,62 @@ void shouldReadAndWriteInterval() {
213228
}).verifyComplete();
214229
}
215230

231+
@Test // gh-1408
232+
void shouldReadAndWriteBlobs() {
233+
234+
R2dbcEntityTemplate template = new R2dbcEntityTemplate(client,
235+
new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE));
236+
237+
WithBlobs withBlobs = new WithBlobs();
238+
byte[] content = "123ä".getBytes(StandardCharsets.UTF_8);
239+
240+
withBlobs.byteArray = content;
241+
withBlobs.byteBuffer = ByteBuffer.wrap(content);
242+
withBlobs.byteBlob = Blob.from(Mono.just(ByteBuffer.wrap(content)));
243+
244+
template.insert(withBlobs) //
245+
.as(StepVerifier::create) //
246+
.expectNextCount(1) //
247+
.verifyComplete();
248+
249+
template.selectOne(Query.empty(), WithBlobs.class) //
250+
.flatMap(it -> {
251+
return Flux.from(it.byteBlob.stream()).last().map(blob -> {
252+
it.byteBlob = Blob.from(Mono.just(blob));
253+
return it;
254+
});
255+
}).as(StepVerifier::create) //
256+
.consumeNextWith(actual -> {
257+
258+
CompletableFuture<byte[]> cf = Mono.from(actual.byteBlob.stream()).map(Unpooled::wrappedBuffer)
259+
.map(ByteBufUtil::getBytes).toFuture();
260+
assertThat(actual.getByteArray()).isEqualTo(content);
261+
assertThat(getBytes(Unpooled.wrappedBuffer(actual.getByteBuffer()))).isEqualTo(content);
262+
assertThat(cf.join()).isEqualTo(content);
263+
}).verifyComplete();
264+
265+
template.selectOne(Query.empty(), WithBlobs.class)
266+
.doOnNext(it -> it.byteArray = "foo".getBytes(StandardCharsets.UTF_8)).flatMap(template::update) //
267+
.as(StepVerifier::create) //
268+
.expectNextCount(1).verifyComplete();
269+
270+
template.selectOne(Query.empty(), WithBlobs.class) //
271+
.flatMap(it -> {
272+
return Flux.from(it.byteBlob.stream()).last().map(blob -> {
273+
it.byteBlob = Blob.from(Mono.just(blob));
274+
return it;
275+
});
276+
}).as(StepVerifier::create) //
277+
.consumeNextWith(actual -> {
278+
279+
CompletableFuture<byte[]> cf = Mono.from(actual.byteBlob.stream()).map(Unpooled::wrappedBuffer)
280+
.map(ByteBufUtil::getBytes).toFuture();
281+
assertThat(actual.getByteArray()).isEqualTo("foo".getBytes(StandardCharsets.UTF_8));
282+
assertThat(getBytes(Unpooled.wrappedBuffer(actual.getByteBuffer()))).isEqualTo(content);
283+
assertThat(cf.join()).isEqualTo(content);
284+
}).verifyComplete();
285+
}
286+
216287
@Data
217288
@AllArgsConstructor
218289
static class EntityWithEnum {
@@ -260,4 +331,16 @@ static class EntityWithInterval {
260331

261332
}
262333

334+
@Data
335+
@Table("with_blobs")
336+
static class WithBlobs {
337+
338+
@Id Integer id;
339+
340+
byte[] byteArray;
341+
ByteBuffer byteBuffer;
342+
Blob byteBlob;
343+
344+
}
345+
263346
}

0 commit comments

Comments
 (0)