在 Grails 和 MongoDB 插件的条件查询中使用 allowDiskUse?

Use allowDiskUse in criteria query with Grails and the MongoDB plugin?

为了使用 Grails (2.5.0) 和 MongoDB 插件 (3.0.2) 遍历 MongoDB (2.6.9) 集合中的所有文档,我创建了一个 forEach像这样:

class MyObjectService {
    def forEach(Closure func) {
        def criteria = MyObject.createCriteria()
        def ids = criteria.list { projections { id() } }
        ids.each { func(MyObject.get(it)) }
    }
}

然后我这样做:

class AnalysisService{
    def myObjectService

    @Async
    def analyze(){
        MyObject.withStatelessSession {
            myObjectService.forEach { myObject ->
                doSomethingAwesome(myObject)
            }
        }
    }
}

这很好用...直到我遇到一个很大的集合(>500K 文档),此时抛出 CommandFailureException,因为聚合结果的大小大于 16MB。

Caused by CommandFailureException: { "serverUsed" : "foo.bar.com:27017" , "errmsg" : "exception: aggregation result exceeds maximum document size (16MB)" , "code" : 16389 , "ok" : 0.0}

在阅读这篇文章时,我认为处理这种情况的一种方法是在 MongoDB 端运行的聚合函数中使用选项 allowDiskUse,这样 16MB 的内存限制就赢了'申请,我可以获得更大的聚合结果。

如何将此选项传递给我的条件查询?我一直在阅读 Grails MongoDB 插件的文档和 Javadoc,但我似乎找不到它。是否有另一种方法来解决一般问题(遍历大量域对象的所有成员)?

MongoDB Grails 插件的当前实现无法做到这一点。 https://github.com/grails/grails-data-mapping/blob/master/grails-datastore-gorm-mongodb/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.java#L957

如果您查看上面的行,您会看到默认选项用于构建 AggregationOptions 实例,因此没有提供选项的方法。

但是使用 Groovy 的 metaclass 还有另一种骇人听闻的方法。让我们开始吧..:-)

在您的服务中编写条件之前,存储 builder() 方法的原始方法参考:

MetaMethod originalMethod = AggregationOptions.metaClass.static.getMetaMethod("builder", [] as Class[])

然后,替换生成器方法以提供您的实现。

AggregationOptions.metaClass.static.builder = { ->
    def builderInstance = new AggregationOptions.Builder()
    builderInstance.allowDiskUse(true)       // solution to your problem
    return builderInstance
}

现在,您的服务方法将通过标准查询调用,并且不会导致您收到的聚合错误,因为我们尚未将 allowDiskUse 属性 设置为 true。

现在,重置原来的方法,这样它就不会影响任何其他调用(可选)。

AggregationOptions.metaClass.static.addMetaMethod(originalMethod)

希望对您有所帮助!

除此之外,为什么在forEach方法中提取所有ID,然后使用get()方法重新获取实例?您正在浪费会影响性能的数据库查询。此外,如果您按照此操作,则不必进行上述更改。

一个相同的例子:(UPDATED)

class MyObjectService {

    void forEach(Closure func) {
        List<MyObject> instanceList = MyObject.createCriteria().list {
            // Your criteria code
            eq("status", "ACTIVE")   // an example
        }

        // Don't do any of this
        // println(instanceList)
        // println(instanceList.size())

        // *** explained below
        instanceList.each { myObjectInstance ->       
            func(myObjectInstance)
        }
    }
}

(我没有添加 AnalysisService 的代码,因为没有变化)

*** 重点就到这里了。所以每当你在域 class 中写入任何条件(没有投影并且在 mongo 中),执行条件代码后, Grails/gmongo 不会立即从数据库中获取记录,除非你调用一些方法就像 toString()、'size()ordump()` 一样。

现在,当您在该实例列表上应用 each 时,您实际上不会将所有实例加载到内存中,而是在幕后和 Mongo 中迭代 Mongo 光标DB,游标使用批处理从数据库中提取记录,这是非常内存安全的。因此,您可以安全地直接调用每个标准结果,这不会破坏 JVM,除非您调用任何触发从数据库加载所有记录的方法。

您甚至可以在代码中确认此行为:https://github.com/grails/grails-data-mapping/blob/master/grails-datastore-gorm-mongodb/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.java#L1775

每当你写任何没有投影的条件时,你都会得到一个 MongoResultList 的实例,并且有一个名为 initializeFully() 的方法正在 toString() 和其他方法上调用。但是,您可以看到 MongoResultList 正在实现迭代器,它依次调用 MongoDB 游标方法来迭代大型集合,这又是内存安全的。

希望对您有所帮助!