I have a Kotlin SpringBoot project. It's a relatively simple API that persists data to a Postgres JsonB database.
I am using the #TypeDef annotation on my entity class, but after upgrading to SpringBoot Version 3.0 with hibernate-core:6.1.7.Final
this annotation is no longer available.
Here is my entity class::
import com.vladmihalcea.hibernate.type.json.JsonBinaryType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.hibernate.annotations.Type
import org.hibernate.annotations.TypeDef // not available with latest hibernate-core
import java.io.Serializable
import java.security.SecureRandom
import kotlin.math.abs
#Entity
#Table(name = "myTable")
#TypeDef(name = "jsonb", typeClass = JsonBinaryType::class) // not available :(
data class RecommendationEntity(
#Id
open var id: Long = abs(SecureRandom().nextInt().toLong()),
#Type(type = "jsonb")
#Column(columnDefinition = "jsonb")
var data: MyModel
)
data class MyModel(
// nothing special just a POJO
) : Serializable
And here is my build.gradle.kts::
plugins {
id("XXXXXXXXXX.gradle-spring-boot") version "5.0.1" // in house plugin thatcopies 3.0.2
kotlin("jvm") version "1.8.10"
kotlin("plugin.jpa") version "1.8.20-Beta"
kotlin("plugin.spring") version "1.8.20-Beta"
}
jacoco.toolVersion = "0.8.8"
configurations {
testImplementation { exclude(group = "org.junit.vintage") }
}
testSets {
"testSmoke"()
}
// allOpen {
// annotations("javax.persistence.Entity")
// }
val springDocVersion = "1.6.14"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-actuator:3.0.1")
implementation("com.deepoove:poi-tl:1.12.1") {
// exclude apache.xmlgraphics batik due to vulnerabilities when imported with poi-tl
exclude("org.apache.xmlgraphics", "batik-codec")
exclude("org.apache.xmlgraphics", "batik-transcoder")
}
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.flywaydb:flyway-core:9.11.0")
implementation("org.postgresql:postgresql:42.5.1")
implementation("org.springdoc:springdoc-openapi-webmvc-core:$springDocVersion")
implementation("org.springdoc:springdoc-openapi-ui:$springDocVersion")
implementation("org.springdoc:springdoc-openapi-kotlin:$springDocVersion")
implementation("org.springdoc:springdoc-openapi-data-rest:$springDocVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.7.0")
implementation("com.vladmihalcea:hibernate-types-52:2.21.1")
// implementation("org.hibernate:hibernate-core:5.6.3.Final")
// https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core
implementation("org.hibernate.orm:hibernate-core:6.1.7.Final")
testImplementation("org.awaitility:awaitility-kotlin:4.2.0")
testImplementation("org.mock-server:mockserver-netty:5.15.0")
testImplementation("io.projectreactor:reactor-test")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation("io.jsonwebtoken:jjwt:0.9.1")
testImplementation("com.natpryce:hamkrest:1.8.0.1")
testImplementation("org.flywaydb.flyway-test-extensions:flyway-spring-test:7.0.0")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(18))
}
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "18"
}
}
}
tasks.test {
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
}
}
val SourceSet.kotlin: SourceDirectorySet
get() = project.extensions.getByType<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension>().sourceSets.getByName(
name
).kotlin
sourceSets {
create("functional-test") {
kotlin.srcDirs("src/functional-test")
compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"]
runtimeClasspath += output + compileClasspath + sourceSets["test"].runtimeClasspath
}
}
tasks.withType<Jar>() {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
What I've tried::
Using hibernate-core:5.6.3.Final instead of 6.1.7.Final where #TypeDef is available, but that gives the error -
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Class org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider does not implement the requested interface jakarta.persistence.spi.PersistenceProvider
Looking through the various options here - how to store PostgreSQL jsonb using SpringBoot + JPA? there isn't any help specific to upgrading to SpringBoot V3.0
I'm quite stumped on this one, and am beginning to think that SpringBoot V3.0 doesn't support Postgres JsonB Nosql :(
Can anybody help me with a solution? Or perhaps confirm whether Postgres JsonB Nosql is indeed not supported in the new SpringBoot version?
Thanks
I found the solution here - https://github.com/vladmihalcea/hypersistence-utils/issues/367#issuecomment-1398737096
I bumped the version of my hibernate-types dependency implementation("com.vladmihalcea:hibernate-types-60:2.21.1")
And made changes to my entity class as recommended -
#Entity
#Table(name = "myTable")
// #TypeDef(name = "jsonb", typeClass = JsonBinaryType::class)
data class MyEntity(
#Id
open var id: Long = abs(SecureRandom().nextInt().toLong()),
// #Type(type = "jsonb")
// #Column(columnDefinition = "jsonb")
#Type(JsonType::class)
#Column(columnDefinition = "jsonb")
var data: MyModel
)
And now the issue is gone!
Related
I'm trying to define a #TestConfiguration class that is executed once before all integration tests to run a MongoDB TestContainer in Kotlin in a Spring Boot project.
Here is the code:
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.utility.DockerImageName
#TestConfiguration
class TestContainerMongoConfig {
companion object {
#JvmStatic
private val MONGO_CONTAINER: MongoDBContainer = MongoDBContainer(DockerImageName.parse("mongo").withTag("latest")).withReuse(true)
#JvmStatic
#DynamicPropertySource
private fun emulatorProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.data.mongodb.uri", MONGO_CONTAINER::getReplicaSetUrl)
}
init { MONGO_CONTAINER.start() }
}
}
The issue seems to be that emulatorProperties method is not being called.
The regular flow should be that the container is started and then the properties are set.
The first step happens, the second does not.
I know there is an alternative for which I can do this configuration in each functional test class but I don't like it as it adds not needed noise to the test class.
For example, with a Java project that uses Postgres I managed to make it work with the following code:
import javax.sql.DataSource;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
#TestConfiguration
public class PostgresqlTestContainersConfig {
static final PostgreSQLContainer POSTGRES_CONTAINER;
private final static DockerImageName IMAGE = DockerImageName.parse("postgres").withTag("latest");
static {
POSTGRES_CONTAINER = new PostgreSQLContainer(IMAGE);
POSTGRES_CONTAINER.start();
}
#Bean
DataSource dataSource() {
return DataSourceBuilder.create()
.username(POSTGRES_CONTAINER.getUsername())
.password(POSTGRES_CONTAINER.getPassword())
.driverClassName(POSTGRES_CONTAINER.getDriverClassName())
.url(POSTGRES_CONTAINER.getJdbcUrl())
.build();
}
}
I'm trying to achieve the same thing but in Kotlin and using MongoDB.
Any idea on what may be the issue causing the #DynamicPropertySource not being called?
#DynamicPropertySource is part of the Spring-Boot context lifecycle. Since you want to replicate the Java setup in a way, it is not required to use #DynamicPropertySource. Instead you can follow the Singleton Container Pattern, and replicate it in Kotlin as well.
Instead of setting the config on the registry, you can set them as a System property and Spring Autoconfig will pick it up:
init {
MONGO_CONTAINER.start()
System.setProperty("spring.data.mongodb.uri", MONGO_CONTAINER.getReplicaSetUrl());
}
I was able to resolve similar problem in Groovy by:
Having static method annotated with #DynamicPropetySource directly in the test class (probably it would also work in superclass.
But I didn't want to copy the code into every test class that needs MongoDB.
I resolved the issue by using ApplicationContexInitializer
The example is written in groovy
class MongoTestContainer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6.0.2"))
#Override
void initialize(ConfigurableApplicationContext applicationContext) {
mongoDBContainer.start()
def testValues = TestPropertyValues.of("spring.data.mongodb.uri="+ mongoDBContainer.getReplicaSetUrl())
testValues.applyTo(applicationContext.getEnvironment())
}
}
To make it complete, in the test class, you just need to add #ContextConfiguration(initializers = MongoTestContainer) to activate context initializer for the test.
For this you could also create custom annotation which would combine #DataMongoTest with previous annotation.
This solution works for me.
Method with #DynamicPropertySource is inside companion object(also added #JvmStatic) and added org.testcontainers.junit.jupiter.Testcontainers on the test class
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.jdbc.DataSourceBuilder
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import javax.sql.DataSource
#ExtendWith(SpringExtension::class)
#Testcontainers
#TestConfiguration
#ContextConfiguration(classes = [PostgresqlTestContainersConfig::class])
class PostgresqlTestContainersConfig {
#Autowired
var dataSource: DataSource? = null
#Test
internal fun name() {
dataSource!!.connection.close()
}
#Bean
fun dataSource(): DataSource? {
return DataSourceBuilder.create()
.username(POSTGRES_CONTAINER.getUsername())
.password(POSTGRES_CONTAINER.getPassword())
.driverClassName(POSTGRES_CONTAINER.getDriverClassName())
.url(POSTGRES_CONTAINER.getJdbcUrl())
.build()
}
companion object {
#JvmStatic
#Container
private val POSTGRES_CONTAINER: PostgreSQLContainer<*> = PostgreSQLContainer("postgres:9.6.12")
.withDatabaseName("integration-tests-db")
.withUsername("sa")
.withPassword("sa")
#JvmStatic
#DynamicPropertySource
fun postgreSQLProperties(registry: DynamicPropertyRegistry) {
registry.add("db.url") { POSTGRES_CONTAINER.jdbcUrl }
registry.add("db.user") { POSTGRES_CONTAINER.username }
registry.add("db.password") { POSTGRES_CONTAINER.password }
}
}
}
I've two MapperConfig:
#MapperConfig(
uses = {
StringTypeMapper.class,
ExtensionMapper.class
}
)
public interface ElementMapperConfig extends GenericMapperConfig {
#Mapping(target = "id", source = "idElement")
#Mapping(target = "extension", source = "extension")
Element mapElement(org.hl7.fhir.r4.model.Element fhir);
}
And GenericMapperConfig:
#MapperConfig(
componentModel = "spring",
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface GenericMapperConfig {
}
As you can see, I'm using spring component model.
Nevertheless, Mapper implementation is getting required Mapper using Mappers.getMapper(...).
As you can see, ElementMapperConfig extends GenericMapperConfig, but it seems that configuration from GenericMapperConfig is ignored.
Generated Mapper example:
#Generated(
value = "org.mapstruct.ap.MappingProcessor"
)
public class StringTypeMapperImpl implements StringTypeMapper {
private final ExtensionMapper extensionMapper = Mappers.getMapper( ExtensionMapper.class );
}
StringTypeMapper is:
#Mapper(
config = ElementMapperConfig.class
)
public interface StringTypeMapper {
#InheritConfiguration(name = "mapElement")
StringType fhirToMpi(org.hl7.fhir.r4.model.StringType stringType);
}
I don't quite figure out why GenericMapperConfig configuration is not populated, I mean, I don't get why componentModel = "spring" is ignored on Mapper implementation.
The documentation does not mention this way of composing multiple MapperConfigurations.
It also does not mention another way of doing it, but this one works. The idea is to extend mappers instead of configurations.
Introduce a base mapper with generic configuration:
#Mapper(
config = GenericMapperConfig.class
)
public interface BaseMapper {
}
Base your concrete mapper on the base one and configure it using the specific configuration:
#Mapper(
config = ElementMapperConfig.class
)
public interface StringTypeMapper extends BaseMapper {
#InheritConfiguration(name = "mapElement")
StringType fhirToMpi(org.hl7.fhir.r4.model.StringType stringType);
}
Finally make ElementMapperConfig not inherit GenericMapperConfig:
#MapperConfig(
uses = {
StringTypeMapper.class,
ExtensionMapper.class
}
)
public interface ElementMapperConfig {
#Mapping(target = "id", source = "idElement")
#Mapping(target = "extension", source = "extension")
Element mapElement(org.hl7.fhir.r4.model.Element fhir);
}
I have a problem when I try to use a MongoRepository. This is my Document class:
package model
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
#Document
class Product (#Id val name: String, var desc: String, var price: Double) {
var pictureCategory: String? = null
}
This is the repository:
package model.repositories
import model.Product
import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.stereotype.Repository
#Repository
interface ProductRepository : MongoRepository <Product, String>
and this is the file where I have the compile error:
package controllers
import model.Product
import model.repositories.ProductRepository
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
#Controller
#RequestMapping("/product")
class ProductController {
#PostMapping("")
fun addProduct(#RequestBody newProduct: Product){
ProductRepository.save() //Unresolved reference: save <----------------------
}
}
I tried to performe and invalidate/restart but nothing is changed.
This is my build.gradle.kts file:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.4.3"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.4.30"
kotlin("plugin.spring") version "1.4.30"
kotlin("plugin.jpa") version "1.4.30"
}
group = "com.example"
version = "1.0.0"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
Maybe some dependencies that I have to add?
You are supposed to inject your repository in the REST controller, not using the static "save" method.
See : https://spring.io/guides/tutorials/rest/ for example.
Previously were using Spring cloud Finchley.M9 release. We used LoadBalancerClient as like below.
'''import java.net.URI;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Component;
#Component
public class UriResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(UriResolver.class);
private LoadBalancerClient loadBalancerClient;
#Autowired
public UriResolver(LoadBalancerClient loadBalancerClient) {
this.loadBalancerClient = loadBalancerClient;
}
public URI getBaseUri(String vendorName) {
ServiceInstance instance = loadBalancerClient.choose(vendorName);
URI uri = null;
if (!Objects.isNull(instance)) {
uri = instance.getUri();
LOGGER.debug("LoadBalancer Instance specific URI: " + instance.getUri());
LOGGER.debug("LoadBalancer Instance specific ServiceId: " + instance.getServiceId());
LOGGER.debug("LoadBalancer Instance specific Host: " + instance.getHost());
}
return uri;
}
}'''
Now we have upgraded the spring cloud version to Hoxton.SR8. We get the below error while starting the spring boot app.
Bean method 'loadBalancerClient' in 'RibbonAutoConfiguration' not loaded because AllNestedConditions 2 matched 2 did not; NestedCondition on RibbonAutoConfiguration.RibbonClassesConditions.RibbonPresent #ConditionalOnClass did not find required class 'com.netflix.ribbon.Ribbon'; NestedCondition on RibbonAutoConfiguration.RibbonClassesConditions.AsyncRestTemplatePresent #ConditionalOnClass found required class 'org.springframework.web.client.AsyncRestTemplate'; NestedCondition on RibbonAutoConfiguration.RibbonClassesConditions.RestTemplatePresent #ConditionalOnClass found required class 'org.springframework.web.client.RestTemplate'; NestedCondition on RibbonAutoConfiguration.RibbonClassesConditions.IClientPresent #ConditionalOnClass did not find required class 'com.netflix.client.IClient'
I'm using eclipselink 2.6 as a persistence provider of spring data jpa, that in my understanding, now allows you to serialize a subtree of an entity as json using the internal moxy serializer.
So I'm trying to mix this to migrate from embedded element collections to a serialized json using the json datatype of postgres.
I have an entity named Product, and this entity have the following mapped property:
#Convert(Convert.JSON)
private List<MetadataIndex> indexes=new ArrayList<MetadataIndex> ();
In which metadata index is a simple class with a few string properties.
I would like to convert this list of object into a json and store it into a column of json data type in postgres.
I thought that the above code should suffice, but it does not. The application crashes on boot (can't create entitymanager factory - npe somwhere inside eclipselink).
If I change the converter to #Convert(Convert.SERIALIZED) it works. It creates a field on the table Products named indexes of type bytea and store the serialized list in it.
Is this an eclipselink bug or I'm missing something?
Thank you.
well, I've used a custom eclipselink converter to convert my classes into json objects, then store them into the db using directly the postgres driver. This is the converter.
import fr.gael.dhus.database.jpa.domain.MetadataIndex;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.sessions.Session;
import org.postgresql.util.PGobject;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
/**
* Created by fmarino on 20/03/2015.
*/
#Converter
public class JsonConverter implements org.eclipse.persistence.mappings.converters.Converter {
private static ObjectMapper mapper = new ObjectMapper();
#Override
public Object convertObjectValueToDataValue(Object objectValue, Session session) {
try {
PGobject out = new PGobject();
out.setType("jsonb");
out.setValue( mapper.writerWithType( new TypeReference<Collection<MetadataIndex>>() {} )
.writeValueAsString(objectValue) );
return out;
} catch (IOException e) {
throw new IllegalArgumentException("Unable to serialize to json field ", e);
} catch (SQLException e) {
throw new IllegalArgumentException("Unable to serialize to json field ", e);
}
}
#Override
public Object convertDataValueToObjectValue(Object dataValue, Session session) {
try {
if(dataValue instanceof PGobject && ((PGobject) dataValue).getType().equals("jsonb"))
return mapper.reader( new TypeReference<Collection<MetadataIndex>>() {}).readValue(((PGobject) dataValue).getValue());
return "-";
} catch (IOException e) {
throw new IllegalArgumentException("Unable to deserialize to json field ", e);
}
}
#Override
public boolean isMutable() {
return false;
}
#Override
public void initialize(DatabaseMapping mapping, Session session) {
}
}
as you can see I use jackson for serialization, and specify the datatype as Collection. You can use the type you want here.
Inside my classes, I've mapped my field with this:
#Convert(converter = JsonConverter.class)
#Column (nullable = true, columnDefinition = "jsonb")
adding also this annotation to the class:
#Converter(converterClass = JsonConverter.class, name = "jsonConverter")
To make things works properly with jackson I've also added to my MetadataIndex class this annotation, on the class element:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#class")
I personally like using directly the postgres driver to store those kind of special datatype. I didn't manage to achieve the same with hibernate.
As for the converter, I've would preferred a more general solution, but jackson forced me to state the object type I want to convert. If you find a better way to do it, let me know.
With a similar approach, I've also manage to use the hstore datatype of postgres.