#Infinite recursion in Spring Boot GetMapping Entity, is it normal?

98 messages · Page 1 of 1 (latest)

deft mesa
#

I'm trying to help to colleague. We tried to do bidirectional @OneToMany and @ManyToOne relationship between entities. But when we return One of the entities through PostMan we get infinite objects mapping recursion. I guess Entities joined wrong. We thought that it is ok. Maybe you know how to relate it correctly?
it's car rental information system. Each User can rent vehicles. Each Booking involves 1 vehicle. A user can make many bookings. But each Booking has one User.
Entities are these:

@Entity
@Data
@Table
public class AbstractUser {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String password;
    private String firstName;
    private String lastName;
    private String emailAddress;
    private String userType;

    // Unidirectional
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_Id")
    private Address address;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Booking> bookings;
    
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "vehicle_Id")
    private Vehicle vehicle;
//...
orchid pelicanBOT
#

This post has been reserved for your question.

Hey @deft mesa! Please use /close or the Close Post button above when you're finished. Please remember to follow the help guidelines. This post will be automatically closed after 300 minutes of inactivity.

TIP: Narrow down your issue to simple and precise questions to maximize the chance that others will reply in here.

deft mesa
#
@Entity
@Data
public class Booking {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int bookingId;
    private Date bookingDate;
    private Date bookingFromDate;
    private Date bookingTillDate;
    private String bookingDescription;
    private String bookingStatus;
    private double totalCost;
    private double distance;
    private String payment;
    // bidirectional
    @ManyToOne
    @JoinColumn(name = "userId")
    private AbstractUser user;

    @ManyToOne
    @JoinColumn(name = "vehicle_id")
    private Vehicle vehicle;
//...
#

Controller to return a Booking:

@RestController
@RequestMapping("/api/bookings")
public class BookingController {
//...
@GetMapping("/BookingById/{bookingId}")
    public Booking getBookingById(@PathVariable Integer bookingId) {
        return bookingService.getBookingsById(bookingId);
    }
//...
}

Service:

// to retrieve a booking by Id
    public Booking getBookingsById(Integer bookingId) {
        return bookingRepository.findById(bookingId).orElse(null) ;
    }
#

Postman response:

#

Each booking has a user that has the booking arraylist.

#

Spring console:

#
Hibernate: select b1_0.booking_id,b1_0.booking_date,b1_0.booking_description,b1_0.booking_from_date,b1_0.booking_status,b1_0.booking_till_date,b1_0.distance,b1_0.payment,b1_0.total_cost,u1_0.id,a1_0.address_id,a1_0.city,a1_0.pincode,a1_0.state,u1_0.email_address,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.user_type,v1_0.vehicle_id,v1_0.capacity,v1_0.category,v1_0.charges_per_day,v1_0.description,d1_0.driver_id,d1_0.mobile_number,d1_0.email_address,d1_0.first_name,d1_0.last_name,d1_0.license_number,v1_0.location,v1_0.type,v1_0.vehicle_number,v2_0.vehicle_id,v2_0.capacity,v2_0.category,v2_0.charges_per_day,v2_0.description,d2_0.driver_id,d2_0.mobile_number,d2_0.email_address,d2_0.first_name,d2_0.last_name,d2_0.license_number,v2_0.location,v2_0.type,v2_0.vehicle_number from booking b1_0 left join abstract_user u1_0 on u1_0.id=b1_0.user_id left join address a1_0 on a1_0.address_id=u1_0.address_id left join vehicle v1_0 on v1_0.vehicle_id=u1_0.vehicle_id left join driver d1_0 on d1_0.driver_id=v1_0.driver_id left join vehicle v2_0 on v2_0.vehicle_id=b1_0.vehicle_id left join driver d2_0 on d2_0.driver_id=v2_0.driver_id where b1_0.booking_id=?
//..
2023-02-14T12:01:33.552+02:00  WARN 5840 --- [nio-8080-exec-1] c.c.o.e.advice.MyControllerAdvice        : Response already committed. Ignoring: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError)
2023-02-14T12:01:33.557+02:00  WARN 5840 --- [nio-8080-exec-1] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError)]
#

What are we doing wrong?

#

How to not get booking in booking, if each user has a list of bookings and each booking has a user? It's pretty common bidirectional mappings. But we never realized that we cannot get a booking because of it.

#

How to do it right keeping relationship between them?

#

Database Booking screen:

#

Bookings can be stored well through postman.

orchid pelicanBOT
#

💤 Post marked as dormant

This post has been inactive for over 300 minutes, thus, it has been archived.
If your question was not answered yet, feel free to re-open this post or create a new one.

deft mesa
#

Don't make my post dormant.

modest temple
#

Or I guess "should" create a dto

deft mesa
#

So I got recursion between two entities that are related with bidirectional relationship just because I use actual entity from database rather than intermediate DTO class?

modest temple
deft mesa
#

alright

#

I will create BookingDTO

modest temple
#

There are other advantages to using dto as outlined in the article

orchid pelicanBOT
#

💤 Post marked as dormant

This post has been inactive for over 300 minutes, thus, it has been archived.
If your question was not answered yet, feel free to re-open this post or create a new one.

deft mesa
#

@modest templeYes Data Transfer Objects really helped. I do no get Stack Overflow Error anymore. Entity objects are mapped during new directives written in service using DTO objects getters setters and constructors. I don't show there for a user to get all of his bookings. I show only the data which is really actual in that use case. Thanks.

modest temple
deft mesa
# modest temple No worries glad to know it worked 👍

One more question. Do you now how to change time zone of the returned Date? For example in the database I save Dates that are 2 hours more than Greenwich time UTC+2. pgAdmin of postgreSQL shows correct Date. But when I do mapping in DTO and get the Date from the Entity object parsed from the database I get it already not like it should be (UTC+2), but I get UTC+0. As aresult returned Date in DTOs are by 2 hours less than it should be.
Do you know how to do that it would show correct time zone Date and time?

//...
   booking.setBookingDate(bookingDM.getBookingDate());
//...
modest temple
#

you have 2 options

#

note that the api returning UTC time is not a big deal since - as you mentioned - it's still the right time just in UTC not (i guess) EEST

deft mesa
#

Maybe I'll format it

#

the second option

modest temple
#

i assume the api response is going to some frontend

deft mesa
#

nah to Postman

modest temple
#

yeah but i mean later?

deft mesa
#

later yes

modest temple
#

if it's going to a frontend you can parse the timestamp and format it for the user's local timezone, which is better than just showing whatever timezone itw as persisted at

#

or better still you can use relative terminology like you see on websites like "2 hours ago"

#
jshell> var x = Instant.now();
x ==> 2023-02-15T09:40:29.963781Z

jshell> x.atZone(ZoneId.of("Australia/Sydney"));
$11 ==> 2023-02-15T20:40:29.963781+11:00[Australia/Sydney]

jshell> x.atZone(ZoneId.of("America/New_York"));
$12 ==> 2023-02-15T04:40:29.963781-05:00[America/New_York]
deft mesa
#

but I imagine to set time zone directly in service

#

in command where I set the dto field

#

I should get it and then convert it

#

I expected that it would get the right time

#

how to use that atZone

#

it is LocalDateTime setting. And I have Date

deft mesa
#

@modest templeI have found the way how to set it in the backend in the service:

    booking.setBookingDate(Date.from(bookingDM.getBookingDate().toInstant().atZone(ZoneId.of("UTC+02:00")).toInstant()));
    System.out.println("BookingDate: "+booking.getBookingDate());

Looks like on the backend the Date is correct. The problems happens in the Postman. The Date is showing 2 hours less than it should.

#

Maybe Postman uses it's own locale and automatically converts it two hours less.

#

on Postman it is "bookingDate": "2023-02-11T08:00:00.000+00:00",

#

the browser also shows it 2 hours less if I use that URL directly in address bar.

#

Then maybe I should use the first method - to define booking_from_date as TIMESTAMP WITH TIME ZONE

modest temple
#

if you know all the dates in the db are UTC then it kind of doesn't matter what they are stored as, as long as you know that the offset is 0

#

i wouldn't force it to be UTC+2 really

#

or unless if it's super important what the user's time zone was who persisted the data

deft mesa
#

For it or not.. in the backend everything is ok

#

In service I return it and it is correct

#

but when I get the JSON on web browser or Postman I see that all Date's have time 2 hours less

#

and date format is +0:00

#

I don't know what's the problem

modest temple
deft mesa
#

yes. It could be.

#

But I'm not sure is it like this for real

modest temple
#

try with curl maybe

#

that will just show the actual reuslt

deft mesa
#

ok lets try with curl

modest temple
#

it could also be (i assume you're using) spring converting to UTC

#

so what's it stored in psql atm? UTC?

deft mesa
#

to use it in browser address bar as input or in terminal?

#

I guess in terminal

deft mesa
# modest temple it could also be (i assume you're using) spring converting to UTC

spring was converting such JSON on POST from postman:

{
    "bookingDate": "2023-02-11T10:00:00.000+02:00",
    "bookingDescription": "vehicle booked on 12th Feb",
    "bookingFromDate": "2023-02-12T22:32:00.000+02:00",
    "bookingTillDate": "2023-02-13T23:00:00.000+02:00",
    "bookingStatus": "booked",
    "distance": 60,
    "payment": "cash",
    "totalCost": 120,
    "user": {
        "id": 1
    },
    "vehicle": {
        "vehicleId": 5
    }
}
#

It was when I added

#

I explicitly showed +02:00 in json

#

as a string

#

but when it is time to return as GET. It maps in such a way that it forgets to offset.

modest temple
#

yeah but it might just be returning the value that you POSTed

#

instead of the DB value

deft mesa
#

Service method is like this:

@Transactional(readOnly = true)
public BookingResponseDTO getBookingsById(Integer bookingId) {
//...
    Booking bookingDM = bookingRepository.findById(bookingId).orElse(null);        
    BookingResponseDTO booking = new BookingResponseDTO();
    booking.setBookingId(bookingDM.getBookingId());
    booking.setBookingDate(Date.from(bookingDM.getBookingDate().toInstant().atZone(ZoneId.of("UTC+02:00")).toInstant()));
    System.out.println("BookingDate: "+booking.getBookingDate());
//...
   return booking;
}
#

So I don't know whether Spring himself converts to UTC and ignores time zone offset.

modest temple
#

very likely

#

since it might know that the db doesn't care about offset so just disposes of it

deft mesa
#

I explicitly add two hours in json when I POST

#

and the time numbers I write the same are written in database.

#

when I return from the database as an object I get it correct Like I sent.

#

But perhaps Spring somehow knows that it was utc+2. But returns as just utc

deft mesa
#

@modest templeI have found the way how to achieve my goal. I wanted "bookingDate": "2023-02-15T12:02:15+02:00", such format Date in Postman and browser.
I had to change response DTO Date type to private OffsetDateTime bookingDate;
and in service to convert Date type field from the database to the OffsetDateTime:

//...
  booking.setBookingDate(bookingDM.getBookingDate().toInstant().atZone(ZoneId.of("UTC+02:00")).toOffsetDateTime());
//...
#

Thanks anyway for everything. For helping to solve infinite recursion on data parsing problem.

modest temple
#

Makes sense

#

What was the type before

#

Sorry I didn't think of that

deft mesa
#

DTO field type was Date. Just like in the entity class