DEV Community

Thomas Schühly
Thomas Schühly

Posted on • Edited on

Spring Content - The better way to save and serve files and images with Spring Boot

Table Of Contents

Save images directly into the DB

Recently I added a feature to my web app where my customer could upload images and display them inside the app.

At first I stored them directly into the db as ByteArray inside an JPA Entity with a @lob annotation

@Entity
class Picture(
    val name: String?,
    val type: String?,
    @Lob
    val data: ByteArray
)
Enter fullscreen mode Exit fullscreen mode

I then returned them as Byte64 encoded string inside my JSON API Response.

Issues

It was working fine in my local dev enviroment, but as soon as I pushed it to prod I encountered an error where my whole json structure needed for my frontend wasn't displayed properly.
The Byte64 encoded image string just stopped in the middle.

I cloned the prod db locally into a postgres container and then the issue occured also locally.

HHH000100: Fail-safe cleanup (collections) : org.hibernate.engine.loading.internal.CollectionLoadContext@2555100f<rs=HikariProxyResultSet@1189576917 wrapping org.postgresql.jdbc.PgResultSet@545635a4>

Enter fullscreen mode Exit fullscreen mode

After some 2am debugging I got a JacksonMapping exception and found the error on stackoverflow.

I thought the solution was too complex and looked for a simpler way.

Learnings

  1. Lessons: Always test with an environment as similiar as production. I can recommend testcontainers for easier integration tests.

  2. Lesson: Use a database versioning tool such as liquibase instead of spring.jpa.hibernate.ddl-auto = update.

With h2 the @lob was correctly saved as bytearray. With postgres the type was assumed to be oid which caused a issue mapping between @lob and the bytea column type.

This was fixed with the hibernate Annotation

@Type(type = "org.hibernate.type.MaterializedBlobType")

  1. Lesson: Dont serve images directly as Base64 encoded string.

Instead use a seperate controller and serve the imagedata raw.

Serialize the entity which stores the data and the metadata.

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
@JsonIdentityReference(alwaysAsId=true)
Enter fullscreen mode Exit fullscreen mode

This avoids huge json responses and slowdowns to your website.

Spring Content to the rescue

Then I discovered spring-content which looked like a better alternative than saving them directly to a db, as I could switch between db, filesystem, AWS S3 and MongoDB GridFs by just switching the dependency.

To use Spring Content you just need to add the depencency in your build.gradle.kts

implementation("com.github.paulcwarren:content-jpa-spring-boot-starter:1.2.5")
Enter fullscreen mode Exit fullscreen mode

Add the @ContentId annotation to your file entity. In my case i replaced:

    @JsonIgnore
    @Type(type = "org.hibernate.type.MaterializedBlobType")
    val data: ByteArray
Enter fullscreen mode Exit fullscreen mode

with

@ContentId var imageId: String
Enter fullscreen mode Exit fullscreen mode

Create a ContentStore

interface PictureContentStore : ContentStore<Picture, String>
Enter fullscreen mode Exit fullscreen mode

In your method where you save the Files you need to use the contentstore.setContent() method. It is important to first save the entity.

val picture = pictureRepository.save(Picture(pictureFile.originalFilename, pictureFile.contentType,null))
pictureContentStore.setContent(picture, pictureFile.inputStream)
pictureRepository.save(picture)
Enter fullscreen mode Exit fullscreen mode

Add the handling of pictures to your file entity

@RestController
@RequestMapping("api/picture")
class PictureController(
    val pictureService: PictureService,
    val pictureContentStore: PictureContentStore,
) {
    @GetMapping("/{id}")
    fun getById(
        @PathVariable id: Long,
    ): ResponseEntity<InputStreamResource> {
        pictureService.getById(id)
            ?.let { pic ->
                val content = pictureContentStore.getContent(pic)
                val inputStreamResource = InputStreamResource(content)

                return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(pic.type.toString()))
                    .body(inputStreamResource)
            }
        throw NoSuchElementException("There is no Image with the corresponding id")
    }
}

Enter fullscreen mode Exit fullscreen mode

and you are done.

You can look up my source code on Github.

This is my first project as a freelancer. Any suggestions or feedback is very welcome

Top comments (0)