【问题标题】:Use allowDiskUse in criteria query with Grails and the MongoDB plugin?在 Grails 和 MongoDB 插件的条件查询中使用 allowDiskUse?
【发布时间】:2015-04-16 16:32:15
【问题描述】:

为了使用 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,但似乎找不到。是否有另一种方法来解决一般问题(迭代大量域对象的所有成员)?

【问题讨论】:

  • 感谢您的清理,Beckwith 先生 :) 最后一句真的是想问“有没有更好的方法来解决一般问题?”

标签: mongodb grails


【解决方案1】:

这在 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 的元类还有另一种骇人听闻的方法。让我们做吧..:-)

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

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

然后,替换 builder 方法以提供您的实现。

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的代码,因为没有变化)

*** 重点就到这里了。因此,每当您在域类中(没有投影和在 mongo 中)编写任何标准时,在执行标准代码后,Grails/gmongo 不会立即从数据库中获取记录,除非您调用诸如 toString()、'size()@ 之类的方法987654333@dump()` 在他们身上。

现在,当您在该实例列表上应用 each 时,您实际上不会将所有实例加载到内存中,而是在幕后迭代 Mongo 游标,在 MongoDB 中,游标使用批量从数据库中提取记录,这非常内存安全。因此,您可以安全地直接在您的标准结果上调用每个,除非您调用任何触发从数据库加载所有记录的方法,否则不会炸毁 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 游标方法来迭代大型集合,这又是内存安全的。

希望这会有所帮助!

【讨论】:

  • 感谢您的回复!
  • 您能否扩展最后两段 - 听起来您有更好的方法,但我不够聪明,无法理解您的意思。我拉入 ID 以进行迭代,而不是尝试拉出对象本身,因为对象很大,而且我可以快速地将 JVM 运行到堆空间之外;我知道单独获取每个对象需要大量的数据库流量,以及对象的创建和销毁,但是我不确定您所说的“另外,如果您遵循此操作,则不必进行上述更改。”是什么意思?再次感谢您。
  • 当然,我已经更新了一个示例和详细说明,包括源代码参考。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-26
  • 1970-01-01
  • 1970-01-01
  • 2015-12-25
  • 1970-01-01
相关资源
最近更新 更多