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

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" );

流程会变成:

  1. 找到已存在的doc1(普通Document),将其转换为MyDocument并替换父文档中的doc1
  2. 找到已存在的doc2(普通Document),转换为MyDocument并替换doc1中的doc2
  3. 调用put("test","yes")时,修改的是关联在父文档链中的MyDocument
  4. save()方法向上找到根文档,此时根文档已经包含了所有变更,replaceOne会正确更新数据库

额外注意事项

  • 递归转换子文档:如果你的文档有多层嵌套,第一次访问子文档时会自动转换成MyDocument,后续操作都没问题。如果想在初始化PlayersDataDB时就把整个文档结构转换成MyDocument,可以写一个递归方法批量转换。
  • 线程安全:Minecraft插件是多线程环境,要注意数据库操作的线程安全,建议把数据库操作放到异步线程中执行,避免阻塞主线程。
  • 日志规范:把System.out换成Minecraft的Logger,这样日志会输出到插件的日志文件中,更便于排查问题。

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

火山引擎 最新活动