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 my Postgres Database as ByteArray inside a JPA Entity with a @Lob annotation
@Entity
class Picture(
val name: String?,
val type: String?,
@Lob
val data: ByteArray
)
I then returned them as Byte64 encoded string inside my JSON API Response.
Issues
It was working fine in my local dev environment, 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>
After some 2 am 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
Lessons: Always test with an environment as similar as production. I can recommend testcontainers for easier integration tests.
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")
- Lesson: Don't serve images directly as Base64 encoded string.
Instead, use a separate controller and serve the image data raw.
Serialize the entity which stores the data and the metadata.
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
@JsonIdentityReference(alwaysAsId=true)
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 dependency in your build.gradle.kts
implementation("com.github.paulcwarren:content-jpa-spring-boot-starter:1.2.5")
Add the @ContentId annotation to your file entity. In my Case I replaced:
@JsonIgnore
@Type(type = "org.hibernate.type.MaterializedBlobType")
val data: ByteArray
with
@ContentId var imageId: String
Create a ContentStore
interface PictureContentStore : ContentStore<Picture, String>
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)
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")
}
}
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
If you want to learn more about HTMX + Spring Boot check out my series Web development without the JavaScript headache with Spring + HTMX.
My side business PhotoQuest is also built with HTMX + JTE