Home > Software design >  java jpa one to one relation is always null for one side?
java jpa one to one relation is always null for one side?

Time:11-11

I have two entity booking and travelAgentBooking, booking could exist by itself while travelAgentBooing must have one booking of it.

TABookingEntity is below

@Entity
@ApplicationScoped
@Table(name = "TABooking")
@NamedQuery(name = "TABooking.findAll", query = "SELECT t FROM TABookingEntity t ORDER BY t.id ASC")
public class TABookingEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "TABookingId_seq")
    @SequenceGenerator(name = "TABookingId_seq", initialValue = 1, allocationSize = 1)
    private Long id;

    @OneToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "booking_id", nullable = false)
    private BookingEntity flightbooking;

    // belong to upstream booking so we just store id here
    private Long taxibookingid;

    private Long hotelbookingid;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public BookingEntity getFlightbooking() {
        return flightbooking;
    }

    public void setFlightbooking(BookingEntity flightbooking) {
        this.flightbooking = flightbooking;
        if (flightbooking != null) {
            flightbooking.setTravelAgentBooking(this);
        }
    }

    public Long getTaxibookingId() {
        return taxibookingid;
    }

    public void setTaxibookingId(Long taxibookingid) {
        this.taxibookingid = taxibookingid;
    }

    public Long getHotelbookingId() {
        return hotelbookingid;
    }

    public void setHotelbookingId(Long hotelbookingid) {
        this.hotelbookingid = hotelbookingid;
    }

BookingEntity is below

@Entity
@ApplicationScoped
@Table(name = "booking")
@NamedQueries({ @NamedQuery(name = "Booking.findAll", query = "SELECT b FROM BookingEntity b ORDER BY b.d ASC"),
        @NamedQuery(name = "Booking.findByFlight", query = "SELECT b FROM BookingEntity b WHERE b.flight = :flight"),
        @NamedQuery(name = "Booking.findByDate", query = "SELECT b FROM BookingEntity b WHERE b.d = :d") })
public class BookingEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "bookingId_seq")
    @SequenceGenerator(name = "bookingId_seq", initialValue = 1, allocationSize = 1)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private CustomerEntity customer;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "flight_id", nullable = false)
    private FlightEntity flight;
    
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "travelAgentBooking_id", nullable = true)
    private TABookingEntity travelAgentBooking;

    @NotNull
    @Column(name = "date")
    private Date d;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public CustomerEntity getCustomer() {
        return customer;
    }

    public void setCustomer(CustomerEntity customer) {
        this.customer = customer;
        if(customer != null)
            customer.addBooking(this);
    }

    public FlightEntity getFlight() {
        return flight;
    }

    public void setFlight(FlightEntity flight) {
        this.flight = flight;
    }

    public Date getDate() {
        return new Date(d.getTime());
    }

    public void setDate(Date d) {
        this.d = d;
    }
    
    public TABookingEntity getTravelAgentBooking() {
        return travelAgentBooking;
    }

    public void setTravelAgentBooking(TABookingEntity travelAgentBooking) {
        this.travelAgentBooking = travelAgentBooking;
    }

here is the code I creating booking firstly, and then set it to tabooking.

then I'm trying to update the booking since when it is created, there is no travelAngentBooking for it to associate.

Booking booking = flightService.createBooking(tabooking.getFlightbooking());
tabooking.setFlightbooking(booking);

,,,,,,,,,,,
,,,,,,,,,,,

tabookingService.create(tabooking);
flightService.updateBooking(tabooking.getFlightbooking().getId(), tabooking.getFlightbooking());

After running it the table of travelAgentBooking is perfect. But booking table column referred to travelAgentBooking is always null for any booking object.

UPDATE:

@PUT
    @Path("/{id:[0-9] }")
    @Operation(description = "Update a Booking in the database")
    @APIResponses(value = { @APIResponse(responseCode = "200", description = "Booking updated successfully"),
            @APIResponse(responseCode = "400", description = "Invalid Booking supplied in request body"),
            @APIResponse(responseCode = "404", description = "Booking with id not found"),
            @APIResponse(responseCode = "409", description = "Booking details supplied in request body conflict with another existing Booking"),
            @APIResponse(responseCode = "500", description = "An unexpected error occurred whilst processing the request") })
    @Transactional
    public Response updateBooking(
            @Parameter(description = "Id of Booking to be updated", required = true) @Schema(minimum = "0") @PathParam("id") Integer id,
            @Parameter(description = "JSON representation of Booking object to be updated in the database", required = true) Booking booking) {
    Customer customer = customerService.findById(booking.getCustomer().getId())
            .orElseThrow(() -> new RestServiceException("We can't found customer", Response.Status.BAD_REQUEST));

    if (!customer.equals(booking.getCustomer()))
        throw new RestServiceException("use custoemr's own API for it update", Response.Status.BAD_REQUEST);

    Flight flight = flightService.findById(booking.getFlight().getId())
            .orElseThrow(() -> new RestServiceException("We can't found flight", Response.Status.BAD_REQUEST));

    if (!flight.equals(booking.getFlight()))
        throw new RestServiceException("use custoemr's own API for it update", Response.Status.BAD_REQUEST);

    try {
        bookingService.validateBooking(booking);
    } catch (ConstraintViolationException ce) {
        // Handle bean validation issues
        Map<String, String> responseObj = new HashMap<>();

        for (ConstraintViolation<?> violation : ce.getConstraintViolations()) {
            responseObj.put(violation.getPropertyPath().toString(), violation.getMessage());
        }
        throw new RestServiceException("Bad Request", responseObj, Response.Status.BAD_REQUEST, ce);
    } catch (UniqueFlightWithDateException e) {
        // we are updating an existence flight, so ignore this as expected
    }

    try {
        bookingService.update(id);
    } catch (ServiceException e) {
        Map<String, String> responseObj = new HashMap<>();
        responseObj.put("id", "please ensure the id is associated with this number");
        throw new RestServiceException("Bad Request", responseObj, Response.Status.NOT_FOUND, e);
    }
    bookingService.update(id);
    return Response.ok(booking).build();
}

BookingEntity update(BookingEntity booking) {
    log.info("BookingRepository.update() - Updating "   booking.getId());

    em.merge(booking);
    return booking;
}

CodePudding user response:

From the original posted code, the problem is that you have two very independent unidirectional relationships and only setting one of them. Since they are independent, the other remains null and can not be anything other than null until it is set.

@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "booking_id", nullable = false)
private BookingEntity flightbooking;

The join column sets a foreign key in the "TABooking" table to point at the bookingEntity. It requires this relationship reference be set to populate that foreign key value. Same thing with:

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "travelAgentBooking_id", nullable = true)
private TABookingEntity travelAgentBooking

It creates its own travelAgentBooking_id foreign key column in the "booking" table that will remain null until you update a booking instance and set this reference. If you only set one side, the other will always remain null in the database.

But there are two problems with the model and your expectations. First, from the comments, you didn't intend this to be two separate relationships and instead expect it to be a single bidirectional relationship. For that, you need a single foreign key, and to pick a side that 'owns' it. The side that owns it controls it:

@OneToOne(mappedBy "flightbooking", fetch = FetchType.LAZY)
private TABookingEntity travelAgentBooking

Using mappedBy tells JPA that the other side owns the relationship. The foreign key column then is only set when you set the TABookingEntity.flightbooking reference and save/merge the TABookingEntity instance.

Second is you are using JSON and so Json serialization and assuming it abides by your object model and the JPA mappings. It does not. JPA annotations are for your persistence provider to tell it how to serialize/deserialize your model into the database but mean nothing for JSON serialization (or xml or any other REST formats). You need to tell your JSON tool how to handle your relationships, and that completely depends on how you are going to be expecting and sending the JSON. There are many tutorials and different strategies to deal with this (see this link for a good primer), but easiest is usually just to pick parts of the graph and exclude them with @JsonIgnore:

@OneToOne(mappedBy "flightbooking", fetch = FetchType.LAZY)
@JsonIgnore
private TABookingEntity travelAgentBooking

This will mean that any JSON you receive representing a booking will have a null travelAgentBooking. So if you need to see or set this relationship, your api would have to send/receive TABookingEntity which would still have the flightbooking reference serialized. I picked this way because flightbooking owns the relationship, so it matches JPA, but it doesn't need to. You can and should figure out what works for your client application and it may be different from the JPA mappings. I would expect that bookings always need to know the TABookingEntity and you'll want that sent to the client, so you might put the @JsonIgnore annotation on the other side. If you do, you'll just have to be sure that when you want to change or add TABookingEntity, that you fix the TABookingEntity.flightbooking reference appropriately so that you don't null out the foreign key.

  • Related