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无法找到数据库来查询关联目标。
你的场景中异常触发的原因
结合你的代码和流程来看,问题出在多线程下的实体对象状态不一致:
- 新创建的
Measure对象一开始是未关联状态,__boxStore为null。 - UI组件拿到这个未关联的
Measure后,直接调用getGPSBaseFrame()(本质是ToOne.getTarget()),此时ObjectBox没有BoxStore引用去查询关联的GPSBaseFrame,就会抛出这个异常。 - 虽然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

