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

如何阻止Hibernate更新子实体时触发父实体更新?

问题描述

仅需更新子实体ItemPriceEntity,但Hibernate会先执行父实体ProductBundleEntity的更新语句,再更新子实体,尽管已在子实体的@ManyToOne关联上设置了updatable=false。

相关代码

父实体ProductBundleEntity

@Entity
@Table(name = "product_bundles")
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@TypeDefs({@TypeDef(name = "json", typeClass = JsonType.class)})
public class ProductBundleEntity extends BaseEntity {

  @JsonIgnore
  @OneToMany(mappedBy = "productBundle")
  private List<ItemPriceEntity> itemPrices;
}

子实体ItemPriceEntity

@Entity
@Table(name = "item_prices")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@TypeDefs({@TypeDef(name = "json", typeClass = JsonType.class)})
public class ItemPriceEntity extends BaseEntity {

  @ManyToOne
  @JoinColumn(
      name = "item_code",
      referencedColumnName = "product_bundle_code",
      insertable = false,
      updatable = false)
  private ProductBundleEntity productBundle;
}

更新逻辑

public ResponseWrapper processCatalogPayload(ErpCatalogList catalogItems) {
  List<ItemPriceEntity> updateItemPrices = new ArrayList<>();
  catalogItems.getData().forEach(territoryData -> {
    territoryData.getData().forEach(catalogItem -> {
      var itemPrice = itemPriceRepository.findByItemCodeAndTerritory(catalogItem.getItemCode(), catalogItem.getTerritory());
      if (itemPrice.isPresent()) {
        itemPrice.get().setStock(catalogItem.getActualQty());
        itemPrice.get().setProjectedStock((catalogItem.getProjectedQty() == null)? 0 : catalogItem.getProjectedQty());
        itemPrice.get().setLastModifiedDate(Instant.now());
        updateItemPrices.add(itemPrice.get());
      }
    });
  });

  if (!updateItemPrices.isEmpty()) {
    var updatedItems = itemPriceRepository.saveAll(updateItemPrices);
    log.info("Updated item prices: {} ", updatedItems);
    return ResponseWrapper.builder()
            .code(HttpStatus.OK.value())
            .message("Item prices updated successfully")
            .build();
  }
  return ResponseWrapper.builder()
          .code(HttpStatus.NOT_FOUND.value())
          .message("No Item prices found to update")
          .build();
}

SQL日志片段

Hibernate: select productbun0_.id as id1_10_0_, productbun0_.active as active2_10_0_, productbun0_.creation_date as creation3_10_0_, productbun0_.last_modified_date as last_mod4_10_0_, productbun0_.display_item_group as display_5_10_0_, productbun0_.image as image6_10_0_, productbun0_.items as items7_10_0_, productbun0_.price_list_set as price_li8_10_0_, productbun0_.product_bundle_code as product_9_10_0_, productbun0_.product_bundle_description as product10_10_0_, productbun0_.product_bundle_group as product11_10_0_, productbun0_.promo_bundle as promo_b12_10_0_ from product_bundles productbun0_ where productbun0_.product_bundle_code=?
Hibernate: update product_bundles set active=?, last_modified_date=?, display_item_group=?, image=?, items=?, price_list_set=?, product_bundle_code=?, product_bundle_description=?, product_bundle_group=?, promo_bundle=? where id=?
Hibernate: update item_prices set active=?, last_modified_date=?, currency=?, discount_price=?, display_name=?, item_code=?, product_bundle_group=?, projected_stock=?, selling_price=?, stock=?, territory=?, territory_status=?, uom=?, warehouse=? where id=?

原因分析

Hibernate的脏检查机制是核心问题:

  1. 默认情况下@ManyToOne是EAGER(立即加载),查询ItemPriceEntity时会自动加载关联的ProductBundleEntity实例,该实例进入Hibernate Session的持久化上下文。
  2. Hibernate会在事务flush时(比如saveAll执行时)检查所有持久化状态的实体,若检测到属性变化(比如BaseEntity中的last_modified_date被自动更新、实体属性被意外修改),就会生成更新语句。
  3. 你设置的updatable=false仅控制子实体是否能修改关联字段(item_code),不影响父实体本身的脏检查逻辑。

解决方案

1. 将父实体关联改为懒加载

修改子实体的@ManyToOne注解,设置fetch = FetchType.LAZY,避免查询子实体时自动加载父实体,从根源上避免父实体进入持久化上下文:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
    name = "item_code",
    referencedColumnName = "product_bundle_code",
    insertable = false,
    updatable = false)
private ProductBundleEntity productBundle;

2. 优化查询,仅加载子实体数据

修改itemPriceRepository.findByItemCodeAndTerritory方法,使用投影查询或DTO查询,只获取ItemPriceEntity的必要字段,不关联加载父实体:

// 示例:使用Spring Data JPA的投影
public interface ItemPriceProjection {
    Long getId();
    Integer getStock();
    Integer getProjectedStock();
    // 其他需要更新的字段
}

@Repository
public interface ItemPriceRepository extends JpaRepository<ItemPriceEntity, Long> {
    Optional<ItemPriceProjection> findByItemCodeAndTerritory(String itemCode, String territory);
}

之后在更新逻辑中,将投影转换为实体或直接使用实体进行更新(若使用投影,需额外查询实体但确保不加载父实体)。

3. 分离父实体(若必须加载)

如果业务逻辑需要加载父实体,可在更新子实体前将父实体从Session中分离,阻止Hibernate跟踪其状态:

// 在获取itemPrice后执行
if (itemPrice.isPresent()) {
    ItemPriceEntity entity = itemPrice.get();
    // 分离父实体
    entityManager.detach(entity.getProductBundle());
    // 执行更新操作
    entity.setStock(catalogItem.getActualQty());
    // ...其他更新
    updateItemPrices.add(entity);
}

4. 检查BaseEntity的自动更新逻辑

确认BaseEntity中是否存在@PreUpdate或类似注解自动更新last_modified_date等字段:

// 若BaseEntity有如下逻辑,会导致父实体被标记为脏数据
@PreUpdate
public void preUpdate() {
    this.lastModifiedDate = Instant.now();
}

如果父实体不需要自动更新该字段,可移除该注解,或针对父实体单独处理。


内容的提问来源于stack exchange,提问作者arriff

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 20:09:25