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

ObjectBox DbDetachedException异常原因及多线程场景排查求助

关于ObjectBox DbDetachedException异常的解惑

异常信息

Fatal Exception: io.objectbox.exception.DbDetachedException
Cannot resolve relation for detached entities, call box.attach(entity) beforehand.

该异常触发于调用ToOne<>关联的getTarget()方法时(对应代码中measureToDisplay.getGPSBaseFrame())。

场景说明

应用中一个组件接收外部数据并创建Measure实体对象,对象创建后会向独立线程的Recorder和UI组件发送消息:

  • Recorder组件将Measure加入列表,当列表大小达到10时,通过Measure Box的Put()批量存入数据库后清空列表。
  • UI组件负责展示Measure数据,过程中会用到其ToOne<>关联。

应用每秒接收10条测量数据,此前稳定运行数十小时,此次崩溃怀疑是多线程问题,但尚未理解异常含义,寻求解惑。


2025年5月7日补充
抱歉延迟回复,自2月以来未处理该项目。
@Uwe: 未使用手动分配ID。

代码细节

Measure类

@Entity
public class Measure implements Comparable<Measure> {

    @Id
    long id;

    long gpsBaseFrameId;

    @TargetIdProperty("gpsBaseFrameId")
    ToOne<GPSBaseFrame> gpsBaseFrame = new ToOne<>(this, Measure_.gpsBaseFrame);

    // 其他属性

    transient BoxStore __boxStore;

    private static Box<Measure> measureBox = null;

}

GPSBaseFrame类

@Entity
public class GPSBaseFrame {
    @Id
    long id;

    // 其他属性

}

BaseFrameReceiver组件(接收数据)

void newMeasure(){
    lastMeasure = new Measure(lastNavBaseFrame, lastILSBaseFrame, lastGPSBaseFrame);

    // 发送至计算器执行计算
    MeasureCalculator.getInstance().onNewMeasureReceived(lastMeasure);

}

MeasureCalculator组件(独立线程)

public void onNewMeasureReceived(@NonNull Measure measure){
    Message msg = this.measureCalculatorHandler.obtainMessage();
    msg.arg1 = MEAS_CALC_MSG_NEW_SINGLE_MEASURE;
    msg.obj = measure;
    this.measureCalculatorHandler.sendMessage(msg);
}

Handler触发的计算方法:

void computeAllCalculations(@NonNull Measure measure, boolean notify){

    // 多项测量相关计算

    MeasureRecorder.getInstance().onNewMeasureReceived(measure);
    if (notify) { // 此处notify为true
        for (MeasureCalculatorListener listener : listeners) {
            listener.onNewMeasure(measure);
        }
    }
}

1. Recorder组件逻辑(独立线程)

public void onNewMeasureReceived(@NonNull Measure measure){
    Message msg = this.measureRecorderHandler.obtainMessage();
    msg.obj = measure;
    this.measureRecorderHandler.sendMessage(msg);
}

Handler触发的存储方法:

void onNewMeasure(@NonNull Measure measure){
    // 检查测量数据是否可记录且一致
    if(measureIsConsistent) {
        log += "true (replayed: " + measure.isReplayedMeasure() + ")";
        currentMission.addMeasure(measure);
        cachedMeasures.add(measure);
        if (cachedMeasures.size() >= MEASURE_CACHE) { // MEASURE_CACHE = 10
            log += ". 所有缓存测量数据已存入数据库!";
            putCachedMeasuresInDB();
        }
    }
}

private void putCachedMeasuresInDB(){
    // ...
    for (Measure measure : this.cachedMeasures) {
        Measure.getBox().put(measure);
    }
    Mission.getBox().put(currentMission);
    Pass.getBox().put(Objects.requireNonNull(currentMission.getCurrentPass()));
    // ...

}

2. UI组件逻辑(前台Activity)

@Override
public void onNewMeasure(@NonNull Measure measure) {
    // 各类检查
    lastMeasure = measure;
    runOnUiThread(
        updateUIRunnable
    );
    // ...
}

异常发生在updateUIRunnable的run()方法中,触发代码:

GPSBaseFrame gpsBaseFrame = measureToDisplay.getGPSBaseFrame();

异常原因与解决方案

异常本质

这个异常的核心含义是:你正在操作一个「未关联到ObjectBox BoxStore的实体对象」去解析它的关联关系。

ObjectBox中,实体对象分为两种状态:

  • 关联(Attached):对象已被存入数据库,或者通过box.attach()手动关联到BoxStore,此时ObjectBox能通过内部的__boxStore引用找到数据库,从而解析ToOne/ToMany这类关联关系。
  • 未关联(Detached):对象是new出来的、还没存入数据库,或者虽然存过但被序列化/反序列化过(导致__boxStore引用丢失),此时ObjectBox无法找到数据库来查询关联目标。

你的场景中异常触发的原因

结合你的代码和流程来看,问题出在多线程下的实体对象状态不一致:

  1. 新创建的Measure对象一开始是未关联状态,__boxStore为null。
  2. UI组件拿到这个未关联的Measure后,直接调用getGPSBaseFrame()(本质是ToOne.getTarget()),此时ObjectBox没有BoxStore引用去查询关联的GPSBaseFrame,就会抛出这个异常。
  3. 虽然Recorder组件会批量存入数据库,但这个存入操作是异步的:UI可能在Recorder完成put()之前就去访问关联关系,此时Measure还没被关联到BoxStore;就算Recorder完成了put(),原来的Measure对象的__boxStore也不会自动更新——因为put()方法会返回一个新的关联状态的对象,而你代码中还是在操作原来的未关联对象。

解决方案

针对你的场景,有几种可行的处理方式:

方案1:在UI访问关联前确保对象已关联

在UI组件调用getGPSBaseFrame()之前,先检查并关联对象:

// 在updateUIRunnable的run()方法中
if(!measureToDisplay.isAttached()){
    Measure.getBox().attach(measureToDisplay);
}
GPSBaseFrame gpsBaseFrame = measureToDisplay.getGPSBaseFrame();

注意:关联操作是线程安全的,可以在UI线程执行。

方案2:存入数据库后传递关联状态的对象给UI

修改Recorder的putCachedMeasuresInDB()方法,用put()返回的新对象替换缓存中的旧对象,然后再通知UI:

private void putCachedMeasuresInDB(){
    List<Measure> attachedMeasures = new ArrayList<>();
    for (Measure measure : this.cachedMeasures) {
        // put()返回关联状态的对象
        Measure attachedMeasure = Measure.getBox().put(measure);
        attachedMeasures.add(attachedMeasure);
    }
    // 更新缓存和Mission中的对象为关联状态的版本
    cachedMeasures.clear();
    currentMission.getMeasures().clear();
    currentMission.getMeasures().addAll(attachedMeasures);
    
    Mission.getBox().put(currentMission);
    Pass.getBox().put(Objects.requireNonNull(currentMission.getCurrentPass()));
    
    // 可选:如果需要通知UI更新最新的关联对象,可以在这里发送通知
}

同时修改UI的通知逻辑,确保拿到的是已经关联的Measure对象。

方案3:预先关联GPSBaseFrame数据

如果GPSBaseFrame已经提前存入数据库,你可以在创建Measure对象时,直接设置gpsBaseFrameId而不是依赖ToOne的自动关联,这样UI可以直接通过ID查询GPSBaseFrame:

// 在创建Measure时
Measure measure = new Measure(...);
measure.gpsBaseFrameId = lastGPSBaseFrame.id;

// UI中直接查询
GPSBaseFrame gpsBaseFrame = GPSBaseFrame.getBox().get(measure.gpsBaseFrameId);

这种方式绕开了ToOne的关联解析,避免了未关联对象的问题。

额外注意点

  • ObjectBox的实体对象状态与__boxStore强绑定,多线程传递对象时要注意状态一致性。
  • 避免在多个线程中操作同一个未关联实体对象,尤其是涉及到存入数据库和关联查询的场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 14:47:34