You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
}

原因分析

  1. Update接口未使用传入的Booking对象:接口中两次调用bookingService.update(id),但未传入包含关联信息的booking对象,导致TABooking的关联数据根本没被持久化。
  2. 游离对象的关联未同步:虽然TABooking.setFlightbooking()会设置Booking.travelAgentBooking,但如果Booking此时处于游离状态(已持久化但不在当前EntityManager上下文),修改后的关联不会自动同步到数据库。
  3. 事务边界不完整:如果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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 03:21:03