I have been consulting a number of approaches/posts/stackoverflow questions in order to deal with the following error (full stack trace) when running a Kotlin/SpringBoot app
I propose my solution in a pull-request
The idea is to change Entity to:
import com.example.demo.pojo.SamplePojo
import com.vladmihalcea.hibernate.type.json.JsonBinaryType
import com.vladmihalcea.hibernate.type.json.JsonStringType
import org.hibernate.annotations.Type
import org.hibernate.annotations.TypeDef
import org.hibernate.annotations.TypeDefs
import javax.persistence.*
@Entity
@Table(name = "tests")
@TypeDefs(
TypeDef(name = "json", typeClass = JsonStringType::class),
TypeDef(name = "jsonb", typeClass = JsonBinaryType::class)
)
data class SampleEntity (
@Id @GeneratedValue
val id: Long?,
val name: String?,
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
var data: Map<String, Any>?
) {
/**
* Dependently on use-case this can be done differently:
* https://stackoverflow.com/questions/37873995/how-to-create-empty-constructor-for-data-class-in-kotlin-android
*/
constructor(): this(null, null, null)
}
Map<String, Any>
typeSince we have a full-control what will be in POJO in business logic the only missing piece will be to convert POJO to Map and Map to POJO
SamplePojo implementation
data class SamplePojo(
val payload: String,
val flag: Boolean
) {
constructor(map: Map<String, Any>) : this(map["payload"] as String, map["flag"] as Boolean)
fun toMap() : Map<String, Any> {
return mapOf("payload" to payload, "flag" to flag)
}
}
This is rather a workaround but it allows us to work with any depth-level structures.
P.S. I noticed that you use Serializer
and redefined equals, toString, hashCode
. You don't need this if using data class.
UPDATE:
If you need a more flexible structure than Map<String, Any>
, you can use JsonNode
. Code example
Entity:
import com.fasterxml.jackson.databind.JsonNode
import com.vladmihalcea.hibernate.type.json.JsonBinaryType
import com.vladmihalcea.hibernate.type.json.JsonStringType
import org.hibernate.annotations.Type
import org.hibernate.annotations.TypeDef
import org.hibernate.annotations.TypeDefs
import javax.persistence.*
@Entity
@Table(name = "tests")
@TypeDefs(
TypeDef(name = "json", typeClass = JsonStringType::class),
TypeDef(name = "jsonb", typeClass = JsonBinaryType::class)
)
data class SampleJsonNodeEntity (
@Id @GeneratedValue
val id: Long?,
val name: String?,
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
var data: JsonNode?
) {
/**
* Dependently on use-case this can be done differently:
* https://stackoverflow.com/questions/37873995/how-to-create-empty-constructor-for-data-class-in-kotlin-android
*/
constructor(): this(null, null, null)
}
Change Entity in Repository:
import com.example.demo.entity.SampleJsonNodeEntity
import org.springframework.data.jpa.repository.JpaRepository
interface SampleJsonNodeRepository: JpaRepository<SampleJsonNodeEntity, Long> {
}
Tests for both approaches:
import com.example.demo.DbTestInitializer
import com.example.demo.entity.SampleJsonNodeEntity
import com.example.demo.entity.SampleMapEntity
import com.example.demo.pojo.SamplePojo
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringRunner
@RunWith(SpringRunner::class)
@SpringBootTest
@ContextConfiguration(initializers = [DbTestInitializer::class])
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class SampleRepositoryTest {
@Autowired
lateinit var sampleMapRepository: SampleMapRepository
@Autowired
lateinit var sampleJsonNodeRepository: SampleJsonNodeRepository
lateinit var dto: SamplePojo
lateinit var mapEntity: SampleMapEntity
lateinit var jsonNodeEntity: SampleJsonNodeEntity
@Before
fun setUp() {
dto = SamplePojo("Test", true)
mapEntity = SampleMapEntity(null,
"POJO1",
dto.toMap()
)
jsonNodeEntity = SampleJsonNodeEntity(null,
"POJO2",
jacksonObjectMapper().valueToTree(dto)
)
}
@Test
fun createMapPojo() {
val id = sampleMapRepository.save(mapEntity).id!!
assertNotNull(sampleMapRepository.getOne(id))
assertEquals(sampleMapRepository.getOne(id).data?.let { SamplePojo(it) }, dto)
}
@Test
fun createJsonNodePojo() {
val id = sampleJsonNodeRepository.save(jsonNodeEntity).id!!
assertNotNull(sampleJsonNodeRepository.getOne(id))
assertEquals(jacksonObjectMapper().treeToValue(sampleJsonNodeRepository.getOne(id).data, SamplePojo::class.java), dto)
}
}
Extending with an example, sorry I know I am a bit late
in your pom.xml:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>2.4.3</version>
</dependency>
Then I have my entity named Day:
import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
@TypeDefs({
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
@Data
@Entity
public class Day {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "DayId")
private Integer id;
private Integer day;
private Integer month;
private Integer year;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private List<Activity> activities;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private Notification notification;
}
Activity and Notification JSONB's class:
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Activity implements Serializable {
private String name;
private String emoji;
private Integer durationInSeconds;
private Boolean highPriority;
public Activity (){}
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Notification implements Serializable {
private String email;
private String mobile;
public Notification (){}
}
Our repository:
@Repository
public interface DayRepository extends CrudRepository<Day, Integer> {
}
Our service:
public interface DayService{
Day saveArbitraryDay();
}
@Service
@Transactional
public DayServiceImpl implements DayService{
private DayRepository repository;
public DayServiceImpl(DayRepository repository){
this.repository = repository;
}
@Override
public Day saveArbitraryDay(){
Day day = new Day();
day.setDay(16);
day.setMonth(04);
day.setYear(1991);
//Set the jsonb objects
//You can use custom constructors whatever
Notification notification = new Notification();
notification.setEmail("contoso@hotmail.com");
day.setNotification(notification);
//Now putting activities
List<Activity> activities = new ArrayList<>();
Activity actOne = new Activity();
actOne.setName("Breakfast");
actOne.setEmoji("