-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
391 additions
and
13 deletions.
There are no files selected for viewing
8 changes: 2 additions & 6 deletions
8
boot-filepart/src/main/java/com/example/demo/PostSummary.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,7 @@ | ||
package com.example.demo; | ||
|
||
import lombok.Value; | ||
|
||
import java.util.UUID; | ||
|
||
public record PostSummary ( | ||
UUID id, | ||
String title | ||
){} | ||
public record PostSummary(UUID id, String title) { | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,188 @@ | ||
# Handling File Upload/Download with Spring WebFlux and Spring Data R2dbc | ||
# Handling File Upload/Download with Spring WebFlux and Spring Data R2dbc | ||
|
||
In [last post](https://hantsy.medium.com/persisting-binary-data-into-postgres-using-spring-data-r2dbc-cefe6afb3e1c), we have explore how to persist binary data into Postgres database using R2dbc/Spring Data R2dbc. In this post, we continue to discuss how to upload file and download file via Spring WebFlux stack, and of course handling the persistence using the former usages we have done in previous post. | ||
|
||
Let's work on the existing project we used in the previous post. | ||
|
||
Create a controller for `Post`. | ||
* A method to retrieve all posts. | ||
* A method to save new Post. | ||
* A method to retrieve Post by id. | ||
|
||
```java | ||
@RestController | ||
@RequestMapping("/posts") | ||
@Slf4j | ||
@RequiredArgsConstructor | ||
class PostController { | ||
|
||
private final PostRepository posts; | ||
|
||
@GetMapping | ||
public Flux<PostSummary> all(@RequestParam(required = true, defaultValue = "") String title, | ||
@RequestParam(required = true, defaultValue = "0") Integer page, | ||
@RequestParam(required = true, defaultValue = "10") Integer size | ||
) { | ||
return posts.findByTitleLike("%" + title + "%", PageRequest.of(page, size)); | ||
} | ||
|
||
@PostMapping | ||
public Mono<ResponseEntity> create(@RequestBody CreatePostCommand data) { | ||
return posts.save(Post.builder().title(data.title()).content(data.content()).build()) | ||
.map(saved -> ResponseEntity.created(URI.create("/posts/" + saved.getId())).build()); | ||
} | ||
|
||
@GetMapping("{id}") | ||
public Mono<ResponseEntity<Post>> get(@PathVariable UUID id) { | ||
return this.posts.findById(id) | ||
.map(post -> ResponseEntity.ok().body(post)) | ||
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); | ||
} | ||
|
||
} | ||
``` | ||
|
||
The `@RequiredArgsConstructor` will append a constructor with the `final` fields as parameters at compile time. | ||
|
||
```java | ||
public PostController(PostRepository posts;) { | ||
this.posts = posts; | ||
} | ||
``` | ||
|
||
Spring supports injection via constructor for the constructor with parameters if the class has one and only such one constructor. | ||
|
||
The `PostSummary` contains the essential info of a `Post`. | ||
|
||
```java | ||
public record PostSummary(UUID id, String title) { | ||
} | ||
``` | ||
|
||
Add a method into the `PostRepository`. | ||
|
||
```java | ||
interface PostRepository extends R2dbcRepository<Post, UUID>{ | ||
|
||
public Flux<PostSummary> findByTitleLike(String title, Pageable pageable); | ||
} | ||
``` | ||
|
||
Notes, in the *reactive* `Repository`, it accepts a `Pageable` as the last parameter in the method parameter list, but it does not return a `Page` object as result. To get the count of the total elements, you should use another count query like the following. | ||
|
||
```java | ||
interface PostRepository extends R2dbcRepository<Post, UUID>{ | ||
// ... | ||
public Long countByTitleLike(String title); | ||
} | ||
``` | ||
|
||
The `/posts` endpoint get all posts that only includes post titles, binary data in the JSON will be displayed as unrecognized characters. | ||
|
||
It is better to use a standalone endpoint to save and read binary data. | ||
|
||
Add two methods to handle attachment upload and download. | ||
|
||
```java | ||
@PutMapping("{id}/attachment") | ||
public Mono<ResponseEntity> upload(@PathVariable UUID id, | ||
@RequestPart Mono<FilePart> fileParts) { | ||
|
||
return Mono | ||
.zip(objects -> { | ||
var post = (Post) objects[0]; | ||
var filePart = (DataBuffer) objects[1]; | ||
post.setAttachment(filePart.toByteBuffer()); | ||
return post; | ||
}, | ||
this.posts.findById(id), | ||
fileParts.flatMap(filePart -> DataBufferUtils.join(filePart.content())) | ||
) | ||
.flatMap(this.posts::save) | ||
.map(saved -> ResponseEntity.noContent().build()); | ||
} | ||
|
||
@GetMapping("{id}/attachment") | ||
public Mono<Void> read(@PathVariable UUID id, ServerWebExchange exchange) { | ||
return this.posts.findById(id) | ||
.log() | ||
.map(post -> Mono.just(new DefaultDataBufferFactory().wrap(post.getAttachment()))) | ||
.flatMap(r -> exchange.getResponse().writeWith(r)); | ||
} | ||
``` | ||
|
||
The `GET posts/{id}/attachment` endpoint writes the binary data into HTTP response directly. | ||
|
||
Create an integration test to the file upload and download progress. | ||
|
||
```java | ||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
@Slf4j | ||
public class IntegrationTests { | ||
|
||
@LocalServerPort | ||
private int port; | ||
|
||
private WebTestClient webClient; | ||
|
||
@BeforeEach | ||
public void setup() { | ||
this.webClient = WebTestClient.bindToServer() | ||
.baseUrl("http://localhost:" + this.port) | ||
.codecs(clientCodecConfigurer -> | ||
clientCodecConfigurer.defaultCodecs().enableLoggingRequestDetails(true) | ||
) | ||
.build(); | ||
} | ||
|
||
@Test | ||
public void willLoadPosts() { | ||
this.webClient.get().uri("/posts") | ||
.exchange() | ||
.expectStatus().is2xxSuccessful(); | ||
} | ||
|
||
@Test | ||
public void testUploadAndDownload() { | ||
var locationUri = this.webClient.post() | ||
.uri("/posts") | ||
.contentType(MediaType.APPLICATION_JSON) | ||
.bodyValue(new CreatePostCommand("test title", "test content")) | ||
.exchange() | ||
.expectStatus().isCreated() | ||
.expectHeader().exists(HttpHeaders.LOCATION) | ||
.returnResult(ParameterizedTypeReference.forType(Void.class)) | ||
.getResponseHeaders().getLocation(); | ||
|
||
log.debug("location uri: {}", locationUri); | ||
assertThat(locationUri).isNotNull(); | ||
|
||
var attachmentUri = locationUri + "/attachment"; | ||
this.webClient.put() | ||
.uri(attachmentUri) | ||
.bodyValue(generateBody()) | ||
.exchange() | ||
.expectStatus().isNoContent(); | ||
|
||
var responseContent = this.webClient.get() | ||
.uri(attachmentUri) | ||
.accept(MediaType.APPLICATION_OCTET_STREAM) | ||
.exchange() | ||
.expectStatus().isOk() | ||
.returnResult(ParameterizedTypeReference.forType(byte[].class)) | ||
.getResponseBodyContent(); | ||
|
||
assertThat(responseContent).isNotNull(); | ||
assertThat(new String(responseContent)).isEqualTo("test"); | ||
} | ||
|
||
private MultiValueMap<String, HttpEntity<?>> generateBody() { | ||
MultipartBodyBuilder builder = new MultipartBodyBuilder(); | ||
builder.part("fileParts", new ClassPathResource("/foo.txt", IntegrationTests.class)); | ||
return builder.build(); | ||
} | ||
|
||
} | ||
``` | ||
|
||
The complete [sample codes](https://github.com/hantsy/spring-r2dbc-sample/blob/master/filepart) are shared on my Github account. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,7 @@ | ||
* [Introduction to R2dbc](./intro.md) | ||
|
||
* [Working with Relational Database using R2dbc DatabaseClient](./database-client.md) | ||
|
||
* [*Update*: Accessing RDBMS with Spring Data R2dbc](./data-r2dbc.md) | ||
|
||
* [Data Auditing with Spring Data R2dbc](./auditing.md) | ||
|
||
* [Dealing with Postgres specific Json/Enum type and NOTIFY/LISTEN with R2dbc](./pg.md) | ||
|
||
|
||
* [Persisting Binary Data into Postgres using Spring Data R2dbc](./docs/persist-bin.md) | ||
* [Handling File Upload/Download with Spring WebFlux and Spring Data R2dbc](./docs/filepart.md) |
Oops, something went wrong.