MongoDB Java 子文档无法保存问题排查(Minecraft玩家数据插件场景)
问题分析与解决方案
看起来你遇到的核心问题是:当修改已存在的子文档时,你的变更没有同步到父文档的结构中,导致保存根文档时数据库没有更新。我仔细梳理了你的MyDocument代码,发现两个关键漏洞,下面一步步给你解释和修复:
1. 子文档构造方法的明显错误
你的子文档构造方法里有个低级逻辑错误:
public MyDocument( MyDocument parent ) { super( key, value ); // 这里的key和value根本没定义! this.parent = parent; }
这个代码会导致编译错误(或运行时异常),而且逻辑上也不合理——子文档应该初始化一个空的Document,而不是带着未定义的key-value对。修改成这样:
// 子文档构造方法,关联父文档并继承根文档的数据库信息 public MyDocument(MyDocument parent) { super(); // 初始化空Document this.parent = parent; // 继承根文档的集合和查询条件(子文档最终要通过根文档完成保存) this.collectionOfCurrentDoc = parent.collectionOfCurrentDoc; this.keyWhereSave = parent.keyWhereSave; }
2. getEmbedded方法的核心逻辑漏洞
这是导致数据库不更新的关键原因:当子文档已经存在时,你创建了新的MyDocument并复制了数据,但没有把这个新对象替换掉父文档中原来的普通Document。
举个实际场景:父文档里的doc1是一个普通Document,你调用getEmbedded("doc1.doc2", true)时,创建了新的MyDocument并复制doc2的数据,但父文档里的doc1仍然是原来的普通Document——你后续修改这个新的MyDocument时,父文档完全感知不到这些变更,自然保存根文档时数据库不会有更新!
修复后的getEmbedded方法:
public MyDocument getEmbedded(String path, boolean createIfMissing) { MyDocument value = this; for (String key : path.split("\\.")) { Object existing = value.get(key); MyDocument nextEmbedded; if (existing == null) { System.out.println("%s embedded don't exist".formatted(key)); if (!createIfMissing) { return null; } System.out.println("creating embedded %s".formatted(key)); nextEmbedded = new MyDocument(value); value.put(key, nextEmbedded); } else if (existing instanceof MyDocument) { // 已经是MyDocument,直接复用 nextEmbedded = (MyDocument) existing; } else if (existing instanceof Document) { // 将普通Document转换为MyDocument,并替换父文档中的旧对象 System.out.println("embedded already exist, converting to MyDocument"); nextEmbedded = new MyDocument(value); nextEmbedded.putAll((Document) existing); value.put(key, nextEmbedded); // 关键!把父文档中的旧Document替换成新的MyDocument } else { System.out.println("%s is not a document type".formatted(key)); return null; } System.out.println("-".repeat(20)); value = nextEmbedded; } return value; }
3. 优化save方法(可选但推荐)
给replaceOne加上upsert选项,防止极端情况下根文档不存在导致保存失败:
public void save() { MyDocument docToSave = this; while (docToSave.parent != null) { System.out.println("found parent"); docToSave = docToSave.parent; } System.out.println("saving document for query: " + docToSave.keyWhereSave); // 使用upsert:true,确保如果文档不存在也能插入(你的场景中大概率不会触发,但能避免意外) docToSave.collectionOfCurrentDoc.replaceOne( docToSave.keyWhereSave, docToSave, new com.mongodb.client.model.ReplaceOptions().upsert(true) ); }
测试验证
现在你再执行第二次修改代码:
new PlayersDataDB(uuid).getDocument().getEmbedded("doc1.doc2", true ).put("test","yes" );
流程会变成:
- 找到已存在的
doc1(普通Document),将其转换为MyDocument并替换父文档中的doc1 - 找到已存在的
doc2(普通Document),转换为MyDocument并替换doc1中的doc2 - 调用
put("test","yes")时,修改的是关联在父文档链中的MyDocument save()方法向上找到根文档,此时根文档已经包含了所有变更,replaceOne会正确更新数据库
额外注意事项
- 递归转换子文档:如果你的文档有多层嵌套,第一次访问子文档时会自动转换成
MyDocument,后续操作都没问题。如果想在初始化PlayersDataDB时就把整个文档结构转换成MyDocument,可以写一个递归方法批量转换。 - 线程安全:Minecraft插件是多线程环境,要注意数据库操作的线程安全,建议把数据库操作放到异步线程中执行,避免阻塞主线程。
- 日志规范:把
System.out换成Minecraft的Logger,这样日志会输出到插件的日志文件中,更便于排查问题。
内容的提问来源于stack exchange,提问作者arbi12321




