How to create a new entity with association - ajax

Let's say i have a form on the frontend with usual fields and dropdowns.
In those dropdowns user is able to select an option, and each option is linked to an entity in Spring data JPA;
Dropdowns contain some label and a link to corresponding entity as a value.
This value is then passed in a POST-request to a PagingAndSorting repository of an entity which we wish to create.
Let's say it's a user with username and he must be associated with one of the offices (Also an entity):
#Data
#Builder
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Table(name="users")
public class User{
#Id
#Coluemn(name="USER_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
#Column(name="USER_NAME", nullable=false)
private String userName;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="OFFICE_ID", **nullable=false**)
private Office office;
}
My first guess would be:
Sending POST-request to http://localhost:8080/api/users/
contentType:'application/json'
{"userName":"Anton","office":"http://localhost:8080/api/offices/1"}
But it throws an exception
{
"cause": {
"cause": null,
"message": "Cannot construct instance of `test.domain.Office` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/offices/1')\n at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 160] (through reference chain: test.domain.User[\"office\"])"
},
"message": "JSON parse error: Cannot construct instance of `test.domain.Office` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/offices/1'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `test.domain.Office` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/office/1')\n at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 160] (through reference chain: test.domain.User[\"office\"])"
}
What am i doing wrong?

You are sending a URL resource as a string in place of a JSON object and expecting some magic to happen between Spring and jackson to look up the value. Naturally this is not what is happening and Jackson is attempting to bind the string value of the URL to the Office field. This fails of course because it does not know how to create an Office object from a string.
A possible solution is to make a distinction between your Entity Objects (those which represent your database tables) and DTO's (Data Transfer Objects) which in this cause represent your contract with your client. When doing this you could receive a User Object like so:
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class User{
private Long userId;
private String userName;
private Long officeId;
}
Now you can simply send an office id instead of a URL and in your code use a Spring data repository to lookup the office object. After that you can then construct your Entity User object like the one you have shown above and persist it.

Turns out it was because i used Lombok, which generated it's own constructor.
To make it work i just needed to set #AllArgsConstructor like this:
#AllArgsConstructor(suppressConstructorProperties = true)
Now it works as i expected:
Json to send to http://localhost:8080/api/users:
{
"userName":"Anton",
"office":"http://localhost:8080/api/offices/1"
}
Which returns:
{
"userName":"Anton",
"_links": {
"self": {
"href": "http://localhost:8080/api/users/28"
},
"user": {
"href": "http://localhost:8080/api/users/28"
},
"office": {
"href": "http://localhost:8080/api/users/28/office"
}
}
}

Related

Failed parsing for a LocalDate value while using Spring Data Rest

I am still a newbie with Spring Data Rest and I'm having some issues with it while I have to parse a LocalDate value to an endpoint. I have searched info's in other topics too but I'm still stucked, this is the problem:
I have one Entity with this code .
#Entity
#Table(name="calendario_parcheggio")
#Setter
#Getter
public class CalendarioParcheggio {
#Id
#Column(name="data_parcheggio")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE )
#JsonFormat(pattern="yyyy-MM-dd")
private LocalDate data;
#Column(columnDefinition = "ENUM('ATTIVO', 'ARCHIVIATO')")
#Enumerated(EnumType.STRING)
private Stato stato;
#OneToMany(cascade=CascadeType.ALL)
#JoinColumn(name="data_parcheggio")
private List<Parcheggio> parcheggio;
public enum Stato {
ATTIVO,
ARCHIVIATO,
}
}
It's an Entity linking the Date and its status for a Parking that works hourly.Matching this table on MySQL
CREATE TABLE calendario_parcheggio (
data_parcheggio DATE PRIMARY KEY,
stato ENUM ('ATTIVO','ARCHIVIATO') NOT NULL DEFAULT ('ATTIVO')
);
When I start the server everything is ok , but when i try (by browser or Postman) to check the data of a particular instance (in my case : "http://localhost:8080/parkingsystem/api/calendario-parcheggio/2022-10-18") ,I get this problem :
{"cause":
{"cause":
{"cause": null,
"message": "Text '2022-10-18' could not be parsed at index 2"
},
"message": "Parse attempt failed for value [2022-10-18]"
},
"message": "Failed to convert from type [java.lang.String] to type [java.time.LocalDate] for value '2022-10-18';
nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2022-10-18]"
}
And this is the Repository
#RepositoryRestResource(collectionResourceRel="calendarioParcheggio", path="calendario-parcheggio")
public interface CalendarioParcheggioRepository extends JpaRepository<CalendarioParcheggio, LocalDate> {
}
Can you help me to find the solution please?I hope I have explained the problem well enough, my English is still in training :)

How to POST entity with relationships in REST endpoint - Kotlin Spring Data

I followed this tutorial to create a basic web app in Kotlin using Spring Boot. However, I fail to POST new entities with a many-to-one relationship to an existing resource.
My code:
#Entity
class Song(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
var title: String,
#ManyToOne(fetch = FetchType.EAGER)
var addedBy: User)
#Entity
class User(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
var email: String,
var displayName: String)
#RestController
#RequestMapping("/api/songs")
class SongController(private val repository: SongRepository) {
#PostMapping("/")
fun add(#RequestBody song: Song) =
repository.save(song)
This answer and others point out that you can reference another resource using its URI, but sending the following request:
{
"title": "Some title",
"addedBy": "http://localhost:8080/api/users/1"
}
gets me an errors with stack trace org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of 'com.example.springboot.User' (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/users/1'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of 'com.example.springboot.User' (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/users/1')\n at [Source: (PushbackInputStream); line: 6, column: 13] (through reference chain: com.example.springboot.Song[\"addedBy\"])
I got out of this that somewhere between Jackson/Hibernate/Spring Data it fails to convert the User resource URI into a User entity, but I'm in the dark where this magic should happen.
It seems to be an issue that occurs with Kotlin specifically. All the suggestions here on SO do not solve this specific error and the tutorial itself stops just short of dealing with relationships. If it's not the right approach at all to handle relationships this way I'd be eager to know what the preferred practice would be.
The tutorial is using HATEOAS. See the request body where they are referencing the corresponding child entity by using
"books" : { "href" : "http://localhost:8080/authors/1/books" }
Meaning you should also apply this pattern to your request. Otherwise this will not work. HATEOAS allows you to directly reference the related child entities by their corresponding resource path but you need to keep the necessary structure which your posted request body is missing. Further you must support HATEOAS in your WebService / WebApi / Spring Boot Project.
What you could do:
{
"title": "Some title",
"addedByUserId": "1"
}
Then
#PostMapping("/")
fun add(#RequestBody song: Song) =
val userEntity = userRepository.findById(song.getAddedByUserId())
Song newSong = new SongEntity();
// map props
newSong.setUser(userEntity)
repository.save(song)
That code does not work but I hope you get the idea.
Further
In your code you are treating the Request Body as an Entity. Please consider to separate your incoming Class and your Entity class. This would make several things easier.
I think you're missing jackson's kotlin module, it's exactly what it was created for.
https://github.com/FasterXML/jackson-module-kotlin
Just adding this dependency in your project will cause spring to autoconfigure your object mapper with this new module. If you have a Bean with your own created objectMapper then you need to configure it manually, there's a section about this in module's README.md

Spring WebClient Post body not getting passed

I am trying to use WebClient to Post a loan object to another microservice which saves this object in a DB. So theoretically the body (JSON loan object) should just be passed on to the API of the DB service. Somehow, I can't figure out how to accomplish this.
This is the API that accepts the JSON loan object:
Mapping: localhost:8081/loans
#PostMapping
public <T extends Loan> void addLoan(#Valid #NonNull #RequestBody T loan) {
loanService.createLoan(loan);
}
It then calls the loanService which should pass on the loan object to the DB-service API
public <T extends Loan> T createLoan(T loan) {
ParameterizedTypeReference<T> typeReference = new ParameterizedTypeReference<T>(){};
T a = client.post().uri("/loans").body(BodyInserters.fromValue(loan)).retrieve().bodyToMono(typeReference).block();
return a;
}
This is the API of that DB service:
Mapping: localhost:8080/api/v1/loans
#PostMapping
#ResponseBody
public <T extends Loan> T createLoan(#RequestBody T loan) {
return loanService.createLoan(loan);
}
And here is its service:
public <T extends Loan> T createLoan(T Loan) {
return (T) loanRepository.save(Loan);
}
If I just pass a loan object directly to the DB service API, everything works fine. But if I pass it to the other API, I get the following error:
"status": 500,
"error": "Internal Server Error",
"trace": "org.springframework.web.reactive.function.client.WebClientResponseException$InternalServerError: 500 Internal Server Error from POST http://localhost:8080/api/v1/loans/\n\tat org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:201)\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t|_ checkpoint ⇢ 500 from POST http://localhost:8080/api/v1/loans/ [DefaultWebClient]\nStack trace:\n\t\tat org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:201)\n\t\tat org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:216)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106)\n\t\tat reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)\n\t\tat reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:99)\n\t\tat reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127)\n\t\tat reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)\n\t\tat reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:295)\n\t\tat reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)\n\t\tat reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1784)\n\t\tat reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:159)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142)\n\t\tat reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:259)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142)\n\t\tat reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:383)\n\t\tat reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:396)\n\t\tat reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:452)\n\t\tat reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:664)\n\t\tat reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:94)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)\n\t\tat io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)\n\t\tat io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)\n\t\tat io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)\n\t\tat io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:795)\n\t\tat io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:480)\n\t\tat io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378)\n\t\tat io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)\n\t\tat io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)\n\t\tat io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)\n\t\tat java.base/java.lang.Thread.run(Thread.java:832)\n\tSuppressed: java.lang.Exception: #block terminated with an error\n\t\tat reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99)\n\t\tat reactor.core.publisher.Mono.block(Mono.java:1679)\n\t\tat de.rwth.swc.lab.ws2021.daifu.businesslogic.services.LoanService.createLoan(LoanService.java:39)\n\t\tat de.rwth.swc.lab.ws2021.daifu.businesslogic.api.LoanController.addLoan(LoanController.java:28)\n\t\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\t\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)\n\t\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\t\tat java.base/java.lang.reflect.Method.invoke(Method.java:564)\n\t\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)\n\t\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:807)\n\t\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\t\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1061)\n\t\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961)\n\t\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\t\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\t\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:652)\n\t\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\t\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:733)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\n\t\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\t\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\n\t\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)\n\t\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\t\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\t\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\t\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)\n\t\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\t\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\n\t\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)\n\t\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\t\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)\n\t\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)\n\t\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\t\tat java.base/java.lang.Thread.run(Thread.java:832)\n",
"message": "500 Internal Server Error from POST http://localhost:8080/api/v1/loans/",
"path": "/loans/"
This is the server-side error:
Servlet.service() for servlet [dispatcherServlet] in context with path [/api/v1] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer] with root cause org.hibernate.PropertyValueException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer
And finally, this is the POST-body:
{
"amount": 10000.00,
"balance": -2000.00,
"customer": {"id": 1},
"interest": 0.06,
"status": "TIMELY",
"reason": "Some reaseon",
"type": "privateLoan"
}
The error says the "not-null property references a null or transient value" but the exact same request works for a direct POST-request to the 2nd API which doesn't make sense to me.
Here is the loan class:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
#Getter
#Setter
#NoArgsConstructor
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = CarLoan.class, name = "carLoan"),
#JsonSubTypes.Type(value = ConstructionLoan.class, name = "constructionLoan"),
#JsonSubTypes.Type(value = Mortgage.class, name = "mortgage"),
#JsonSubTypes.Type(value = PrivateLoan.class, name = "privateLoan"),
#JsonSubTypes.Type(value = PropertyLoan.class, name = "propertyLoan")
})
#ApiModel(
discriminator = "type",
subTypes = {CarLoan.class, ConstructionLoan.class, Mortgage.class, PrivateLoan.class, PropertyLoan.class}
)
public abstract class Loan {
#Id
#GeneratedValue(strategy = GenerationType.TABLE)
#Column(name = "id")
#ApiModelProperty(required = false, hidden = true)
protected Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "customer_id", nullable = false)
#JsonBackReference(value = "customer-loans")
protected Customer customer;
#OneToMany(mappedBy = "loan", cascade = CascadeType.ALL)
#JsonManagedReference(value = "loan-loanRates")
private Set<LoanRate> loanRates;
#NonNull
protected Double amount;
#NonNull
protected Double interest;
#NonNull
protected Double balance;
#NonNull
protected LoanStatus status;
public enum LoanStatus {
TIMELY("timely"),
GRACE_PERIOD("grace period"),
DEFAULT("default"),
DEFICIT("deficit"),
IRRECOVERABLE_DEBT("irrecoverable debt"),
CLOSED("closed");
#Getter
private String stringRepresentation;
private LoanStatus(String s) {
this.stringRepresentation = s;
}
}
public <T extends Loan> boolean isOfSameInstance(T otherLoan) {
return (this.getClass().equals(otherLoan.getClass()));
}
}
Let me know if I should post anything else.
Thanks in advance.
The problem is due to the models being used in the projects. As your are reusing the model classes of the one webservice which offers the CRUD api for the backend, you are also reusing the jackson's #JsonManagedReference and #JsonBackReference. This leads to null values for the models being defined as the back reference, such as the customer in you loan class. Jackson does not serialize such tagged objects to JSON in order to not run into a stackoverflow due to infinite recursion. Thus, when you serialize a loan model in your service and send the request to the other service, jackson nulls the back reference, e.g. customer in the loan model and the 2nd webservice therefore receives an invalid loan model, since a loan model is required to have a customer not to be null.
I suggest to either remove the jackson annotations from the models in the service you develop, which would required copy pasted model classes (on the one side the classes using the required jackson annotations in the web service, and on the other side the classes not using these in the other web service). However, this solution has the typical disadvantages of duplicated code. The more elegant but more complicated solution will be to implement a custom jackson serializer and deserializer by specializing jackson's StdSerializer<Loan> and StdDeserializer<Loan>. These custom serializers and deserializers should override its serialize(T value, JsonGenerator gen, SerializerProvider provider) respectively its deserialize(JsonParser, DeserializationContext) method such that the #JsonManagedReference and #JsonBackReference, as well as, if being used, #JsonIgnore annotations in the model are being ignored.
It might be sufficient to just implement a custom serializer. However, I guess that you will also run into problems when receiving a response from the other web service when not using a custom deserializer.
This error may happen if the customer object in the loan you are trying to save is null or not yet added to the database (even though it is set in loan). You should check before saving the Loan in the DB if customer is null or not. If not, and if it is a customer not yet in the database, you should consider adding it first or specify CascadeType.PERSIST in the relation type annotation. In any case, it would be better if you post the entire model that both services are using.

How to do not send #IdClass object in Spring JSON queries

I'm setting a server to get a CRUD api from a postgresql Database using JPA. Everytime I want to expose an object from the DB it duplicate the idObject.
When I get an object from the database using springframework and send it after that, it duplicate the idObject like this:
{
"siteId": 3,
"contractId": "1",
"name": "sitenumber1",
"siteIdObject": {
"siteId": 3,
"contractId": "1"
}
}
SiteId and contractId are repeating...
but I want something like that:
{
"siteId": 3,
"contractId": "1",
"name": "sitenumber1"
}
I want to avoid using DTO because I think there is a better way but I don't find it. Since I'm using springFramework for just one or two month I'm maybe forgeting something...
there is the code:
Site code:
#Entity
#IdClass(SiteId.class)
#Table(name = "site", schema="public")
public class Site {
#Id
#Column(name="siteid")
private Integer siteId;
#Id
#Column(name="clientid")
private Integer contractId;
private String name;
#JsonIgnore
#OneToMany(cascade=CascadeType.ALL, mappedBy = "site")
public Set<Device> devices;
//setter, getter, hash, equals, tostring, constructor empty one and full one
SiteId code:
public class SiteId implements Serializable {
private Integer siteId;
private Integer contractId;
// setter, getter, constructor empty and full, hash and equals
Thanks to help :)
Bessaix Daniel
If you are using Spring you might also be using Jackson so if you annotate your SiteIdclass with #JsonIgnoreType it shouldn't be serialized at all when the Site object is serialized.
I am however unsure if this will break your application logic now that the id object is not serialized anymore.

Spring Data Rest & Lombok - Exception while adding adding relation

In my project I have 2 entities. Survey and entries to survey. They are in relation one to many (thare can be many entries to one survey).
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#Entity
#Table(name = "survey_entries")
#TypeDef(name = "SurveyEntry", typeClass = SurveyEntry.class)
public class SurveyEntryEntity extends AbstractEntity {
#ManyToOne
#JoinColumn(name = "survey_id")
private SurveyEntity survey;
#NonNull
#Type(type = "SurveyEntry")
#Column(name = "responses")
// JSON db column type mapped to custom type
private SurveyEntry responses;
}
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#Entity
#Table(name = "surveys")
#TypeDef(name = "Survey", typeClass = Survey.class)
public class SurveyEntity extends AbstractEntity {
#NonNull
#Type(type = "Survey")
#Column(name = "template")
// JSON db column type mapped to custom type
private Survey survey;
#OneToMany(mappedBy = "survey")
private List<SurveyEntryEntity> entries;
}
I have also created 2 rest repositories using Spring Data Rest:
#RepositoryRestResource(collectionResourceRel = "survey_entries", path = "survey-entries")
public interface SurveyEntryRepository extends PagingAndSortingRepository<SurveyEntryEntity, Long> {
}
#RepositoryRestResource(collectionResourceRel = "surveys", path = "surveys")
public interface SurveyRepository extends PagingAndSortingRepository<SurveyEntity,Long> {
}
I have successfully added survey by rest POST request and I can access it entries (currently empty) by sending GET to /api/surveys/1/entries.Now I want to add entry to exisiting survey. And while I can add it by sending POST (content below) to /api/survey-entries I have troubles adding it directly as a reference to survey. I'm using POST method with the same content and url /api/surveys/1/entries. What is interesting, I'm getting NullPointerException in logs and entry is not inserted but audit modify timestamp in survey is changed. What am I doing wrong? Did I miss same configuration? Or should I use different content?
Content of POST with entry:
{
"responses": {
"question1": "response1",
"question2": "response2",
"question3": "response3"
}
}
Content of POST with survey:
{
"survey": {
//survey structure
}
}
Exception:
08:41:14.730 [http-nio-8080-exec-3] DEBUG org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod - Failed to resolve argument 1 of type 'org.springframework.data.rest.webmvc.PersistentEntityResource'
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: No content to map due to end-of-input; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
#EDIT
I have tried adding entry by POST to /api/survey-entries with 'application/hal+json' Content-Type header and content as below, but now I'm getting other exception:
Content:
{
"survey" : "http://localhost:8080/api/surveys/1",
"responses": {
"question1": "response1",
"question2": "response2",
"question3": "response3"
}
}
Exception:
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.domain.SurveyEntity` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/surveys/1')
at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 41] (through reference chain: com.domain.SurveyEntryEntity["survey"])
#Edit 2
Added Lombok annotations present on Entity classess
Unfortunatelly problem lied in Lombok annotations which weren't included in sample code. I added them now so any one can see where the problem lies.
I managed to solve it by downgrading Lombok to version (1.16.14) and changing annotation #AllArgsConstructor to #AllArgsConstructor(suppressConstructorProperties = true). It's immposible to achieve in later Lombok versions as this property is currently removed.
I have found solution on Spring Data Rest JIRA. There is already issue DATAREST-884 mentioning problem and presenting solution/workaround.
Sorry for wasted time while it was impossible to see solution without all the code.

Resources