You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

GWT RPC与Flutter API调用下Hibernate事务行为差异排查:用户站点迁移函数无法找到关联数据

GWT RPC与Flutter API调用下Hibernate事务行为差异排查:用户站点迁移函数无法找到关联数据

问题背景梳理

先把你的场景理清楚:我们正在把GWT开发的DAQ系统逐步迁移到Flutter(为了兼顾移动端兼容性),后端通过REST API和Flutter交互。在用户跨组织迁移的功能中,遇到了非常诡异的行为:

  • 用GWT RPC调用带@Transactional注解的saveUser函数时,关联的saveAndMoveSites能正常拉取用户的所有站点(Area of Interest),并把这些站点同步迁移到新组织;
  • 但用Flutter的POST请求调用同一个saveUser时,saveAndMoveSites完全找不到用户的任何站点,导致迁移直接失败。

两者传入的DTO完全一致(sites字段都是空的),但结果天差地别。你的临时解决方法是:如果是Flutter发起的请求,就先执行saveAndMoveSites再做DTO到实体的映射,这样就能正常工作。


核心差异分析:GWT RPC vs REST API的Hibernate事务/会话逻辑

要搞懂这个问题,得从GWT RPC和Spring MVC(REST API)的请求模型,以及Hibernate的持久化上下文管理说起:

1. GWT RPC的会话绑定特性

GWT RPC是基于Servlet的双向通信机制,它的请求和后端的HttpSession、Hibernate的Session绑定得更紧密。在GWT的调用链里:

  • 可能在saveUser执行前,User实体已经被加载到Hibernate的一级缓存(Session缓存)里了(比如GWT前端之前的查询操作);
  • 当mapper把空sites的DTO映射到User实体时,虽然设置了sites为null,但Hibernate的持久化上下文会保留实体的原始状态(除非显式触发更新),或者saveUser里对User的修改还没同步到数据库;
  • 这时候saveAndMoveSites去查用户的sites,Hibernate会优先从一级缓存拿完整的User实体(包含原始的sites关联),或者直接查数据库(还没执行update操作),所以能找到所有站点。

2. REST API(Flutter调用)的无状态与实体映射干扰

Flutter通过REST API调用时,请求是无状态的,后端会为每个请求新建事务上下文:

  • mapper把空sites的DTO映射到User实体后,User的sites字段直接被设为null;
  • 因为saveUser标注了@Transactional,Hibernate会在事务内跟踪这个User的状态变化;
  • saveAndMoveSites尝试查询sites之前,Hibernate可能已经触发了脏检查,把User的sites=null状态同步到了数据库(或者一级缓存里的sites值已经被覆盖);
  • 这时候saveAndMoveSites去查用户的sites,要么从缓存拿到null,要么查到数据库里已经被清空的sites,自然找不到任何数据。

3. 关键:DTO映射时机对Hibernate状态的影响

不管是GWT还是Flutter,DTO的sites都是空的,但映射时机不同导致结果差异:

  • GWT场景:映射后的User实体可能还在Hibernate的持久化上下文里,但RPC的会话特性让原始的sites关联没被立即覆盖;
  • Flutter场景:映射操作在事务早期就把User的sites设为null,Hibernate的事务上下文立刻感知到这个变化,直接干扰了后续的关联数据查询。

问题本质与彻底解决方案

本质总结

这个问题的核心是事务内实体状态的修改时机,和Hibernate一级缓存、脏检查机制的交互差异。GWT RPC的会话绑定特性让实体的原始关联数据在事务内得以保留,而REST API的无状态特性让实体映射的空值直接破坏了后续的关联数据查询。

彻底优化方案

你的临时解决方案(根据调用方调整映射时机)能解决问题,但不够优雅。可以从这几个方向优化:

  1. 分离实体映射与关联数据操作

    • 先不要直接把DTO映射到User实体并修改状态,而是根据用户ID从数据库加载完整的User实体(包含sites关联);
    • 先执行saveAndMoveSites迁移站点;
    • 最后再把DTO里的其他字段(除了sites)映射到已加载的User实体,执行保存。
    • 这样不管调用方是GWT还是Flutter,迁移sites时用户的关联数据都是完整的。

    伪代码示例:

    @Transactional
    public User saveUser(UserDTO dto) {
        // 1. 从数据库加载完整的用户实体(包含sites关联)
        User existingUser = userRepository.findById(dto.getId()).orElseThrow(() -> new UserNotFoundException());
        // 2. 执行站点迁移逻辑
        saveAndMoveSites(existingUser, dto.getNewOrganizationId());
        // 3. 映射DTO的其他字段到现有实体(跳过sites字段)
        mappedFacade.mapExcludingSites(dto, existingUser);
        // 4. 保存最终的用户实体
        return userRepository.save(existingUser);
    }
    
  2. 调整Hibernate的关联加载或事务传播策略

    • 检查User类中sites字段的@OneToMany配置,确保eager fetch是合理的;或者在saveAndMoveSites里用entityManager.refresh(existingUser)从数据库重新加载关联数据;
    • 可以把saveAndMoveSites的事务传播行为设为REQUIRES_NEW,让它在独立事务中执行,避免受之前实体映射操作的影响,但要注意事务边界的一致性。
  3. 优化DTO设计,明确语义

    • 当前DTO的空sites字段语义模糊:既可能是“清空用户的sites”,也可能是“不修改用户的sites”;
    • 可以在DTO里加一个boolean clearSites字段,只有当这个字段为true时,才把User的sites设为空;否则保留原关联。这样映射器就不会随意覆盖关联数据。

验证方法

可以在saveUser里加日志或断点,观察这些内容:

  • 映射后的User实体的sites字段值;
  • 调用saveAndMoveSites前后,数据库中用户的sites关联状态;
  • 开启Hibernate的SQL日志,看是否在saveAndMoveSites之前执行了更新User sites的SQL语句。

通过这些验证,能明确是Hibernate缓存还是事务内脏检查导致的问题,从而针对性解决。

火山引擎 最新活动