Spring Data Jdbc and Oracle21c - spring-boot

for my latest assignment I'm developing a Spring boot application which will connect with an Oracle 21c database.
The feature of the oracle release we're interested in is the native JSON data type called OSON (reference here: Oracle 21c JSON data type )
I've developed an old fashion DAO approach to accomplish the task, but I would like to use Spring Data JDBC project for the data access layer ideally with minimal extra configuration.
Actually I'm struggling with the mapping of the columns where the OSON type will be stored. After several tries I've obtained the error below following the idea of creating a custom converter for the datatype.
Any suggestion on how to proceed?
pom:
<!-- ORACLE -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11-production</artifactId>
<version>21.1.0.0</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
Entity class:
#Table("T_BUDGET")
#Data #NoArgsConstructor
public class BudgetEntityData {
#Id
private Long id;
#Column("BUDGET")
private JsonObjectWrapper budget;
}
Wrapper used for the converter:
#Data
public class JsonObjectWrapper {
private OracleJsonValue json;
}
Jdbc configuration with custom converter:
#Configuration
#EnableJdbcRepositories
public class JdbcConfig extends AbstractJdbcConfiguration {
//utility object used to centralize the use of OracleJsonFactory, not involved in the problem
private static OracleJsonFactoryWrapper factoryWrapper = new OracleJsonFactoryWrapper(new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false),
new OracleJsonFactory());
#Override
public JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(Arrays.asList(StringToJsonObjectWrapper.INSTANCE,JsonObjectWrapperToString.INSTANCE));
}
#WritingConverter
enum JsonObjectWrapperToString implements Converter<JsonObjectWrapper, String> {
INSTANCE;
#Override
public String convert(JsonObjectWrapper source) {
return source.toString();
}
}
#ReadingConverter
enum StringToJsonObjectWrapper implements Converter<String, JsonObjectWrapper> {
INSTANCE;
#Override
public JsonObjectWrapper convert(String source) {
JsonObjectWrapper jsonObjectWrapper = new JsonObjectWrapper();
OracleJsonValue osonObject = factoryWrapper.createOsonObject(source);
jsonObjectWrapper.setJson(osonObject);
return jsonObjectWrapper;
}
}
}
Error:
2022-04-07 09:47:27.335 DEBUG 24220 --- [nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL query
2022-04-07 09:47:27.335 DEBUG 24220 --- [nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [SELECT "T_BUDGET"."ID" AS "ID", "T_BUDGET"."BUDGET" AS "BUDGET" FROM "T_BUDGET"]
2022-04-07 09:48:58.006 ERROR 24220 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.mapping.MappingException: Could not read value BUDGET from result set!] with root cause
java.sql.SQLException: Invalid column type: getOracleObject not
implemented for class oracle.jdbc.driver.T4CJsonAccessor at
oracle.jdbc.driver.GeneratedAccessor.getOracleObject(GeneratedAccessor.java:1221)
~[ojdbc11-21.1.0.0.jar:21.1.0.0.0] at
oracle.jdbc.driver.JsonAccessor.getObject(JsonAccessor.java:200)
~[ojdbc11-21.1.0.0.jar:21.1.0.0.0] at
oracle.jdbc.driver.GeneratedStatement.getObject(GeneratedStatement.java:196)
~[ojdbc11-21.1.0.0.jar:21.1.0.0.0] at
oracle.jdbc.driver.GeneratedScrollableResultSet.getObject(GeneratedScrollableResultSet.java:334)
~[ojdbc11-21.1.0.0.jar:21.1.0.0.0] at
com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java)
~[HikariCP-3.4.5.jar:na] at
org.springframework.jdbc.support.JdbcUtils.getResultSetValue(JdbcUtils.java:283)
~[spring-jdbc-5.3.8.jar:5.3.8]

I had the very same issue. I fixed it with the RowMapper like this:
Create DTO for the JSON content (JsonObjectWrapper in your case)
#Getter
#Setter
public class JsonContent {
private String code;
private String name;
}
Create entity (BudgetEntityData in your case)
#Data
#Relation( collectionRelation = "persons" )
public class Person {
#Id
private Long id;
private JsonContent content;
}
Create custom RowMapper (probably JdbcConfig in your case)
public class PersonMapper implements RowMapper<Person> {
static ObjectMapper objectMapper = new ObjectMapper();
#Override
public Person mapRow( ResultSet rs, int rowNum ) throws SQLException {
try {
var jsonContent = rs.getBytes( 2 );
var content = objectMapper.readValue( jsonContent, JsonContent.class );
var person = new Person();
person.setId( rs.getLong( 1 ) );
person.setContent( content );
return person;
} catch ( IOException e ) {
throw new RuntimeException( "JSON unmarschalling failed!", e );
}
}
}
Use it in repository (not mentioned in your case) as
#Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
#Query( value = "SELECT id, ctnt FROM PERSON", rowMapperClass = PersonMapper.class )
#Override
List<Person> findAll();
}
Note: you can even simplify it with the spring-data-jpa as:
Define entity
#Entity
#Table( name = Person.TABLE_NAME )
#Data
#Relation( collectionRelation = "persons" )
public class Person {
static final String TABLE_NAME = "PERSON";
#Id
#GeneratedValue
private Long id;
#Column( name = "CTNT" )
#Convert( converter = JsonContentConverter.class )
private JsonContent content;
}
And the converter
public class JsonContentConverter implements AttributeConverter<JsonContent, byte[]> {
static ObjectMapper objectMapper = new ObjectMapper();
#Override
public byte[] convertToDatabaseColumn( JsonContent attribute ) {
try {
return objectMapper.writeValueAsBytes( attribute );
} catch ( JsonProcessingException e ) {
throw new RuntimeException( "JSON marschalling failed!", e );
}
}
#Override
public JsonContent convertToEntityAttribute( byte[] jsonContent ) {
try {
return objectMapper.readValue( jsonContent, JsonContent.class );
} catch ( IOException e ) {
throw new RuntimeException( "JSON unmarschalling failed!", e );
}
}
}

Related

Trying to insert Json into Neo4j

Everyone I am new to neo4j and I am trying to enter Json into Neo4j but I am getting Match statement instead of create. Earlier I tried something myself and when When I inserted Json message only as
{"name":"john","dept":"Science"}
it went without a glitch but everytime I want try to add numeric data it gets error.
2020-03-10 13:21:59.793 INFO 94817 --- [ntainer#0-0-C-1] o.n.o.drivers.http.request.HttpRequest : Thread:
29, url: http://localhost:7474/db/data/transaction/92, request: {"statements":[{"statement":"UNWIND {rows}
as row **MATCH** (n) WHERE ID(n)=row.nodeId SET n:`UsersInfo` SET n += row.props RETURN row.nodeId as ref,
ID(n) as id, {type} as type","parameters":{"type":"node","rows":[{"nodeId":23,"props":{"name":"raj",
"dept":"science","age":11}}]},"resultDataContents":["row"],"includeStats":false}]}
These are my classes
KafkaConfiguration
#EnableKafka
#Configuration
public class KafkaConfiguration {
#Bean
public ConsumerFactory<String, Users> userConsumerFactory(){
Map<String, Object> config = new HashMap<>();
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
config.put(ConsumerConfig.GROUP_ID_CONFIG, "group_json");
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
return new DefaultKafkaConsumerFactory<>(config, new StringDeserializer(),
new JsonDeserializer<>(Users.class));
}
#Bean
public ConcurrentKafkaListenerContainerFactory<String, Users> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Users> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(userConsumerFactory());
return factory;
}
}
KafkaConsumer class
Service
public class KafkaConsumer {
#Autowired
public Neo4jservice neo4jService;
#KafkaListener(topics = "UsersJson", groupId = "group_id", containerFactory = "kafkaListenerContainerFactory")
public void consume(Users users) {
System.out.println("Consumed message: " + users);
UsersInfo usern = new UsersInfo();
usern.setAge(users.getAge());
usern.setDept(users.getDept());
usern.setId(users.getId());
usern.setName(users.getName());
neo4jService.saveIntoStudentsTable(usern);
}
}
Neo4jService
#Service
public class Neo4jservice {
#Autowired
private UsersRepo userRepo;
public UsersInfo saveIntoStudentsTable(UsersInfo users) {
UsersInfo usern = userRepo.save(users);
return (usern);
}
}
UsersRepo
#Repository
public interface UsersRepo extends Neo4jRepository<UsersInfo, Long>{
}
Users class
public class Users {
private Long id;
private String name;
private String dept;
private Integer age;
**getters,setters and toString method here**
}
Likewise UsersInfo class
#NodeEntity
public class Users {
#Id
private Long id;
private String name;
private String dept;
private Integer age;
**getters,setters and toString method here**
}
Any help will be greatly appreciated. Thanks
You are setting also the id value of the User class.
This will make Spring Data Neo4j and the Neo4j Object Graph Mapper that is used for the persistence think that the entity already exists.
In this case it will MATCH on an existing id(n) and update the properties as you can see in the logs instead of CREATE a new node.

List all DB tables - JPA

I want to list all the tables in my DB using Spring boot and JPA, I have created a DataSource configuration like - configuring-spring-boot-for-oracle and tried -
#Repository
public interface TestRepo extends JpaRepository<Table, Long>{
#Query("SELECT owner, table_name FROM dba_tables")
List<Table> findAllDB();
}
And my Table Entity -
#Entity
public class Table {
String owner;
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
}
And got -
No identifier specified for entity: com.siemens.plm.it.aws.connect.repos.Table
So how do I query for DB tables names?
So far my main -
#SpringBootApplication
public class AwsFileUploadApplication implements CommandLineRunner{
#Autowired
DataSource dataSource;
#Autowired
TestRepo repo;
public static void main(String[] args) {
//https://wwwtest.plm.automation.siemens.com/subsadmin/app/products
SpringApplication.run(AwsFileUploadApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
System.out.println("DATASOURCE = " + dataSource); //some value - ds init sucess
List<Table> findAllDB = repo.findAllDB();
System.out.println(findAllDB);
}
}
When removing #Entity from Table - Not a managed type: class com.siemens.plm.it.aws.connect.repos.Table.
I print all my tables and columns using JDBC:
#Autowired
protected DataSource dataSource;
public void showTables() throws Exception {
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
ResultSet tables = metaData.getTables(null, null, null, new String[] { "TABLE" });
while (tables.next()) {
String tableName=tables.getString("TABLE_NAME");
System.out.println(tableName);
ResultSet columns = metaData.getColumns(null, null, tableName, "%");
while (columns.next()) {
String columnName=columns.getString("COLUMN_NAME");
System.out.println("\t" + columnName);
}
}
}
If you look at the getTables API you can see how to refine the search of tables.

Ignite : select query returns null

I am new to ignite , I am trying to fetch data using ignite repository but below query returns 'null'.
my repository
#Component
#RepositoryConfig(cacheName = "UserCache")
#Repository
public interface UserRepository extends IgniteRepository<UserEntity, Long> {
#Query("select a.* from UserEntity a where a.lastname=? ")
UserEntity selectUserlastName(String plastName);
My cache configuration as
CacheConfiguration<Long, UserEntity> lUserCacheConfig =
createCacheConfigurationStore("UserCache", UserCacheStore.class);
CacheJdbcPojoStoreFactory<Long, UserEntity> lUserJdbcStoreFactory = new
CacheJdbcPojoStoreFactory<>();
UserJdbcPojoStoreFactory<? super Long, ? super UserEntity>
lUserJdbcPojoStoreFactory = new UserJdbcPojoStoreFactory<>();
lUserJdbcStoreFactory.setDataSource(datasource);
lUserJdbcStoreFactory.setDialect(new OracleDialect());
lUserJdbcStoreFactory.setTypes(lUserJdbcPojoStoreFactory.
configJdbcContactType());
lUserCacheConfig.setCacheStoreFactory(lUserJdbcStoreFactory);
// Configure Cache..
cfg.setCacheConfiguration(lUserCacheConfig);
My PojoStore is as below:
public class UserJdbcPojoStoreFactory<K, V> extends
AnstractJdbcPojoStoreFactory<Long, UserEntity> {
private static final long serialVersionUID = 1L;
#Autowired
DataSource datasource;
#Override
public CacheJdbcPojoStore<Long, UserEntity> create() {
// TODO Auto-generated method stub
setDataSource(datasource);
return super.create();
}
#Override
public JdbcType configJdbcContactType() {
JdbcType jdbcContactType = new JdbcType();
jdbcContactType.setCacheName("UserCache");
jdbcContactType.setKeyType(Long.class);
jdbcContactType.setValueType(UserEntity.class);
jdbcContactType.setDatabaseTable("USER");
jdbcContactType.setDatabaseSchema("ORGNITATION");
jdbcContactType.setKeyFields(new JdbcTypeField(Types.INTEGER, "id",
Long.class, "id"));
jdbcContactType.setValueFields(
new JdbcTypeField(Types.VARCHAR, "NAME", String.class, "NAME"), //
new JdbcTypeField(Types.VARCHAR, "LASTNAME", String.class, "lastname"),
//
return jdbcContactType;
}
}
Please suggest ..
Please check that #Query annotation imported from ignite-spring-data library and test your query using SqlFieldsQuery.

Spring Boot class cast exception in PostConstruct method

I am running a Spring Boot application with a PostConstruct method to populate a POJO before application initialization. This is to ensure that the database isn't hit by multiple requests to get the POJO content after it starts running.
I'm able to pull the data from Oracle database through Hibernate query and store it in my POJO. The problem arises when I try to access the stored data. The dataset contains a list of objects that contain strings and numbers. Just trying to print the description of the object at the top of the list raises a class cast exception. How should I mitigate this issue?
#Autowired
private TaskDescrBean taskBean;
#PostConstruct
public void loadDescriptions() {
TaskDataLoader taskData = new TaskDataLoader(taskBean.acquireDataSourceParams());
List<TaskDescription> taskList = tdf.getTaskDescription();
taskBean.setTaskDescriptionList(taskList);
System.out.println("Task description size: " + taskBean.getTaskDescriptionList().get(0).getTaskDescription());
}
My POJO class:
#Component
public class TaskDescrBean implements ApplicationContextAware {
#Resource
private Environment environment;
protected List<TaskDescription> taskDescriptionList;
public Properties acquireDataSourceParams() {
Properties dataSource = new Properties();
dataSource.setProperty("hibernate.connection.driver_class", environment.getProperty("spring.datasource.driver-class-name"));
dataSource.setProperty("hibernate.connection.url", environment.getProperty("spring.datasource.url"));
dataSource.setProperty("hibernate.connection.username", environment.getProperty("spring.datasource.username"));
dataSource.setProperty("hibernate.connection.password", environment.getProperty("spring.datasource.password"));
return dataSource;
}
public List<TaskDescription> getTaskDescriptionList() {
return taskDescriptionList;
}
public void setTaskDescriptionList(List<TaskDescription> taskDescriptionList) {
this.taskDescriptionList = taskDescriptionList;
}
public ApplicationContext getApplicationContext() {
return applicationContext;
}
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}
My DAO class:
public class TaskDataLoader {
private Session session;
private SessionFactory sessionFactory;
public TaskDataLoader(Properties connectionProperties) {
Configuration config = new Configuration().setProperties(connectionProperties);
config.addAnnotatedClass(TaskDescription.class);
sessionFactory = config.buildSessionFactory();
}
#SuppressWarnings("unchecked")
public List<TaskDescription> getTaskDescription() {
List<TaskDescription> taskList = null;
session = sessionFactory.openSession();
try {
String description = "from TaskDescription des";
Query taskDescriptionQuery = session.createQuery(description);
taskList = taskDescriptionQuery.list();
System.out.println("Task description fetched. " + taskList.getClass());
} catch (Exception e) {
e.printStackTrace();
} finally {
session.close();
}
return taskList;
}
TaskDescription Entity:
#Entity
#Table(name="TASK_DESCRIPTION")
#JsonIgnoreProperties
public class TaskDescription implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name="TASK_DESCRIPTION_ID")
private Long taskDescriptionId;
#Column(name="TASK_DESCRIPTION")
private String taskDescription;
public Long getTaskDescriptionId() {
return taskDescriptionId;
}
public void setTaskDescriptionId(Long taskDescriptionId) {
this.taskDescriptionId = taskDescriptionId;
}
public String getTaskDescription() {
return taskDescription;
}
public void setTaskDescription(String taskDescription) {
this.taskDescription = taskDescription;
}
}
StackTrace
Instead of sending the List in the return statement, I transformed it into a JSON object and sent its String representation which I mapped back to the Object after transforming it using mapper.readValue()

How to POST nested entities with Spring Data REST

I'm building a Spring Data REST application and I'm having some problems when I try to POST it. The main entity has other two related entities nested.
There is a "questionary" object which has many answers and each one of these answers have many replies.
I generate a JSON like this from the front application to POST the questionary:
{
"user": "http://localhost:8080/users/1",
"status": 1,
"answers": [
{
"img": "urlOfImg",
"question": "http://localhost:8080/question/6",
"replies": [
{
"literal": "http://localhost:8080/literal/1",
"result": "6"
},
{
"literal": "http://localhost:8080/literal/1",
"result": "6"
}
]
},
{
"img": "urlOfImg",
"question": "http://localhost:8080/question/6",
"replies": [
{
"literal": "http://localhost:8080/literal/3",
"result": "10"
}
]
}
]
}
But when I try to post it, I get the follow error response:
{
"cause" : {
"cause" : {
"cause" : null,
"message" : "Template must not be null or empty!"
},
"message" : "Template must not be null or empty! (through reference chain: project.models.Questionary[\"answers\"])"
},
"message" : "Could not read JSON: Template must not be null or empty! (through reference chain: project.models.Questionary[\"answers\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: project.models.Questionary[\"answers\"])"
}
Edit:
I also add my repository:
#RepositoryRestResource(collectionResourceRel = "questionaries", path = "questionaries")
public interface InspeccionRepository extends JpaRepository<Inspeccion, Integer> {
#RestResource(rel="byUser", path="byUser")
public List<Questionary> findByUser (#Param("user") User user);
}
My Entity Questionary class is :
#Entity #Table(name="QUESTIONARY", schema="enco" )
public class Questionary implements Serializable {
private static final long serialVersionUID = 1L;
//----------------------------------------------------------------------
// ENTITY PRIMARY KEY ( BASED ON A SINGLE FIELD )
//----------------------------------------------------------------------
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEC_QUESTIONARY")
#SequenceGenerator(name = "SEC_QUESTIONARY", sequenceName = "ENCO.SEC_QUESTIONARY", allocationSize = 1)
#Column(name="IDQUES", nullable=false)
private Integer idques ;
//----------------------------------------------------------------------
// ENTITY DATA FIELDS
//----------------------------------------------------------------------
#Column(name="ESTATUS")
private Integer estatus ;
//----------------------------------------------------------------------
// ENTITY LINKS ( RELATIONSHIP )
//----------------------------------------------------------------------
#ManyToOne
#JoinColumn(name="IDUSER", referencedColumnName="IDUSER")
private User user;
#OneToMany(mappedBy="questionary", targetEntity=Answer.class)
private List<Answer> answers;
//----------------------------------------------------------------------
// CONSTRUCTOR(S)
//----------------------------------------------------------------------
public Questionary()
{
super();
}
//----------------------------------------------------------------------
// GETTERS & SETTERS FOR FIELDS
//----------------------------------------------------------------------
//--- DATABASE MAPPING : IDNSE ( NUMBER )
public void setIdnse( Integer idnse )
{
this.idnse = idnse;
}
public Integer getIdnse()
{
return this.idnse;
}
//--- DATABASE MAPPING : ESTADO ( NUMBER )
public void setEstatus Integer estatus )
{
this.estatus = estatus;
}
public Integer getEstatus()
{
return this.estatus;
}
//----------------------------------------------------------------------
// GETTERS & SETTERS FOR LINKS
//----------------------------------------------------------------------
public void setUser( Usuario user )
{
this.user = user;
}
public User getUser()
{
return this.user;
}
public void setAnswers( List<Respuesta> answers )
{
this.answers = answer;
}
public List<Answer> getAnswers()
{
return this.answers;
}
// Get Complete Object method public List<Answer>
getAnswerComplete() {
List<Answer> answers = this.answers;
return answers;
}
}
My Answer Entity:
#Entity #Table(name="ANSWER", schema="enco" ) public class Answer
implements Serializable {
private static final long serialVersionUID = 1L;
//----------------------------------------------------------------------
// ENTITY PRIMARY KEY ( BASED ON A SINGLE FIELD )
//----------------------------------------------------------------------
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEC_ANSWER")
#SequenceGenerator(name = "SEC_ANSWER", sequenceName = "ENCOADMIN.SEC_ANSWER", allocationSize = 1)
#Column(name="IDANS", nullable=false)
private Integer idans ;
//----------------------------------------------------------------------
// ENTITY DATA FIELDS
//----------------------------------------------------------------------
#Column(name="IMG", length=100)
private String img ;
//----------------------------------------------------------------------
// ENTITY LINKS ( RELATIONSHIP )
//----------------------------------------------------------------------
#ManyToOne
#JoinColumn(name="IDQUES", referencedColumnName="IDQUES")
private Questionary questionary ;
#OneToMany(mappedBy="answer", targetEntity=Reply.class)
private List<Reply> replies;
#ManyToOne
#JoinColumn(name="IDQUE", referencedColumnName="IDQUE")
private Question Question ;
//----------------------------------------------------------------------
// CONSTRUCTOR(S)
//----------------------------------------------------------------------
public Answer()
{
super();
}
//----------------------------------------------------------------------
// GETTER & SETTER FOR THE KEY FIELD
//----------------------------------------------------------------------
public void setIdans( Integer idans )
{
this.idans = idans ;
}
public Integer getIdans()
{
return this.idans;
}
//----------------------------------------------------------------------
// GETTERS & SETTERS FOR FIELDS
//----------------------------------------------------------------------
//--- DATABASE MAPPING : IMAGEN ( VARCHAR2 )
public void setImg( String img )
{
this.img = img;
}
public String getImg()
{
return this.img;
}
//----------------------------------------------------------------------
// GETTERS & SETTERS FOR LINKS
//----------------------------------------------------------------------
public void setQuestionary( Questionary questionary )
{
this.questionary = questionary;
}
public Questionary getQuestionary()
{
return this.questionary;
}
public void setReplies( List<Reply> contestaciones )
{
this.replies = replies;
}
public List<Reply> getReplies()
{
return this.replies;
}
public void setQuestion( Question question )
{
this.question = question;
}
public Question getQuestion()
{
return this.question;
}
}
And this is the error console:
Caused by: com.fasterxml.jackson.databind.JsonMappingException:
Template must not be null or empty! (through reference chain:
project.models.Questionary["answers"]) at
com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:232)
~[jackson-databind-2.3.3.jar:2.3.3] at *snip*
Try adding #RestResource(exported = false) on field answers in class Questionary.
According to me, this error occurs because the deserializer expects URIs to fetch the answers from, instead of having the answers nested in the JSON. Adding the annotation tells it to look in JSON instead.
I'm still seeing this error with 2.3.0.M1, but I finally found a workaround.
The basic issue is this: If you post the url of the embedded entity in the JSON, it works. If you post the actual embedded entity JSON, it doesn't. It tries to deserialize the entity JSON into a URI, which of course fails.
It looks like the issue is with the two TypeConstrainedMappingJackson2HttpMessageConverter objects that spring data rest creates in its configuration (in RepositoryRestMvcConfiguration.defaultMessageConverters()).
I finally got around the issue by configuring the supported media types of the messageConverters so that it skips those two and hits the plain MappingJackson2HttpMessageConverter, which works fine with nested entities.
For example, if you extend RepositoryRestMvcConfiguration and add this method, then when you send a request with content-type of 'application/json', it will hit the plain MappingJackson2HttpMessageConverter instead of trying to deserialize into URIs:
#Override
public void configureHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
((MappingJackson2HttpMessageConverter) messageConverters.get(0))
.setSupportedMediaTypes(asList(MediaTypes.HAL_JSON));
((MappingJackson2HttpMessageConverter) messageConverters.get(2))
.setSupportedMediaTypes(asList(MediaType.APPLICATION_JSON));
}
That configures the message converters produced by defaultMessageConverters() in RepositoryRestMvcConfiguration.
Keep in mind that the plain objectMapper can't handle URIs in the JSON - you'll still need to hit one of the two preconfigured message converters any time you pass URIs of embedded entities.
One issue with your JSON is that you are trying to deserialize a string as a question:
"question": "http://localhost:8080/question/6"
In your Answer object, Jackson is expecting an object for question. It appears that you are using URLs for IDs, so instead of a string you need to pass something like this for your question:
"question": {
"id": "http://localhost:8080/question/6"
}
Try to update "Spring Boot Data REST Starter" library. Worked for me.
With Spring Boot 2.7.2 it is achievable with the following config (accepts both links and entities in the request bodies):
package com.my.project.config;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.CreatorProperty;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.support.EntityLookup;
import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module;
import org.springframework.data.rest.webmvc.mapping.Associations;
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
import org.springframework.data.rest.webmvc.support.ExcerptProjector;
import org.springframework.data.util.StreamUtils;
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.ReflectionUtils;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static com.fasterxml.jackson.core.JsonToken.START_OBJECT;
// Allows POST'ing nested objects and not only links
#Configuration
public class CustomRepositoryRestMvcConfiguration implements RepositoryRestConfigurer {
private final ApplicationContext context;
private final PersistentEntities entities;
private final RepositoryInvokerFactory invokerFactory;
private final Repositories repositories;
private final Associations associations;
private final ExcerptProjector projector;
private final ObjectProvider<RepresentationModelProcessorInvoker> modelInvoker;
private final LinkCollector linkCollector;
private final RepositoryRestConfiguration repositoryRestConfiguration;
public CustomRepositoryRestMvcConfiguration(
ApplicationContext context,
PersistentEntities entities,
#Lazy RepositoryInvokerFactory invokerFactory,
Repositories repositories,
#Lazy Associations associations,
#Lazy ExcerptProjector projector,
#Lazy ObjectProvider<RepresentationModelProcessorInvoker> modelInvoker,
#Lazy LinkCollector linkCollector,
#Lazy RepositoryRestConfiguration repositoryRestConfiguration) {
this.context = context;
this.entities = entities;
this.invokerFactory = invokerFactory;
this.repositories = repositories;
this.associations = associations;
this.projector = projector;
this.modelInvoker = modelInvoker;
this.linkCollector = linkCollector;
this.repositoryRestConfiguration = repositoryRestConfiguration;
}
#Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
objectMapper.registerModule(persistentEntityJackson2Module(linkCollector));
}
protected Module persistentEntityJackson2Module(LinkCollector linkCollector) {
List<EntityLookup<?>> lookups = new ArrayList<>();
lookups.addAll(repositoryRestConfiguration.getEntityLookups(repositories));
lookups.addAll((Collection) beansOfType(context, EntityLookup.class).get());
EmbeddedResourcesAssembler assembler = new EmbeddedResourcesAssembler(entities, associations, projector);
PersistentEntityJackson2Module.LookupObjectSerializer lookupObjectSerializer = new PersistentEntityJackson2Module.LookupObjectSerializer(PluginRegistry.of(lookups));
// AssociationUriResolvingDeserializerModifier delegates
return new NestedSupportPersistentEntityJackson2Module(associations,
entities,
new UriToEntityConverter(entities, invokerFactory, repositories),
linkCollector,
invokerFactory,
lookupObjectSerializer,
modelInvoker.getObject(),
assembler
);
}
public static class NestedSupportPersistentEntityJackson2Module extends PersistentEntityJackson2Module {
public NestedSupportPersistentEntityJackson2Module(Associations associations,
PersistentEntities entities,
UriToEntityConverter converter,
LinkCollector collector,
RepositoryInvokerFactory factory,
LookupObjectSerializer lookupObjectSerializer,
RepresentationModelProcessorInvoker invoker,
EmbeddedResourcesAssembler assembler) {
super(associations, entities, converter, collector, factory, lookupObjectSerializer, invoker, assembler);
}
#Override
public SimpleModule setDeserializerModifier(BeanDeserializerModifier mod) {
super.setDeserializerModifier(new NestedObjectSuppAssociationUriResolvingDeserializerModifier(
(PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier) mod)
);
return this;
}
}
#RequiredArgsConstructor
public static class NestedObjectSuppAssociationUriResolvingDeserializerModifier extends BeanDeserializerModifier {
private final PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier uriDelegate;
#SneakyThrows
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config,
BeanDescription beanDesc,
BeanDeserializerBuilder builder) {
// Pushes Uri* deserializer
uriDelegate.updateBuilder(config, beanDesc, builder);
// Replace Uri* deserializers with delegates
var customizer = new ValueInstantiatorCustomizer(builder.getValueInstantiator(), config);
var properties = builder.getProperties();
while (properties.hasNext()) {
var prop = properties.next();
if (!prop.hasValueDeserializer()) {
continue;
}
if (prop.getValueDeserializer() instanceof PersistentEntityJackson2Module.UriStringDeserializer) {
customizer.replacePropertyIfNeeded(
builder,
prop.withValueDeserializer(new ObjectOrUriStringDeserializer(
prop.getValueDeserializer().handledType(),
prop.getValueDeserializer(),
new LateDelegatingDeser(prop.getType())
))
);
}
if ((Object) prop.getValueDeserializer() instanceof CollectionDeserializer) {
var collDeser = (CollectionDeserializer) ((Object) prop.getValueDeserializer());
if (!(collDeser.getContentDeserializer() instanceof PersistentEntityJackson2Module.UriStringDeserializer)) {
continue;
}
customizer.replacePropertyIfNeeded(
builder,
prop.withValueDeserializer(
new CollectionDeserializer(
collDeser.getValueType(),
new ObjectOrUriStringDeserializer(
prop.getValueDeserializer().handledType(),
((CollectionDeserializer) (Object) prop.getValueDeserializer()).getContentDeserializer(),
new LateDelegatingDeser(prop.getType().getContentType())
),
null,
collDeser.getValueInstantiator()
)
)
);
}
}
return customizer.conclude(builder);
}
#Getter
#RequiredArgsConstructor
public static class LateDelegatingDeser extends JsonDeserializer<Object> {
private final JavaType type;
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
return ctxt.findNonContextualValueDeserializer(type).deserialize(p, ctxt);
}
}
}
public static class ObjectOrUriStringDeserializer extends StdDeserializer<Object> {
private final JsonDeserializer<Object> uriDelegate;
private final JsonDeserializer<Object> vanillaDelegate;
public ObjectOrUriStringDeserializer(Class<?> type, JsonDeserializer<Object> uriDelegate, JsonDeserializer<Object> vanillaDelegate) {
super(type);
this.uriDelegate = uriDelegate;
this.vanillaDelegate = vanillaDelegate;
}
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JacksonException {
if (START_OBJECT == jp.getCurrentToken()) {
return vanillaDelegate.deserialize(jp, ctxt);
}
return uriDelegate.deserialize(jp, ctxt);
}
}
// Copied from original ValueInstantiatorCustomizer
public static class ValueInstantiatorCustomizer {
private final SettableBeanProperty[] properties;
private final StdValueInstantiator instantiator;
ValueInstantiatorCustomizer(ValueInstantiator instantiator, DeserializationConfig config) {
this.instantiator = StdValueInstantiator.class.isInstance(instantiator) //
? StdValueInstantiator.class.cast(instantiator) //
: null;
this.properties = this.instantiator == null || this.instantiator.getFromObjectArguments(config) == null //
? new SettableBeanProperty[0] //
: this.instantiator.getFromObjectArguments(config).clone(); //
}
/**
* Replaces the logically same property with the given {#link SettableBeanProperty} on the given
* {#link BeanDeserializerBuilder}. In case we get a {#link CreatorProperty} we also register that one to be later
* exposed via the {#link ValueInstantiator} backing the {#link BeanDeserializerBuilder}.
*
* #param builder must not be {#literal null}.
* #param property must not be {#literal null}.
*/
void replacePropertyIfNeeded(BeanDeserializerBuilder builder, SettableBeanProperty property) {
builder.addOrReplaceProperty(property, false);
if (!CreatorProperty.class.isInstance(property)) {
return;
}
properties[((CreatorProperty) property).getCreatorIndex()] = property;
}
/**
* Concludes the setup of the given {#link BeanDeserializerBuilder} by reflectively registering the potentially
* customized {#link SettableBeanProperty} instances in the {#link ValueInstantiator} backing the builder.
*
* #param builder must not be {#literal null}.
* #return
*/
BeanDeserializerBuilder conclude(BeanDeserializerBuilder builder) {
if (instantiator == null) {
return builder;
}
Field field = ReflectionUtils.findField(StdValueInstantiator.class, "_constructorArguments");
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, instantiator, properties);
builder.setValueInstantiator(instantiator);
return builder;
}
}
private static <S> org.springframework.data.util.Lazy<List<S>> beansOfType(ApplicationContext context, Class<?> type) {
return org.springframework.data.util.Lazy.of(() -> (List<S>) context.getBeanProvider(type)
.orderedStream()
.collect(StreamUtils.toUnmodifiableList()));
}
}
It is ugly, but it works. Don't forget about cascades and proper setters for entities, i.e. one must have for OneToMany:
public class DeliveryOrder {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private Collection<Delivery> deliveries;
public void setDeliveries(Collection<Delivery> deliveries) {
if (null != deliveries) {
deliveries.forEach(delivery -> delivery.setOrder(this));
}
this.deliveries = deliveries;
}
}

Resources