Now I learn about nested jsons and geojson and how to save them in PostgreSQL as JsonB directly in database and before starting.
Inspiration source: https://thorben-janssen.com/persist-postgresqls-jsonb-data-type-hibernate/
Error I get when running the app:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is org.hibernate.boot.registry.classloading.spi.ClassLoadingException: Unable to load class [FeatureType]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1154) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:908) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.2.jar:2.7.2]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) ~[spring-boot-2.7.2.jar:2.7.2]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) ~[spring-boot-2.7.2.jar:2.7.2]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-2.7.2.jar:2.7.2]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-2.7.2.jar:2.7.2]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-2.7.2.jar:2.7.2]
at com.example.GeojsonTest.GeojsonTestApplication.main(GeojsonTestApplication.java:10) ~[classes/:na]
Caused by: org.hibernate.boot.registry.classloading.spi.ClassLoadingException: Unable to load class [FeatureType]
at org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl.classForName(ClassLoaderServiceImpl.java:133) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.boot.internal.ClassLoaderAccessImpl.classForName(ClassLoaderAccessImpl.java:67) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.cfg.annotations.SimpleValueBinder.fillSimpleValue(SimpleValueBinder.java:536) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.cfg.SetSimpleValueTypeSecondPass.doSecondPass(SetSimpleValueTypeSecondPass.java:25) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1653) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1621) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:295) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1460) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1494) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58) ~[spring-orm-5.3.22.jar:5.3.22]
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) ~[spring-orm-5.3.22.jar:5.3.22]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-5.3.22.jar:5.3.22]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-5.3.22.jar:5.3.22]
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341) ~[spring-orm-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.22.jar:5.3.22]
... 16 common frames omitted
Caused by: java.lang.ClassNotFoundException: Could not load requested class : FeatureType
at org.hibernate.boot.registry.classloading.internal.AggregatedClassLoader.findClass(AggregatedClassLoader.java:210) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:587) ~[na:na]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) ~[na:na]
at java.base/java.lang.Class.forName0(Native Method) ~[na:na]
at java.base/java.lang.Class.forName(Class.java:467) ~[na:na]
at org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl.classForName(ClassLoaderServiceImpl.java:130) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
... 31 common frames omitted
Here is the Main Entity:
@Getter
@Setter
@ToString
@RequiredArgsConstructor
@Entity
public class Geojson {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private int id;
@Column(name = "type")
private String type;
@Column(name = "feature")
@Type(type = "FeatureType")
private Feature features;
Here is the geojson(nested json) I want to save as Jsonb in a column in database:
@Getter
@Setter
@ToString
@RequiredArgsConstructor
public class Feature implements Serializable {
private String type;
private Properties properties;
private Geometry geometry;
}
Here is the implementation of Usertype for my json:
package com.example.GeojsonTest.Entity;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hibernate.HibernateException;
import org.hibernate.annotations.TypeDef;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.UserType;
import java.io.*;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
@TypeDef(name = "FeatureType", typeClass = FeatureType.class)
public class FeatureType implements UserType {
@Override
public int[] sqlTypes() {
return new int[]{Types.JAVA_OBJECT};
}
@Override
public Class<Feature> returnedClass() {
return Feature.class;
}
@Override
public Object nullSafeGet(final ResultSet resultSet, final String[] strings, final SharedSessionContractImplementor sharedSessionContractImplementor,
final Object o) throws HibernateException, SQLException {
final String cellContent = resultSet.getString(strings[0]);
if (cellContent == null) {
return null;
}
try {
final ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(cellContent.getBytes("UTF-8"), returnedClass());
} catch (final Exception ex) {
throw new RuntimeException("Failed to convert String to Invoice: " + ex.getMessage(), ex);
}
}
@Override
public void nullSafeSet(final PreparedStatement preparedStatement, final Object o, final int idx,
final SharedSessionContractImplementor sharedSessionContractImplementor) throws HibernateException, SQLException {
if (o == null) {
preparedStatement.setNull(idx, Types.OTHER);
return;
}
try {
final ObjectMapper mapper = new ObjectMapper();
final StringWriter w = new StringWriter();
mapper.writeValue(w, o);
w.flush();
preparedStatement.setObject(idx, w.toString(), Types.OTHER);
} catch (final Exception ex) {
throw new RuntimeException("Failed to convert Invoice to String: " + ex.getMessage(), ex);
}
}
@Override
public Object deepCopy(final Object value) throws HibernateException {
try {
// use serialization to create a deep copy
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(value);
oos.flush();
oos.close();
bos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(bos.toByteArray());
return new ObjectInputStream(bais).readObject();
} catch (ClassNotFoundException | IOException ex) {
throw new HibernateException(ex);
}
}
@Override
public boolean isMutable() {
return true;
}
@Override
public Serializable disassemble(final Object value) throws HibernateException {
return (Serializable) this.deepCopy(value);
}
@Override
public Object assemble(final Serializable cached, final Object owner) throws HibernateException {
return this.deepCopy(cached);
}
@Override
public Object replace(final Object original, final Object target, final Object owner) throws HibernateException {
return this.deepCopy(original);
}
@Override
public boolean equals(final Object obj1, final Object obj2) throws HibernateException {
if (obj1 == null) {
return obj2 == null;
}
return obj1.equals(obj2);
}
@Override
public int hashCode(final Object obj) throws HibernateException {
return obj.hashCode();
}
}
here is the registration of hibernate dialect:
public class MyPostgreSQL94Dialect extends PostgreSQL94Dialect {
public MyPostgreSQL94Dialect() {
this.registerColumnType(Types.JAVA_OBJECT, "jsonb");
}
}
App.properties:
#postgres database config
#spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
#spring.jpa.hibernate.ddl-auto=create
spring.jpa.generate-ddl=true
spring.jpa.hibernate.show-sql=true
spring.datasource.url=jdbc:postgresql://localhost:5432/geojson_copy
spring.datasource.username=postgres
spring.datasource.password=********
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL81Dialect
and finally, here is the POM.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>GeojsonTest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>GeojsonTest</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20220320</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
I don’t understand why i get the error about the FeatureType, if anyone can help me. Thank you!
3
Answers
I found an way easier solution working for hibernate 5 or higher:
Found a different approach that worked as expected:
Base Entity:
Main entity:
Geojson(nested json) I want to save as Jsonb in a column in database:
I'm not sure yet why I was not able to use @TypeDefs directly on "Geojson" class, and i needed to create a BaseEntity, but it works.
PS: If you tried storing some other type of data before, please make sure that you drop the table and create a new one before running the app, since the table was created with a different variable type for that column.
You need to have @NoArgsConstructor also in your beans. This is required for jackson.
You have to put the
@TypeDef(name = "FeatureType", typeClass = FeatureType.class)
part on a class that is scanned by Hibernate i.e. an entity class.