Skip to content

Commit b6a464d

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 #809
1 parent 1d3ec3b commit b6a464d

File tree

3 files changed

+103
-7
lines changed

3 files changed

+103
-7
lines changed

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;
@@ -167,7 +169,14 @@ private Object readFrom(Row row, @Nullable RowMetadata metadata, RelationalPersi
167169

168170
Object value = null;
169171
if (metadata == null || RowMetadataUtils.containsColumn(metadata, identifier)) {
170-
value = row.get(identifier);
172+
173+
if (property.getType().equals(Clob.class)) {
174+
value = row.get(identifier, Clob.class);
175+
} else if (property.getType().equals(Blob.class)) {
176+
value = row.get(identifier, Blob.class);
177+
} else {
178+
value = row.get(identifier);
179+
}
171180
}
172181

173182
if (value == null) {

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

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,23 +33,28 @@
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.Arrays;
4048
import java.util.Collections;
4149
import java.util.List;
50+
import java.util.concurrent.CompletableFuture;
4251
import java.util.function.Consumer;
4352

4453
import javax.sql.DataSource;
4554

4655
import org.junit.jupiter.api.BeforeEach;
4756
import org.junit.jupiter.api.Test;
4857
import org.junit.jupiter.api.extension.RegisterExtension;
49-
5058
import org.springframework.dao.DataAccessException;
5159
import org.springframework.data.annotation.Id;
5260
import org.springframework.data.r2dbc.convert.EnumWriteSupport;
@@ -82,6 +90,13 @@ void before() {
8290
+ "primitive_array INT[]," //
8391
+ "multidimensional_array INT[]," //
8492
+ "collection_array INT[][])");
93+
94+
template.execute("DROP TABLE IF EXISTS with_blobs");
95+
template.execute("CREATE TABLE with_blobs (" //
96+
+ "id serial PRIMARY KEY," //
97+
+ "byte_array bytea," //
98+
+ "byte_buffer bytea," //
99+
+ "byte_blob bytea)");
85100
}
86101

87102
@Test // gh-30
@@ -255,9 +270,9 @@ void shouldReadAndWriteInterval() {
255270

256271
template.execute("DROP TABLE IF EXISTS with_interval");
257272
template.execute("CREATE TABLE with_interval (" //
258-
+ "id serial PRIMARY KEY," //
259-
+ "interval INTERVAL" //
260-
+ ")");
273+
+ "id serial PRIMARY KEY," //
274+
+ "interval INTERVAL" //
275+
+ ")");
261276

262277
R2dbcEntityTemplate template = new R2dbcEntityTemplate(client,
263278
new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE));
@@ -289,6 +304,62 @@ private void selectAndAssert(Consumer<? super EntityWithArrays> assertion) {
289304
.consumeNextWith(assertion).verifyComplete();
290305
}
291306

307+
@Test // gh-1408
308+
void shouldReadAndWriteBlobs() {
309+
310+
R2dbcEntityTemplate template = new R2dbcEntityTemplate(client,
311+
new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE));
312+
313+
WithBlobs withBlobs = new WithBlobs();
314+
byte[] content = "123ä".getBytes(StandardCharsets.UTF_8);
315+
316+
withBlobs.byteArray = content;
317+
withBlobs.byteBuffer = ByteBuffer.wrap(content);
318+
withBlobs.byteBlob = Blob.from(Mono.just(ByteBuffer.wrap(content)));
319+
320+
template.insert(withBlobs) //
321+
.as(StepVerifier::create) //
322+
.expectNextCount(1) //
323+
.verifyComplete();
324+
325+
template.selectOne(Query.empty(), WithBlobs.class) //
326+
.flatMap(it -> {
327+
return Flux.from(it.byteBlob.stream()).last().map(blob -> {
328+
it.byteBlob = Blob.from(Mono.just(blob));
329+
return it;
330+
});
331+
}).as(StepVerifier::create) //
332+
.consumeNextWith(actual -> {
333+
334+
CompletableFuture<byte[]> cf = Mono.from(actual.byteBlob.stream()).map(Unpooled::wrappedBuffer)
335+
.map(ByteBufUtil::getBytes).toFuture();
336+
assertThat(actual.getByteArray()).isEqualTo(content);
337+
assertThat(getBytes(Unpooled.wrappedBuffer(actual.getByteBuffer()))).isEqualTo(content);
338+
assertThat(cf.join()).isEqualTo(content);
339+
}).verifyComplete();
340+
341+
template.selectOne(Query.empty(), WithBlobs.class)
342+
.doOnNext(it -> it.byteArray = "foo".getBytes(StandardCharsets.UTF_8)).flatMap(template::update) //
343+
.as(StepVerifier::create) //
344+
.expectNextCount(1).verifyComplete();
345+
346+
template.selectOne(Query.empty(), WithBlobs.class) //
347+
.flatMap(it -> {
348+
return Flux.from(it.byteBlob.stream()).last().map(blob -> {
349+
it.byteBlob = Blob.from(Mono.just(blob));
350+
return it;
351+
});
352+
}).as(StepVerifier::create) //
353+
.consumeNextWith(actual -> {
354+
355+
CompletableFuture<byte[]> cf = Mono.from(actual.byteBlob.stream()).map(Unpooled::wrappedBuffer)
356+
.map(ByteBufUtil::getBytes).toFuture();
357+
assertThat(actual.getByteArray()).isEqualTo("foo".getBytes(StandardCharsets.UTF_8));
358+
assertThat(getBytes(Unpooled.wrappedBuffer(actual.getByteBuffer()))).isEqualTo(content);
359+
assertThat(cf.join()).isEqualTo(content);
360+
}).verifyComplete();
361+
}
362+
292363
@Data
293364
@AllArgsConstructor
294365
static class EntityWithEnum {
@@ -336,4 +407,16 @@ static class EntityWithInterval {
336407

337408
}
338409

410+
@Data
411+
@Table("with_blobs")
412+
static class WithBlobs {
413+
414+
@Id Integer id;
415+
416+
byte[] byteArray;
417+
ByteBuffer byteBuffer;
418+
Blob byteBlob;
419+
420+
}
421+
339422
}

0 commit comments

Comments
 (0)