JPA一对一关联中Booking侧关联字段始终为空的问题排查
解决Booking表中关联TABooking字段为Null的问题
问题背景
有两个实体:Booking和TABooking,其中Booking可独立存在,TABooking必须关联一个Booking。
实体代码
TABookingEntity
@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; 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
@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; } }
业务逻辑与问题现象
业务流程:先创建Booking,关联到TABooking后保存TABooking,再尝试更新Booking:
Booking booking = flightService.createBooking(tabooking.getFlightbooking()); tabooking.setFlightbooking(booking); // 省略部分代码 tabookingService.create(tabooking); flightService.updateBooking(tabooking.getFlightbooking().getId(), tabooking.getFlightbooking());
执行后,TABooking表数据正常,但Booking表中关联TABooking的travelAgentBooking_id字段始终为Null。
更新的接口与Repository代码
UpdateBooking接口
@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) { 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(); }
BookingRepository的update方法
BookingEntity update(BookingEntity booking) { log.info("BookingRepository.update() - Updating " + booking.getId()); em.merge(booking); return booking; }
原因分析
- Update接口未使用传入的Booking对象:接口中两次调用
bookingService.update(id),但未传入包含关联信息的booking对象,导致TABooking的关联数据根本没被持久化。 - 游离对象的关联未同步:虽然
TABooking.setFlightbooking()会设置Booking.travelAgentBooking,但如果Booking此时处于游离状态(已持久化但不在当前EntityManager上下文),修改后的关联不会自动同步到数据库。 - 事务边界不完整:如果
tabookingService.create(tabooking)和flightService.updateBooking()不在同一个事务中,Booking的修改可能无法正确提交。
解决方案
1. 修正Update接口逻辑,传入完整的Booking对象
修改updateBooking方法中调用bookingService.update的逻辑,将包含关联信息的booking对象传入:
// 替换原来的两次bookingService.update(id); try { // 传入修改后的booking对象,确保关联信息被持久化 bookingService.update(booking); } 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方法,确保接收BookingEntity参数并调用Repository:
// BookingService层的update方法 public void update(BookingEntity booking) throws ServiceException { Optional<BookingEntity> existing = bookingRepository.findById(booking.getId()); if (!existing.isPresent()) { throw new ServiceException("Booking not found"); } bookingRepository.update(booking); }
2. 优化业务流程的事务覆盖
将创建Booking、关联TABooking、更新Booking的逻辑放在同一个事务中,避免游离对象问题:
// 确保这段代码处于同一个事务上下文 @Transactional public void createBookingWithTABooking(TABooking tabooking) { Booking booking = flightService.createBooking(tabooking.getFlightbooking()); tabooking.setFlightbooking(booking); tabookingService.create(tabooking); // 直接更新关联后的Booking,无需调用远程接口 bookingService.update(booking); }
3. 可选:调整双向关联的维护方
对于双向@OneToOne关联,明确维护方可以简化持久化逻辑。将Booking设为关联维护方(因为它存储外键travelAgentBooking_id):
// TABookingEntity的flightbooking字段修改为反向映射 @OneToOne(fetch = FetchType.EAGER, mappedBy = "travelAgentBooking") private BookingEntity flightbooking; // BookingEntity的travelAgentBooking字段保留外键配置 @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "travelAgentBooking_id", nullable = true) private TABookingEntity travelAgentBooking;
此时修改Booking.travelAgentBooking会直接更新数据库字段,TABooking的关联仅作为反向查询使用。
验证方案
修改后执行业务流程,检查Booking表的travelAgentBooking_id字段是否被正确填充为TABooking的ID,同时确认TABooking表的booking_id关联正常。
内容的提问来源于stack exchange,提问作者akanesora
相关产品推荐
相关产品推荐

